@ -0,0 +1,13 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `company` on the `QuestionsQuestionEncounter` table. All the data in the column will be lost.
|
||||
- Added the required column `companyId` to the `QuestionsQuestionEncounter` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "QuestionsQuestionEncounter" DROP COLUMN "company",
|
||||
ADD COLUMN "companyId" TEXT NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "QuestionsQuestionEncounter" ADD CONSTRAINT "QuestionsQuestionEncounter_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
@ -0,0 +1,60 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "OffersAnalysis" (
|
||||
"id" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"offerId" TEXT NOT NULL,
|
||||
"overallPercentile" INTEGER NOT NULL,
|
||||
"noOfSimilarOffers" INTEGER NOT NULL,
|
||||
"companyPercentile" INTEGER NOT NULL,
|
||||
"noOfSimilarCompanyOffers" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "OffersAnalysis_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_TopOverallOffers" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_TopCompanyOffers" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OffersAnalysis_profileId_key" ON "OffersAnalysis"("profileId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OffersAnalysis_offerId_key" ON "OffersAnalysis"("offerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_TopOverallOffers_AB_unique" ON "_TopOverallOffers"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_TopOverallOffers_B_index" ON "_TopOverallOffers"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_TopCompanyOffers_AB_unique" ON "_TopCompanyOffers"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_TopCompanyOffers_B_index" ON "_TopCompanyOffers"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OffersAnalysis" ADD CONSTRAINT "OffersAnalysis_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "OffersProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OffersAnalysis" ADD CONSTRAINT "OffersAnalysis_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "OffersOffer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_TopOverallOffers" ADD CONSTRAINT "_TopOverallOffers_A_fkey" FOREIGN KEY ("A") REFERENCES "OffersAnalysis"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_TopOverallOffers" ADD CONSTRAINT "_TopOverallOffers_B_fkey" FOREIGN KEY ("B") REFERENCES "OffersOffer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_TopCompanyOffers" ADD CONSTRAINT "_TopCompanyOffers_A_fkey" FOREIGN KEY ("A") REFERENCES "OffersAnalysis"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_TopCompanyOffers" ADD CONSTRAINT "_TopCompanyOffers_B_fkey" FOREIGN KEY ("B") REFERENCES "OffersOffer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "OffersAnalysis" ALTER COLUMN "overallPercentile" SET DATA TYPE DOUBLE PRECISION,
|
||||
ALTER COLUMN "companyPercentile" SET DATA TYPE DOUBLE PRECISION;
|
@ -0,0 +1,11 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "OffersAnalysis" DROP CONSTRAINT "OffersAnalysis_offerId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "OffersAnalysis" DROP CONSTRAINT "OffersAnalysis_profileId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OffersAnalysis" ADD CONSTRAINT "OffersAnalysis_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "OffersProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OffersAnalysis" ADD CONSTRAINT "OffersAnalysis_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "OffersOffer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,14 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Made the column `totalYoe` on table `OffersBackground` required. This step will fail if there are existing NULL values in that column.
|
||||
- Made the column `negotiationStrategy` on table `OffersOffer` required. This step will fail if there are existing NULL values in that column.
|
||||
- Made the column `comments` on table `OffersOffer` required. This step will fail if there are existing NULL values in that column.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "OffersBackground" ALTER COLUMN "totalYoe" SET NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "OffersOffer" ALTER COLUMN "negotiationStrategy" SET NOT NULL,
|
||||
ALTER COLUMN "comments" SET NOT NULL;
|
@ -0,0 +1,27 @@
|
||||
import type { Analysis } from '~/types/offers';
|
||||
|
||||
type OfferPercentileAnalysisProps = Readonly<{
|
||||
companyName: string;
|
||||
offerAnalysis: Analysis;
|
||||
tab: string;
|
||||
}>;
|
||||
|
||||
export default function OfferPercentileAnalysis({
|
||||
tab,
|
||||
companyName,
|
||||
offerAnalysis: { noOfOffers, percentile },
|
||||
}: OfferPercentileAnalysisProps) {
|
||||
return tab === 'Overall' ? (
|
||||
<p>
|
||||
Your highest offer is from {companyName}, which is {percentile} percentile
|
||||
out of {noOfOffers} offers received for the same job type, same level, and
|
||||
same YOE(+/-1) in the last year.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Your offer from {companyName} is {percentile} percentile out of{' '}
|
||||
{noOfOffers} offers received in {companyName} for the same job type, same
|
||||
level, and same YOE(+/-1) in the last year.
|
||||
</p>
|
||||
);
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
import { UserCircleIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
import { HorizontalDivider } from '~/../../../packages/ui/dist';
|
||||
import { formatDate } from '~/utils/offers/time';
|
||||
|
||||
import { JobType } from '../types';
|
||||
|
||||
import type { AnalysisOffer } from '~/types/offers';
|
||||
|
||||
type OfferProfileCardProps = Readonly<{
|
||||
offerProfile: AnalysisOffer;
|
||||
}>;
|
||||
|
||||
export default function OfferProfileCard({
|
||||
offerProfile: {
|
||||
company,
|
||||
income,
|
||||
profileName,
|
||||
totalYoe,
|
||||
level,
|
||||
monthYearReceived,
|
||||
jobType,
|
||||
location,
|
||||
title,
|
||||
previousCompanies,
|
||||
},
|
||||
}: OfferProfileCardProps) {
|
||||
return (
|
||||
<div className="my-5 block rounded-lg border p-4">
|
||||
<div className="grid grid-flow-col grid-cols-12 gap-x-10">
|
||||
<div className="col-span-1">
|
||||
<UserCircleIcon width={50} />
|
||||
</div>
|
||||
<div className="col-span-10">
|
||||
<p className="text-sm font-semibold">{profileName}</p>
|
||||
<p className="text-xs ">Previous company: {previousCompanies[0]}</p>
|
||||
<p className="text-xs ">YOE: {totalYoe} year(s)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HorizontalDivider />
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-x-10">
|
||||
<div className="col-span-1 row-span-3">
|
||||
<p className="text-sm font-semibold">{title}</p>
|
||||
<p className="text-xs ">
|
||||
Company: {company.name}, {location}
|
||||
</p>
|
||||
<p className="text-xs ">Level: {level}</p>
|
||||
</div>
|
||||
<div className="col-span-1 row-span-3">
|
||||
<p className="text-end text-sm">{formatDate(monthYearReceived)}</p>
|
||||
<p className="text-end text-xl">
|
||||
{jobType === JobType.FullTime
|
||||
? `$${income} / year`
|
||||
: `$${income} / month`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { UserCircleIcon } from '@heroicons/react/20/solid';
|
||||
import { HorizontalDivider, Tabs } from '@tih/ui';
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: 'Overall',
|
||||
value: 'overall',
|
||||
},
|
||||
{
|
||||
label: 'Shopee',
|
||||
value: 'company-id',
|
||||
},
|
||||
];
|
||||
|
||||
function OfferPercentileAnalysis() {
|
||||
const result = {
|
||||
company: 'Shopee',
|
||||
numberOfOffers: 105,
|
||||
percentile: 56,
|
||||
};
|
||||
|
||||
return (
|
||||
<p>
|
||||
Your highest offer is from {result.company}, which is {result.percentile}{' '}
|
||||
percentile out of {result.numberOfOffers} offers received in Singapore for
|
||||
the same job type, same level, and same YOE in the last year.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function OfferProfileCard() {
|
||||
return (
|
||||
<div className="my-5 block rounded-lg border p-4">
|
||||
<div className="grid grid-flow-col grid-cols-12 gap-x-10">
|
||||
<div className="col-span-1">
|
||||
<UserCircleIcon width={50} />
|
||||
</div>
|
||||
<div className="col-span-10">
|
||||
<p className="text-sm font-semibold">profile-name</p>
|
||||
<p className="text-xs ">Previous company: Meta, Singapore</p>
|
||||
<p className="text-xs ">YOE: 4 years</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HorizontalDivider />
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-x-10">
|
||||
<div className="col-span-1 row-span-3">
|
||||
<p className="text-sm font-semibold">Software engineer</p>
|
||||
<p className="text-xs ">Company: Google, Singapore</p>
|
||||
<p className="text-xs ">Level: G4</p>
|
||||
</div>
|
||||
<div className="col-span-1 row-span-3">
|
||||
<p className="text-end text-sm">Sept 2022</p>
|
||||
<p className="text-end text-xl">$125,000 / year</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TopOfferProfileList() {
|
||||
return (
|
||||
<>
|
||||
<OfferProfileCard />
|
||||
<OfferProfileCard />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function OfferAnalysisContent() {
|
||||
return (
|
||||
<>
|
||||
<OfferPercentileAnalysis />
|
||||
<TopOfferProfileList />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OfferAnalysis() {
|
||||
const [tab, setTab] = useState('Overall');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
|
||||
Result
|
||||
</h5>
|
||||
<div>
|
||||
<Tabs
|
||||
label="Result Navigation"
|
||||
tabs={tabs}
|
||||
value={tab}
|
||||
onChange={setTab}
|
||||
/>
|
||||
<HorizontalDivider className="mb-5" />
|
||||
<OfferAnalysisContent />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
import Error from 'next/error';
|
||||
import { useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { HorizontalDivider, Spinner, Tabs } from '@tih/ui';
|
||||
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import OfferPercentileAnalysis from '../analysis/OfferPercentileAnalysis';
|
||||
import OfferProfileCard from '../analysis/OfferProfileCard';
|
||||
import { OVERALL_TAB } from '../constants';
|
||||
|
||||
import type {
|
||||
Analysis,
|
||||
AnalysisHighestOffer,
|
||||
ProfileAnalysis,
|
||||
} from '~/types/offers';
|
||||
|
||||
type OfferAnalysisData = {
|
||||
offer?: AnalysisHighestOffer;
|
||||
offerAnalysis?: Analysis;
|
||||
};
|
||||
|
||||
type OfferAnalysisContentProps = Readonly<{
|
||||
analysis: OfferAnalysisData;
|
||||
tab: string;
|
||||
}>;
|
||||
|
||||
function OfferAnalysisContent({
|
||||
analysis: { offer, offerAnalysis },
|
||||
tab,
|
||||
}: OfferAnalysisContentProps) {
|
||||
if (!offerAnalysis || !offer || offerAnalysis.noOfOffers === 0) {
|
||||
return (
|
||||
<p className="m-10">
|
||||
You are the first to submit an offer for these companies! Check back
|
||||
later when there are more submissions.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<OfferPercentileAnalysis
|
||||
companyName={offer.company.name}
|
||||
offerAnalysis={offerAnalysis}
|
||||
tab={tab}
|
||||
/>
|
||||
{offerAnalysis.topPercentileOffers.map((topPercentileOffer) => (
|
||||
<OfferProfileCard
|
||||
key={topPercentileOffer.id}
|
||||
offerProfile={topPercentileOffer}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type OfferAnalysisProps = Readonly<{
|
||||
profileId?: string;
|
||||
}>;
|
||||
|
||||
export default function OfferAnalysis({ profileId }: OfferAnalysisProps) {
|
||||
const [tab, setTab] = useState(OVERALL_TAB);
|
||||
const [allAnalysis, setAllAnalysis] = useState<ProfileAnalysis | null>(null);
|
||||
const [analysis, setAnalysis] = useState<OfferAnalysisData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === OVERALL_TAB) {
|
||||
setAnalysis({
|
||||
offer: allAnalysis?.overallHighestOffer,
|
||||
offerAnalysis: allAnalysis?.overallAnalysis,
|
||||
});
|
||||
} else {
|
||||
setAnalysis({
|
||||
offer: allAnalysis?.overallHighestOffer,
|
||||
offerAnalysis: allAnalysis?.companyAnalysis[0],
|
||||
});
|
||||
}
|
||||
}, [tab, allAnalysis]);
|
||||
|
||||
if (!profileId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getAnalysisResult = trpc.useQuery(
|
||||
['offers.analysis.get', { profileId }],
|
||||
{
|
||||
onError(error) {
|
||||
console.error(error.message);
|
||||
},
|
||||
onSuccess(data) {
|
||||
setAllAnalysis(data);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const tabOptions = [
|
||||
{
|
||||
label: OVERALL_TAB,
|
||||
value: OVERALL_TAB,
|
||||
},
|
||||
{
|
||||
label: allAnalysis?.overallHighestOffer.company.name || '',
|
||||
value: allAnalysis?.overallHighestOffer.company.id || '',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{getAnalysisResult.isError && (
|
||||
<Error
|
||||
statusCode={404}
|
||||
title="An error occurred while generating profile analysis."
|
||||
/>
|
||||
)}
|
||||
{!getAnalysisResult.isError && analysis && (
|
||||
<div>
|
||||
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
|
||||
Result
|
||||
</h5>
|
||||
{getAnalysisResult.isLoading ? (
|
||||
<Spinner className="m-10" display="block" size="lg" />
|
||||
) : (
|
||||
<div>
|
||||
<Tabs
|
||||
label="Result Navigation"
|
||||
tabs={tabOptions}
|
||||
value={tab}
|
||||
onChange={setTab}
|
||||
/>
|
||||
<HorizontalDivider className="mb-5" />
|
||||
<OfferAnalysisContent analysis={analysis} tab={tab} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
import { signIn, useSession } from 'next-auth/react';
|
||||
import { useState } from 'react';
|
||||
import { ChatBubbleBottomCenterIcon } from '@heroicons/react/24/outline';
|
||||
import { Button, HorizontalDivider, TextArea } from '@tih/ui';
|
||||
|
||||
import { timeSinceNow } from '~/utils/offers/time';
|
||||
|
||||
import { trpc } from '../../../../utils/trpc';
|
||||
|
||||
import type { Reply } from '~/types/offers';
|
||||
|
||||
type Props = Readonly<{
|
||||
comment: Reply;
|
||||
disableReply?: boolean;
|
||||
handleExpanded?: () => void;
|
||||
isExpanded?: boolean;
|
||||
profileId: string;
|
||||
replyLength?: number;
|
||||
token?: string;
|
||||
}>;
|
||||
|
||||
export default function CommentCard({
|
||||
comment: { createdAt, id, message, user },
|
||||
disableReply,
|
||||
handleExpanded,
|
||||
isExpanded,
|
||||
profileId,
|
||||
token = '',
|
||||
replyLength = 0,
|
||||
}: Props) {
|
||||
const { data: session, status } = useSession();
|
||||
const [isReplying, setIsReplying] = useState(false);
|
||||
const [currentReply, setCurrentReply] = useState<string>('');
|
||||
|
||||
const trpcContext = trpc.useContext();
|
||||
const createCommentMutation = trpc.useMutation(['offers.comments.create'], {
|
||||
onSuccess() {
|
||||
trpcContext.invalidateQueries([
|
||||
'offers.comments.getComments',
|
||||
{ profileId },
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
function handleReply() {
|
||||
if (token && token.length > 0) {
|
||||
// If it is with edit permission, send comment to API with username = null
|
||||
createCommentMutation.mutate(
|
||||
{
|
||||
message: currentReply,
|
||||
profileId,
|
||||
replyingToId: id,
|
||||
token,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setCurrentReply('');
|
||||
setIsReplying(false);
|
||||
if (!isExpanded) {
|
||||
handleExpanded?.();
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
} else if (status === 'authenticated') {
|
||||
// If not the OP and logged in, send comment to API
|
||||
createCommentMutation.mutate(
|
||||
{
|
||||
message: currentReply,
|
||||
profileId,
|
||||
replyingToId: id,
|
||||
userId: session.user?.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setCurrentReply('');
|
||||
setIsReplying(false);
|
||||
if (!isExpanded) {
|
||||
handleExpanded?.();
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// If not the OP and not logged in, direct users to log in
|
||||
signIn();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex pl-2">
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="flex flex-row font-bold">
|
||||
{user?.name ?? 'unknown user'}
|
||||
</div>
|
||||
<div className="mt-2 mb-2 flex flex-row ">{message}</div>
|
||||
<div className="flex flex-row items-center justify-start space-x-4 ">
|
||||
<div className="flex flex-col text-sm font-light text-gray-400">{`${timeSinceNow(
|
||||
createdAt,
|
||||
)} ago`}</div>
|
||||
{replyLength > 0 && (
|
||||
<div
|
||||
className="flex cursor-pointer flex-col text-sm text-purple-600 hover:underline"
|
||||
onClick={handleExpanded}>
|
||||
{isExpanded ? `Hide replies` : `View replies (${replyLength})`}
|
||||
</div>
|
||||
)}
|
||||
{!disableReply && (
|
||||
<div className="flex flex-col">
|
||||
<Button
|
||||
icon={ChatBubbleBottomCenterIcon}
|
||||
isLabelHidden={true}
|
||||
label="Reply"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onClick={() => setIsReplying(!isReplying)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!disableReply && isReplying && (
|
||||
<div className="mt-2 mr-2">
|
||||
<TextArea
|
||||
isLabelHidden={true}
|
||||
label="Comment"
|
||||
placeholder="Type your comment here"
|
||||
resize="none"
|
||||
value={currentReply}
|
||||
onChange={(value) => setCurrentReply(value)}
|
||||
/>
|
||||
<div className="mt-2 flex w-full justify-end">
|
||||
<div className="w-fit">
|
||||
<Button
|
||||
display="block"
|
||||
isLabelHidden={false}
|
||||
isLoading={createCommentMutation.isLoading}
|
||||
label="Reply"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onClick={handleReply}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<HorizontalDivider />
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import CommentCard from '~/components/offers/profile/comments/CommentCard';
|
||||
|
||||
import type { Reply } from '~/types/offers';
|
||||
|
||||
type Props = Readonly<{
|
||||
comment: Reply;
|
||||
profileId: string;
|
||||
token?: string;
|
||||
}>;
|
||||
|
||||
export default function ExpandableCommentCard({
|
||||
comment,
|
||||
profileId,
|
||||
token = '',
|
||||
}: Props) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
return (
|
||||
<div>
|
||||
<CommentCard
|
||||
comment={comment}
|
||||
handleExpanded={() => setIsExpanded(!isExpanded)}
|
||||
isExpanded={isExpanded}
|
||||
profileId={profileId}
|
||||
replyLength={comment.replies?.length ?? 0}
|
||||
token={token}
|
||||
/>
|
||||
{comment.replies && (
|
||||
<div className="pl-8">
|
||||
{isExpanded &&
|
||||
comment.replies.map((reply) => (
|
||||
<CommentCard
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
disableReply={true}
|
||||
profileId={profileId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
export default function BronzeReviewerBadgeIcon() {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
height="36px"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 36 36"
|
||||
width="36px"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.042 26c.33 0 .651.121.963.331c1.368-8.106 20.362-8.248 21.755-.29c1.666.412 3.08 4.378 3.748 9.959h-31c.793-5.899 2.522-10 4.534-10z"
|
||||
fill="#292F33"></path>
|
||||
<path
|
||||
d="M7.043 23.688C10.966 12.533 6.508 3 17.508 3s8.736 8.173 13.193 19.125c1.119 2.75-1.443 5.908-1.443 5.908s-2.612-4.756-4.75-5.846c-.591 3.277-1.75 6.938-1.75 6.938s-2.581-2.965-5.587-5.587c-.879 1.009-2.065 2.183-3.663 3.462c-.349-1.048-.943-2.339-1.568-3.576c-1.468 2.238-3.182 4.951-3.182 4.951s-2.507-2.435-1.715-4.687z"
|
||||
fill="#E1E8ED"></path>
|
||||
<path
|
||||
d="M11.507 5c-4.36 3.059-5.542 2.16-7.812 3.562c-2.125 1.312-2 4.938-.125 8.062c.579-2.661-.5-3.149 6.938-3.149c5 0 7.928.289 7-1c-.927-1.289-10.027.459-6.001-7.475z"
|
||||
fill="#FFCC4D"></path>
|
||||
<path
|
||||
d="M16.535 7.517a1.483 1.483 0 1 1-2.967 0c0-.157.031-.305.076-.446h2.816c.044.141.075.289.075.446z"
|
||||
fill="#292F33"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
export default function GoldReviewerBadgeIcon() {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
height="36px"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
role="img"
|
||||
viewBox="0 0 36 36"
|
||||
width="36px"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M33.035 28.055c-3.843-2.612-14.989 2.92-15.037 2.944c-.047-.024-11.193-5.556-15.037-2.944C-.021 30.082 0 36 0 36h35.996s.021-5.918-2.961-7.945z"
|
||||
fill="#A0041E"></path>
|
||||
<path
|
||||
d="M32 29c-2.155-1.085-4 0-4 0l-10-1l-10 1s-1.845-1.085-4 0c-3.995 2.011-2 7-2 7h32s1.995-4.989-2-7z"
|
||||
fill="#55ACEE"></path>
|
||||
<path
|
||||
d="M24.056 36c-1.211-1.194-3.466-2-6.056-2s-4.845.806-6.056 2h12.112z"
|
||||
fill="#DD2E44"></path>
|
||||
<path
|
||||
d="M13.64 28.537C15.384 29.805 16.487 30.5 18 30.5c1.512 0 2.615-.696 4.359-1.963V24.29h-8.72v4.247z"
|
||||
fill="#D4AB88"></path>
|
||||
<path
|
||||
d="M30.453 27c-1.953-.266-3.594.547-3.594.547s-.845-.594-1.845-.614c-1.469-.03-2.442.935-3.014 1.755C21.281 29.719 19 30 18 30s-3.281-.281-4-1.312c-.572-.82-1.545-1.784-3.014-1.755c-1 .02-1.845.614-1.845.614S7.5 26.734 5.547 27c-1.305.177-2.357.764-2.846 1.248c2.83-1.685 4.757-.229 6.065.643C10.074 29.763 11 32 11 32c2-1 7-1 7-1s5 0 7 1c0 0 .926-2.237 2.234-3.109c1.308-.872 3.234-2.328 6.065-.643c-.489-.484-1.541-1.071-2.846-1.248z"
|
||||
fill="#DD2E44"></path>
|
||||
<path
|
||||
d="M13.632 25.5c.368 2.027 2.724 2.219 4.364 2.219c1.639 0 4.004-.191 4.363-2.219v-3.019h-8.728V25.5z"
|
||||
fill="#CC9B7A"></path>
|
||||
<path
|
||||
d="M11.444 15.936c0 1.448-.734 2.622-1.639 2.622s-1.639-1.174-1.639-2.622s.734-2.623 1.639-2.623c.905-.001 1.639 1.174 1.639 2.623m16.389 0c0 1.448-.733 2.622-1.639 2.622c-.905 0-1.639-1.174-1.639-2.622s.733-2.623 1.639-2.623c.906-.001 1.639 1.174 1.639 2.623"
|
||||
fill="#D4AB88"></path>
|
||||
<path
|
||||
d="M18 7c-5 0-8 2-8 5s0 9 2 12s4 3 6 3s4 0 6-3s2-9 2-12s-3-5-8-5z"
|
||||
fill="#D4AB88"></path>
|
||||
<path
|
||||
d="M18.821 3.118c6.004.49 8.356 4.246 8.356 7.851c0 3.604-.706 5.047-1.412 3.604c-.706-1.441-1.356-3.368-1.356-3.368s-4.292.485-5.704-.957c0 0 2.118 4.326-2.118 0c0 0 .706 2.884-3.53-.72c0 0-2.118 1.442-2.824 5.046c-.196 1.001-1.412 0-1.412-3.604c.001-2.677.179-6.652 4.908-6.17c1.028-1.639 3.018-1.851 5.092-1.682z"
|
||||
fill="#963B22"></path>
|
||||
<path
|
||||
d="M25 12c-3 0-5 1-7 1s-4-1-7-1s-1 5.72 0 6.72s5-1 7-1s6 2 7 1S28 12 25 12z"
|
||||
fill="#269"></path>
|
||||
<path
|
||||
d="M14 17c-.55 0-1-.45-1-1v-1c0-.55.45-1 1-1s1 .45 1 1v1c0 .55-.45 1-1 1m8 0c-.55 0-1-.45-1-1v-1c0-.55.45-1 1-1s1 .45 1 1v1c0 .55-.45 1-1 1"
|
||||
fill="#88C9F9"></path>
|
||||
<path
|
||||
d="M18.75 19.75h-1.5c-.413 0-.75-.337-.75-.75s.337-.75.75-.75h1.5c.413 0 .75.337.75.75s-.337.75-.75.75m-.75 3.5c-2.058 0-3.594-.504-3.658-.525a.5.5 0 0 1 .316-.949c.014.004 1.455.474 3.342.474s3.328-.47 3.343-.475a.5.5 0 0 1 .316.949c-.065.022-1.601.526-3.659.526z"
|
||||
fill="#C1694F"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
export default function SilverReviewerBadgeIcon() {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
height="36px"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
role="img"
|
||||
viewBox="0 0 36 36"
|
||||
width="36px"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M33 36v-1a6 6 0 0 0-6-6H9a6 6 0 0 0-6 6v1h30zm-6.25-15.565c1.188.208 2.619.129 2.416.917c-.479 1.854-2.604 1.167-2.979 1.188c-.375.02.563-2.105.563-2.105z"
|
||||
fill="#66757F"></path>
|
||||
<path
|
||||
d="M27.062 20.645c1.875.25 2.541.416 1.166.958c-.772.305-2.243 4.803-3.331 4.118c-1.087-.685 2.165-5.076 2.165-5.076z"
|
||||
fill="#292F33"></path>
|
||||
<path
|
||||
d="M9.255 20.435c-1.188.208-2.619.129-2.416.917c.479 1.854 2.604 1.167 2.979 1.188c.375.02-.563-2.105-.563-2.105z"
|
||||
fill="#66757F"></path>
|
||||
<path
|
||||
d="M8.943 20.645c-1.875.25-2.541.416-1.166.958c.772.305 2.243 4.803 3.331 4.118c1.088-.685-2.165-5.076-2.165-5.076z"
|
||||
fill="#292F33"></path>
|
||||
<path
|
||||
d="M21.771 4.017c-1.958-.634-6.566-.461-7.718 1.037c-2.995.058-6.508 2.764-6.969 6.335c-.456 3.534.56 5.175.922 7.833c.409 3.011 2.102 3.974 3.456 4.377c1.947 2.572 4.017 2.462 7.492 2.462c6.787 0 10.019-4.541 10.305-12.253c.172-4.665-2.565-8.198-7.488-9.791z"
|
||||
fill="#FFAC33"></path>
|
||||
<path
|
||||
d="M25.652 14.137c-.657-.909-1.497-1.641-3.34-1.901c.691.317 1.353 1.411 1.44 2.016c.086.605.173 1.094-.374.49c-2.192-2.423-4.579-1.469-6.944-2.949c-1.652-1.034-2.155-2.177-2.155-2.177s-.202 1.526-2.707 3.081c-.726.451-1.593 1.455-2.073 2.937c-.346 1.066-.238 2.016-.238 3.64c0 4.74 3.906 8.726 8.726 8.726s8.726-4.02 8.726-8.726c-.004-2.948-.312-4.1-1.061-5.137z"
|
||||
fill="#FFDC5D"></path>
|
||||
<path
|
||||
d="M18.934 21.565h-1.922a.481.481 0 0 1-.481-.481v-.174c0-.265.215-.482.481-.482h1.922c.265 0 .482.216.482.482v.174a.481.481 0 0 1-.482.481"
|
||||
fill="#C1694F"></path>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M7.657 14.788c.148.147.888.591 1.036 1.034c.148.443.445 2.954 1.333 3.693c.916.762 4.37.478 5.032.149c1.48-.738 1.662-2.798 1.924-3.842c.148-.591 1.036-.591 1.036-.591s.888 0 1.036.591c.262 1.044.444 3.104 1.924 3.841c.662.33 4.116.614 5.034-.147c.887-.739 1.183-3.25 1.331-3.694c.146-.443.888-.886 1.035-1.034c.148-.148.148-.739 0-.887c-.296-.295-3.788-.559-7.548-.148c-.75.082-1.035.295-2.812.295c-1.776 0-2.062-.214-2.812-.295c-3.759-.411-7.252-.148-7.548.148c-.149.148-.149.74-.001.887z"
|
||||
fill="#292F33"
|
||||
fill-rule="evenodd"></path>
|
||||
<path
|
||||
d="M7.858 8.395S9.217-.506 13.79.023c3.512.406 4.89.825 7.833.097c1.947-.482 4.065 1.136 5.342 4.379a27.72 27.72 0 0 1 1.224 4.041s3.938-.385 4.165 1.732c.228 2.117-4.354 4.716-15.889 4.716C10 14.987 3.33 12.63 3.013 10.657c-.317-1.973 4.845-2.262 4.845-2.262z"
|
||||
fill="#66757F"></path>
|
||||
<path
|
||||
d="M8.125 7.15s-.27 1.104-.406 1.871c-.136.768.226 1.296 2.705 1.824c3.287.7 10.679.692 15.058-.383c1.759-.432 2.886-.72 2.751-1.583c-.167-1.068-.196-1.066-.541-2.208c0 0-1.477.502-3.427.96c-2.66.624-9.964.911-13.481.144c-1.874-.41-2.659-.625-2.659-.625zm-.136 13.953c-.354.145 2.921 1.378 7.48 1.458c4.771.084 6.234.39 5.146 1.459c-1.146 1.125-.852 2.894-.771 3.418c.081.524 2.047 1.916 2.208 2.56c.161.645-1.229 5.961-1.229 5.961l-8.729-.252c-2.565-8.844-2.883-8.501-4.105-13.604c-.241-1.008 0-1 0-1z"
|
||||
fill="#292F33"></path>
|
||||
<path
|
||||
d="M6.989 21.144c-.354.146 2.921 1.378 7.48 1.458c4.771.084 6.234.39 5.146 1.459c-1.146 1.125-.664 2.894-.583 3.418c.081.524 1.859 1.916 2.021 2.561c.16.644-1.231 5.96-1.231 5.96l-8.729-.252c-2.565-8.844-2.883-8.501-4.105-13.604c-.24-1.008.001-1 .001-1z"
|
||||
fill="#66757F"></path>
|
||||
<path
|
||||
d="M28.052 21.103c.354.145-2.921 1.378-7.479 1.458c-4.771.084-6.234.39-5.146 1.459c1.146 1.125 2.976 2.892 2.896 3.416c-.081.524-4.172 1.918-4.333 2.562c-.161.645 1.229 5.961 1.229 5.961l8.729-.252c2.565-8.844 2.883-8.501 4.104-13.604c.241-1.008 0-1 0-1z"
|
||||
fill="#292F33"></path>
|
||||
<path
|
||||
d="M28.958 21.103c.354.145-2.921 1.378-7.479 1.458c-4.771.084-6.234.39-5.146 1.459c1.146 1.125 2.977 2.892 2.896 3.416c-.081.524-4.172 1.918-4.333 2.562c-.161.645 1.229 5.961 1.229 5.961l8.657.01c2.565-8.844 2.955-8.763 4.177-13.866c.24-1.008-.001-1-.001-1z"
|
||||
fill="#66757F"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
export const BROWSE_TABS_VALUES = {
|
||||
ALL: 'all',
|
||||
MY: 'my',
|
||||
STARRED: 'starred',
|
||||
};
|
||||
|
||||
export type SortOrder = 'latest' | 'popular' | 'topComments';
|
||||
type SortOption = {
|
||||
name: string;
|
||||
value: SortOrder;
|
||||
};
|
||||
|
||||
export const SORT_OPTIONS: Array<SortOption> = [
|
||||
{ name: 'Latest', value: 'latest' },
|
||||
{ name: 'Popular', value: 'popular' },
|
||||
{ name: 'Top Comments', value: 'topComments' },
|
||||
];
|
||||
|
||||
export const TOP_HITS = [
|
||||
{ href: '#', name: 'Unreviewed' },
|
||||
{ href: '#', name: 'Fresh Grad' },
|
||||
{ href: '#', name: 'GOATs' },
|
||||
{ href: '#', name: 'US Only' },
|
||||
];
|
||||
|
||||
export type FilterOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const ROLE: Array<FilterOption> = [
|
||||
{
|
||||
label: 'Full-Stack Engineer',
|
||||
value: 'Full-Stack Engineer',
|
||||
},
|
||||
{ label: 'Frontend Engineer', value: 'Frontend Engineer' },
|
||||
{ label: 'Backend Engineer', value: 'Backend Engineer' },
|
||||
{ label: 'DevOps Engineer', value: 'DevOps Engineer' },
|
||||
{ label: 'iOS Engineer', value: 'iOS Engineer' },
|
||||
{ label: 'Android Engineer', value: 'Android Engineer' },
|
||||
];
|
||||
|
||||
export const EXPERIENCE: Array<FilterOption> = [
|
||||
{ label: 'Freshman', value: 'Freshman' },
|
||||
{ label: 'Sophomore', value: 'Sophomore' },
|
||||
{ label: 'Junior', value: 'Junior' },
|
||||
{ label: 'Senior', value: 'Senior' },
|
||||
{
|
||||
label: 'Entry Level (0 - 2 years)',
|
||||
value: 'Entry Level (0 - 2 years)',
|
||||
},
|
||||
{
|
||||
label: 'Mid Level (3 - 5 years)',
|
||||
value: 'Mid Level (3 - 5 years)',
|
||||
},
|
||||
{
|
||||
label: 'Senior Level (5+ years)',
|
||||
value: 'Senior Level (5+ years)',
|
||||
},
|
||||
];
|
||||
|
||||
export const LOCATION: Array<FilterOption> = [
|
||||
{ label: 'Singapore', value: 'Singapore' },
|
||||
{ label: 'United States', value: 'United States' },
|
||||
{ label: 'India', value: 'India' },
|
||||
];
|
||||
|
||||
export const TEST_RESUMES = [
|
||||
{
|
||||
createdAt: new Date(),
|
||||
experience: 'Fresh Grad (0-1 years)',
|
||||
numComments: 9,
|
||||
numStars: 1,
|
||||
role: 'Backend Engineer',
|
||||
title: 'Rejected from multiple companies, please help...:(',
|
||||
user: 'Git Ji Ra',
|
||||
},
|
||||
{
|
||||
createdAt: new Date(),
|
||||
experience: 'Fresh Grad (0-1 years)',
|
||||
numComments: 9,
|
||||
numStars: 1,
|
||||
role: 'Backend Engineer',
|
||||
title: 'Rejected from multiple companies, please help...:(',
|
||||
user: 'Git Ji Ra',
|
||||
},
|
||||
{
|
||||
createdAt: new Date(),
|
||||
experience: 'Fresh Grad (0-1 years)',
|
||||
numComments: 9,
|
||||
numStars: 1,
|
||||
role: 'Backend Engineer',
|
||||
title: 'Rejected from multiple companies, please help...:(',
|
||||
user: 'Git Ji Ra',
|
||||
},
|
||||
];
|
@ -0,0 +1,141 @@
|
||||
export type FilterId = 'experience' | 'location' | 'role';
|
||||
|
||||
export type CustomFilter = {
|
||||
numComments: number;
|
||||
};
|
||||
|
||||
type RoleFilter =
|
||||
| 'Android Engineer'
|
||||
| 'Backend Engineer'
|
||||
| 'DevOps Engineer'
|
||||
| 'Frontend Engineer'
|
||||
| 'Full-Stack Engineer'
|
||||
| 'iOS Engineer';
|
||||
|
||||
type ExperienceFilter =
|
||||
| 'Entry Level (0 - 2 years)'
|
||||
| 'Freshman'
|
||||
| 'Junior'
|
||||
| 'Mid Level (3 - 5 years)'
|
||||
| 'Senior Level (5+ years)'
|
||||
| 'Senior'
|
||||
| 'Sophomore';
|
||||
|
||||
type LocationFilter = 'India' | 'Singapore' | 'United States';
|
||||
|
||||
export type FilterValue = ExperienceFilter | LocationFilter | RoleFilter;
|
||||
|
||||
export type FilterOption<T> = {
|
||||
label: string;
|
||||
value: T;
|
||||
};
|
||||
|
||||
export type Filter = {
|
||||
id: FilterId;
|
||||
label: string;
|
||||
options: Array<FilterOption<FilterValue>>;
|
||||
};
|
||||
|
||||
export type FilterState = Partial<CustomFilter> &
|
||||
Record<FilterId, Array<FilterValue>>;
|
||||
|
||||
export type SortOrder = 'latest' | 'popular' | 'topComments';
|
||||
|
||||
export type Shortcut = {
|
||||
customFilters?: CustomFilter;
|
||||
filters: FilterState;
|
||||
name: string;
|
||||
sortOrder: SortOrder;
|
||||
};
|
||||
|
||||
export const BROWSE_TABS_VALUES = {
|
||||
ALL: 'all',
|
||||
MY: 'my',
|
||||
STARRED: 'starred',
|
||||
};
|
||||
|
||||
export const SORT_OPTIONS: Record<string, string> = {
|
||||
latest: 'Latest',
|
||||
popular: 'Popular',
|
||||
topComments: 'Top Comments',
|
||||
};
|
||||
|
||||
export const ROLE: Array<FilterOption<RoleFilter>> = [
|
||||
{
|
||||
label: 'Full-Stack Engineer',
|
||||
value: 'Full-Stack Engineer',
|
||||
},
|
||||
{ label: 'Frontend Engineer', value: 'Frontend Engineer' },
|
||||
{ label: 'Backend Engineer', value: 'Backend Engineer' },
|
||||
{ label: 'DevOps Engineer', value: 'DevOps Engineer' },
|
||||
{ label: 'iOS Engineer', value: 'iOS Engineer' },
|
||||
{ label: 'Android Engineer', value: 'Android Engineer' },
|
||||
];
|
||||
|
||||
export const EXPERIENCE: Array<FilterOption<ExperienceFilter>> = [
|
||||
{ label: 'Freshman', value: 'Freshman' },
|
||||
{ label: 'Sophomore', value: 'Sophomore' },
|
||||
{ label: 'Junior', value: 'Junior' },
|
||||
{ label: 'Senior', value: 'Senior' },
|
||||
{
|
||||
label: 'Entry Level (0 - 2 years)',
|
||||
value: 'Entry Level (0 - 2 years)',
|
||||
},
|
||||
{
|
||||
label: 'Mid Level (3 - 5 years)',
|
||||
value: 'Mid Level (3 - 5 years)',
|
||||
},
|
||||
{
|
||||
label: 'Senior Level (5+ years)',
|
||||
value: 'Senior Level (5+ years)',
|
||||
},
|
||||
];
|
||||
|
||||
export const LOCATION: Array<FilterOption<LocationFilter>> = [
|
||||
{ label: 'Singapore', value: 'Singapore' },
|
||||
{ label: 'United States', value: 'United States' },
|
||||
{ label: 'India', value: 'India' },
|
||||
];
|
||||
|
||||
export const INITIAL_FILTER_STATE: FilterState = {
|
||||
experience: Object.values(EXPERIENCE).map(({ value }) => value),
|
||||
location: Object.values(LOCATION).map(({ value }) => value),
|
||||
role: Object.values(ROLE).map(({ value }) => value),
|
||||
};
|
||||
|
||||
export const SHORTCUTS: Array<Shortcut> = [
|
||||
{
|
||||
filters: INITIAL_FILTER_STATE,
|
||||
name: 'All',
|
||||
sortOrder: 'latest',
|
||||
},
|
||||
{
|
||||
filters: {
|
||||
...INITIAL_FILTER_STATE,
|
||||
numComments: 0,
|
||||
},
|
||||
name: 'Unreviewed',
|
||||
sortOrder: 'latest',
|
||||
},
|
||||
{
|
||||
filters: {
|
||||
...INITIAL_FILTER_STATE,
|
||||
experience: ['Entry Level (0 - 2 years)'],
|
||||
},
|
||||
name: 'Fresh Grad',
|
||||
sortOrder: 'latest',
|
||||
},
|
||||
{
|
||||
filters: INITIAL_FILTER_STATE,
|
||||
name: 'GOATs',
|
||||
sortOrder: 'popular',
|
||||
},
|
||||
{
|
||||
filters: {
|
||||
...INITIAL_FILTER_STATE,
|
||||
location: ['United States'],
|
||||
},
|
||||
name: 'US Only',
|
||||
sortOrder: 'latest',
|
||||
},
|
||||
];
|
@ -1,45 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
import Link from 'next/link';
|
||||
|
||||
const baseStyles = {
|
||||
outline:
|
||||
'group inline-flex ring-1 items-center justify-center rounded-full py-2 px-4 text-sm focus:outline-none',
|
||||
solid:
|
||||
'group inline-flex items-center justify-center rounded-full py-2 px-4 text-sm font-semibold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
};
|
||||
|
||||
const variantStyles = {
|
||||
outline: {
|
||||
slate:
|
||||
'ring-slate-200 text-slate-700 hover:text-slate-900 hover:ring-slate-300 active:bg-slate-100 active:text-slate-600 focus-visible:outline-blue-600 focus-visible:ring-slate-300',
|
||||
white:
|
||||
'ring-slate-700 text-white hover:ring-slate-500 active:ring-slate-700 active:text-slate-400 focus-visible:outline-white',
|
||||
},
|
||||
solid: {
|
||||
blue: 'bg-blue-600 text-white hover:text-slate-100 hover:bg-blue-500 active:bg-blue-800 active:text-blue-100 focus-visible:outline-blue-600',
|
||||
slate:
|
||||
'bg-slate-900 text-white hover:bg-slate-700 hover:text-slate-100 active:bg-slate-800 active:text-slate-300 focus-visible:outline-slate-900',
|
||||
white:
|
||||
'bg-white text-slate-900 hover:bg-blue-50 active:bg-blue-200 active:text-slate-600 focus-visible:outline-white',
|
||||
},
|
||||
};
|
||||
|
||||
export function Button({
|
||||
variant = 'solid',
|
||||
color = 'slate',
|
||||
className,
|
||||
href,
|
||||
...props
|
||||
}) {
|
||||
className = clsx(
|
||||
baseStyles[variant],
|
||||
variantStyles[variant][color],
|
||||
className,
|
||||
);
|
||||
|
||||
return href ? (
|
||||
<Link className={className} href={href} {...props} />
|
||||
) : (
|
||||
<button className={className} type="button" {...props} />
|
||||
);
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
import Image from 'next/future/image';
|
||||
|
||||
import { Button } from './Button';
|
||||
import { Container } from './Container';
|
||||
import backgroundImage from './images/background-call-to-action.jpg';
|
||||
|
||||
export function CallToAction() {
|
||||
return (
|
||||
<section
|
||||
className="relative overflow-hidden bg-blue-600 py-32"
|
||||
id="get-started-today">
|
||||
<Image
|
||||
alt=""
|
||||
className="absolute top-1/2 left-1/2 max-w-none -translate-x-1/2 -translate-y-1/2"
|
||||
height={1244}
|
||||
src={backgroundImage}
|
||||
unoptimized={true}
|
||||
width={2347}
|
||||
/>
|
||||
<Container className="relative">
|
||||
<div className="mx-auto max-w-lg text-center">
|
||||
<h2 className="font-display text-3xl tracking-tight text-white sm:text-4xl">
|
||||
Resume review can start right now.
|
||||
</h2>
|
||||
<p className="mt-4 text-lg tracking-tight text-white">
|
||||
It's free! Take charge of your resume game by learning from the top
|
||||
engineers in the field.
|
||||
</p>
|
||||
<Button className="mt-10" color="white" href="/resumes/browse">
|
||||
Start browsing now
|
||||
</Button>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Container } from './Container';
|
||||
|
||||
export function CallToAction() {
|
||||
return (
|
||||
<section className="relative overflow-hidden py-32" id="get-started-today">
|
||||
<Container className="relative">
|
||||
<div className="mx-auto max-w-lg text-center">
|
||||
<h2 className="font-display text-3xl tracking-tight text-gray-900 sm:text-4xl">
|
||||
Resume review can start right now.
|
||||
</h2>
|
||||
<p className="mt-4 text-lg tracking-tight text-gray-600">
|
||||
It's free! Take charge of your resume game by learning from the top
|
||||
engineers in the field.
|
||||
</p>
|
||||
<Link href="/resumes/browse">
|
||||
<button
|
||||
className="mt-4 rounded-md bg-indigo-500 py-2 px-3 text-sm font-medium text-white"
|
||||
type="button">
|
||||
Start browsing now
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
export function Container({ className, ...props }) {
|
||||
return (
|
||||
<div
|
||||
className={clsx('mx-auto max-w-7xl px-4 sm:px-6 lg:px-8', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import clsx from 'clsx';
|
||||
import type { FC } from 'react';
|
||||
|
||||
type ContainerProps = {
|
||||
children: Array<JSX.Element> | JSX.Element;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Container: FC<ContainerProps> = ({ className, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx('mx-auto max-w-7xl px-4 sm:px-6 lg:px-8', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,50 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Container } from './Container';
|
||||
import { Logo } from './Logo';
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="bg-slate-50">
|
||||
<Container>
|
||||
<div className="py-16">
|
||||
<Logo className="mx-auto h-10 w-auto" />
|
||||
<nav aria-label="quick links" className="mt-10 text-sm">
|
||||
<div className="-my-1 flex justify-center gap-x-6">
|
||||
<Link href="#features">Features</Link>
|
||||
<Link href="#testimonials">Testimonials</Link>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-col items-center border-t border-slate-400/10 py-10 sm:flex-row-reverse sm:justify-between">
|
||||
<div className="flex gap-x-6">
|
||||
<Link
|
||||
aria-label="TaxPal on Twitter"
|
||||
className="group"
|
||||
href="https://twitter.com">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-6 w-6 fill-slate-500 group-hover:fill-slate-700">
|
||||
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0 0 22 5.92a8.19 8.19 0 0 1-2.357.646 4.118 4.118 0 0 0 1.804-2.27 8.224 8.224 0 0 1-2.605.996 4.107 4.107 0 0 0-6.993 3.743 11.65 11.65 0 0 1-8.457-4.287 4.106 4.106 0 0 0 1.27 5.477A4.073 4.073 0 0 1 2.8 9.713v.052a4.105 4.105 0 0 0 3.292 4.022 4.093 4.093 0 0 1-1.853.07 4.108 4.108 0 0 0 3.834 2.85A8.233 8.233 0 0 1 2 18.407a11.615 11.615 0 0 0 6.29 1.84" />
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
aria-label="TaxPal on GitHub"
|
||||
className="group"
|
||||
href="https://github.com">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-6 w-6 fill-slate-500 group-hover:fill-slate-700">
|
||||
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2Z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-6 text-sm text-slate-500 sm:mt-0">
|
||||
Copyright © {new Date().getFullYear()} Resume Review. All
|
||||
rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</Container>
|
||||
</footer>
|
||||
);
|
||||
}
|
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 162 KiB |
Before Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 181 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 967 B |
@ -1,48 +1,45 @@
|
||||
import clsx from 'clsx';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type ResumeExpandableTextProps = Readonly<{
|
||||
children: ReactNode;
|
||||
text: string;
|
||||
}>;
|
||||
|
||||
export default function ResumeExpandableText({
|
||||
children,
|
||||
text,
|
||||
}: ResumeExpandableTextProps) {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [descriptionOverflow, setDescriptionOverflow] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (ref.current && ref.current.clientHeight < ref.current.scrollHeight) {
|
||||
useEffect(() => {
|
||||
const lines = text.split(/\r\n|\r|\n/);
|
||||
if (lines.length > 3) {
|
||||
setDescriptionOverflow(true);
|
||||
} else {
|
||||
setDescriptionOverflow(false);
|
||||
}
|
||||
}, [ref]);
|
||||
}, [text]);
|
||||
|
||||
const onSeeActionClicked = () => {
|
||||
setDescriptionExpanded(!descriptionExpanded);
|
||||
setIsExpanded((prevExpanded) => !prevExpanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<span
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
'whitespace-pre-wrap text-sm',
|
||||
'line-clamp-3',
|
||||
descriptionExpanded ? 'line-clamp-none' : '',
|
||||
'line-clamp-3 whitespace-pre-wrap text-sm',
|
||||
isExpanded ? 'line-clamp-none' : '',
|
||||
)}>
|
||||
{children}
|
||||
{text}
|
||||
</span>
|
||||
{descriptionOverflow && (
|
||||
<div className="flex flex-row">
|
||||
<div
|
||||
className="text-xs text-indigo-500 hover:text-indigo-300"
|
||||
onClick={onSeeActionClicked}>
|
||||
{descriptionExpanded ? 'See Less' : 'See More'}
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
className="mt-1 cursor-pointer text-xs text-indigo-500 hover:text-indigo-300"
|
||||
onClick={onSeeActionClicked}>
|
||||
{isExpanded ? 'See Less' : 'See More'}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
export default function SubmissionGuidelines() {
|
||||
return (
|
||||
<div className="mb-4 text-left text-sm text-slate-700">
|
||||
<h2 className="mb-2 text-xl font-medium">Submission Guidelines</h2>
|
||||
<p>
|
||||
Before you submit, please review and acknolwedge our
|
||||
<span className="font-bold"> submission guidelines </span>
|
||||
stated below.
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-lg font-bold">• </span>
|
||||
Ensure that you do not divulge any of your
|
||||
<span className="font-bold"> personal particulars</span>.
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-lg font-bold">• </span>
|
||||
Ensure that you do not divulge any
|
||||
<span className="font-bold">
|
||||
{' '}
|
||||
company's proprietary and confidential information
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-lg font-bold">• </span>
|
||||
Proof-read your resumes to look for grammatical/spelling errors.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,574 @@
|
||||
import type {
|
||||
Company,
|
||||
OffersAnalysis,
|
||||
OffersBackground,
|
||||
OffersCurrency,
|
||||
OffersEducation,
|
||||
OffersExperience,
|
||||
OffersFullTime,
|
||||
OffersIntern,
|
||||
OffersOffer,
|
||||
OffersProfile,
|
||||
OffersReply,
|
||||
OffersSpecificYoe,
|
||||
User,
|
||||
} from '@prisma/client';
|
||||
import { JobType } from '@prisma/client';
|
||||
|
||||
import type {
|
||||
AddToProfileResponse,
|
||||
Analysis,
|
||||
AnalysisHighestOffer,
|
||||
AnalysisOffer,
|
||||
Background,
|
||||
CreateOfferProfileResponse,
|
||||
DashboardOffer,
|
||||
Education,
|
||||
Experience,
|
||||
GetOffersResponse,
|
||||
OffersCompany,
|
||||
Paging,
|
||||
Profile,
|
||||
ProfileAnalysis,
|
||||
ProfileOffer,
|
||||
SpecificYoe,
|
||||
Valuation,
|
||||
} from '~/types/offers';
|
||||
|
||||
const analysisOfferDtoMapper = (
|
||||
offer: OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||
profile: OffersProfile & { background: OffersBackground | null };
|
||||
},
|
||||
) => {
|
||||
const { background, profileName } = offer.profile;
|
||||
const analysisOfferDto: AnalysisOffer = {
|
||||
company: offersCompanyDtoMapper(offer.company),
|
||||
id: offer.id,
|
||||
income: -1,
|
||||
jobType: offer.jobType,
|
||||
level: offer.offersFullTime?.level ?? '',
|
||||
location: offer.location,
|
||||
monthYearReceived: offer.monthYearReceived,
|
||||
negotiationStrategy: offer.negotiationStrategy,
|
||||
previousCompanies: [],
|
||||
profileName,
|
||||
specialization:
|
||||
offer.jobType === JobType.FULLTIME
|
||||
? offer.offersFullTime?.specialization ?? ''
|
||||
: offer.offersIntern?.specialization ?? '',
|
||||
title:
|
||||
offer.jobType === JobType.FULLTIME
|
||||
? offer.offersFullTime?.title ?? ''
|
||||
: offer.offersIntern?.title ?? '',
|
||||
totalYoe: background?.totalYoe ?? -1,
|
||||
};
|
||||
|
||||
if (offer.offersFullTime?.totalCompensation) {
|
||||
analysisOfferDto.income = offer.offersFullTime.totalCompensation.value;
|
||||
} else if (offer.offersIntern?.monthlySalary) {
|
||||
analysisOfferDto.income = offer.offersIntern.monthlySalary.value;
|
||||
}
|
||||
|
||||
return analysisOfferDto;
|
||||
};
|
||||
|
||||
const analysisDtoMapper = (
|
||||
noOfOffers: number,
|
||||
percentile: number,
|
||||
topPercentileOffers: Array<
|
||||
OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||
profile: OffersProfile & { background: OffersBackground | null };
|
||||
}
|
||||
>,
|
||||
) => {
|
||||
const analysisDto: Analysis = {
|
||||
noOfOffers,
|
||||
percentile,
|
||||
topPercentileOffers: topPercentileOffers.map((offer) =>
|
||||
analysisOfferDtoMapper(offer),
|
||||
),
|
||||
};
|
||||
return analysisDto;
|
||||
};
|
||||
|
||||
const analysisHighestOfferDtoMapper = (
|
||||
offer: OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||
profile: OffersProfile & { background: OffersBackground | null };
|
||||
},
|
||||
) => {
|
||||
const analysisHighestOfferDto: AnalysisHighestOffer = {
|
||||
company: offersCompanyDtoMapper(offer.company),
|
||||
id: offer.id,
|
||||
level: offer.offersFullTime?.level ?? '',
|
||||
location: offer.location,
|
||||
specialization:
|
||||
offer.jobType === JobType.FULLTIME
|
||||
? offer.offersFullTime?.specialization ?? ''
|
||||
: offer.offersIntern?.specialization ?? '',
|
||||
totalYoe: offer.profile.background?.totalYoe ?? -1,
|
||||
};
|
||||
return analysisHighestOfferDto;
|
||||
};
|
||||
|
||||
export const profileAnalysisDtoMapper = (
|
||||
analysis:
|
||||
| (OffersAnalysis & {
|
||||
overallHighestOffer: OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern:
|
||||
| (OffersIntern & { monthlySalary: OffersCurrency })
|
||||
| null;
|
||||
profile: OffersProfile & { background: OffersBackground | null };
|
||||
};
|
||||
topCompanyOffers: Array<
|
||||
OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern:
|
||||
| (OffersIntern & { monthlySalary: OffersCurrency })
|
||||
| null;
|
||||
profile: OffersProfile & {
|
||||
background:
|
||||
| (OffersBackground & {
|
||||
experiences: Array<
|
||||
OffersExperience & { company: Company | null }
|
||||
>;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
}
|
||||
>;
|
||||
topOverallOffers: Array<
|
||||
OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern:
|
||||
| (OffersIntern & { monthlySalary: OffersCurrency })
|
||||
| null;
|
||||
profile: OffersProfile & {
|
||||
background:
|
||||
| (OffersBackground & {
|
||||
experiences: Array<
|
||||
OffersExperience & { company: Company | null }
|
||||
>;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
}
|
||||
>;
|
||||
})
|
||||
| null,
|
||||
) => {
|
||||
if (!analysis) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profileAnalysisDto: ProfileAnalysis = {
|
||||
companyAnalysis: [
|
||||
analysisDtoMapper(
|
||||
analysis.noOfSimilarCompanyOffers,
|
||||
analysis.companyPercentile,
|
||||
analysis.topCompanyOffers,
|
||||
),
|
||||
],
|
||||
id: analysis.id,
|
||||
overallAnalysis: analysisDtoMapper(
|
||||
analysis.noOfSimilarOffers,
|
||||
analysis.overallPercentile,
|
||||
analysis.topOverallOffers,
|
||||
),
|
||||
overallHighestOffer: analysisHighestOfferDtoMapper(
|
||||
analysis.overallHighestOffer,
|
||||
),
|
||||
profileId: analysis.profileId,
|
||||
};
|
||||
return profileAnalysisDto;
|
||||
};
|
||||
|
||||
export const valuationDtoMapper = (currency: {
|
||||
currency: string;
|
||||
id?: string;
|
||||
value: number;
|
||||
}) => {
|
||||
const valuationDto: Valuation = {
|
||||
currency: currency.currency,
|
||||
value: currency.value,
|
||||
};
|
||||
return valuationDto;
|
||||
};
|
||||
|
||||
export const offersCompanyDtoMapper = (company: Company) => {
|
||||
const companyDto: OffersCompany = {
|
||||
createdAt: company.createdAt,
|
||||
description: company?.description ?? '',
|
||||
id: company.id,
|
||||
logoUrl: company.logoUrl ?? '',
|
||||
name: company.name,
|
||||
slug: company.slug,
|
||||
updatedAt: company.updatedAt,
|
||||
};
|
||||
return companyDto;
|
||||
};
|
||||
|
||||
export const educationDtoMapper = (education: {
|
||||
backgroundId?: string;
|
||||
endDate: Date | null;
|
||||
field: string | null;
|
||||
id: string;
|
||||
school: string | null;
|
||||
startDate: Date | null;
|
||||
type: string | null;
|
||||
}) => {
|
||||
const educationDto: Education = {
|
||||
endDate: education.endDate,
|
||||
field: education.field,
|
||||
id: education.id,
|
||||
school: education.school,
|
||||
startDate: education.startDate,
|
||||
type: education.type,
|
||||
};
|
||||
return educationDto;
|
||||
};
|
||||
|
||||
export const experienceDtoMapper = (
|
||||
experience: OffersExperience & {
|
||||
company: Company | null;
|
||||
monthlySalary: OffersCurrency | null;
|
||||
totalCompensation: OffersCurrency | null;
|
||||
},
|
||||
) => {
|
||||
const experienceDto: Experience = {
|
||||
company: experience.company
|
||||
? offersCompanyDtoMapper(experience.company)
|
||||
: null,
|
||||
durationInMonths: experience.durationInMonths,
|
||||
id: experience.id,
|
||||
jobType: experience.jobType,
|
||||
level: experience.level,
|
||||
monthlySalary: experience.monthlySalary
|
||||
? valuationDtoMapper(experience.monthlySalary)
|
||||
: experience.monthlySalary,
|
||||
specialization: experience.specialization,
|
||||
title: experience.title,
|
||||
totalCompensation: experience.totalCompensation
|
||||
? valuationDtoMapper(experience.totalCompensation)
|
||||
: experience.totalCompensation,
|
||||
};
|
||||
return experienceDto;
|
||||
};
|
||||
|
||||
export const specificYoeDtoMapper = (specificYoe: {
|
||||
backgroundId?: string;
|
||||
domain: string;
|
||||
id: string;
|
||||
yoe: number;
|
||||
}) => {
|
||||
const specificYoeDto: SpecificYoe = {
|
||||
domain: specificYoe.domain,
|
||||
id: specificYoe.id,
|
||||
yoe: specificYoe.yoe,
|
||||
};
|
||||
return specificYoeDto;
|
||||
};
|
||||
|
||||
export const backgroundDtoMapper = (
|
||||
background:
|
||||
| (OffersBackground & {
|
||||
educations: Array<OffersEducation>;
|
||||
experiences: Array<
|
||||
OffersExperience & {
|
||||
company: Company | null;
|
||||
monthlySalary: OffersCurrency | null;
|
||||
totalCompensation: OffersCurrency | null;
|
||||
}
|
||||
>;
|
||||
specificYoes: Array<OffersSpecificYoe>;
|
||||
})
|
||||
| null,
|
||||
) => {
|
||||
if (!background) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const educations = background.educations.map((education) =>
|
||||
educationDtoMapper(education),
|
||||
);
|
||||
|
||||
const experiences = background.experiences.map((experience) =>
|
||||
experienceDtoMapper(experience),
|
||||
);
|
||||
|
||||
const specificYoes = background.specificYoes.map((specificYoe) =>
|
||||
specificYoeDtoMapper(specificYoe),
|
||||
);
|
||||
|
||||
const backgroundDto: Background = {
|
||||
educations,
|
||||
experiences,
|
||||
id: background.id,
|
||||
specificYoes,
|
||||
totalYoe: background.totalYoe,
|
||||
};
|
||||
|
||||
return backgroundDto;
|
||||
};
|
||||
|
||||
export const profileOfferDtoMapper = (
|
||||
offer: OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & {
|
||||
baseSalary: OffersCurrency;
|
||||
bonus: OffersCurrency;
|
||||
stocks: OffersCurrency;
|
||||
totalCompensation: OffersCurrency;
|
||||
})
|
||||
| null;
|
||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||
},
|
||||
) => {
|
||||
const profileOfferDto: ProfileOffer = {
|
||||
comments: offer.comments,
|
||||
company: offersCompanyDtoMapper(offer.company),
|
||||
id: offer.id,
|
||||
jobType: offer.jobType,
|
||||
location: offer.location,
|
||||
monthYearReceived: offer.monthYearReceived,
|
||||
negotiationStrategy: offer.negotiationStrategy,
|
||||
offersFullTime: offer.offersFullTime,
|
||||
offersIntern: offer.offersIntern,
|
||||
};
|
||||
|
||||
if (offer.offersFullTime) {
|
||||
profileOfferDto.offersFullTime = {
|
||||
baseSalary: valuationDtoMapper(offer.offersFullTime.baseSalary),
|
||||
bonus: valuationDtoMapper(offer.offersFullTime.bonus),
|
||||
id: offer.offersFullTime.id,
|
||||
level: offer.offersFullTime.level,
|
||||
specialization: offer.offersFullTime.specialization,
|
||||
stocks: valuationDtoMapper(offer.offersFullTime.stocks),
|
||||
title: offer.offersFullTime.title,
|
||||
totalCompensation: valuationDtoMapper(
|
||||
offer.offersFullTime.totalCompensation,
|
||||
),
|
||||
};
|
||||
} else if (offer.offersIntern) {
|
||||
profileOfferDto.offersIntern = {
|
||||
id: offer.offersIntern.id,
|
||||
internshipCycle: offer.offersIntern.internshipCycle,
|
||||
monthlySalary: valuationDtoMapper(offer.offersIntern.monthlySalary),
|
||||
specialization: offer.offersIntern.specialization,
|
||||
startYear: offer.offersIntern.startYear,
|
||||
title: offer.offersIntern.title,
|
||||
};
|
||||
}
|
||||
|
||||
return profileOfferDto;
|
||||
};
|
||||
|
||||
export const profileDtoMapper = (
|
||||
profile: OffersProfile & {
|
||||
analysis:
|
||||
| (OffersAnalysis & {
|
||||
overallHighestOffer: OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern:
|
||||
| (OffersIntern & { monthlySalary: OffersCurrency })
|
||||
| null;
|
||||
profile: OffersProfile & { background: OffersBackground | null };
|
||||
};
|
||||
topCompanyOffers: Array<
|
||||
OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern:
|
||||
| (OffersIntern & { monthlySalary: OffersCurrency })
|
||||
| null;
|
||||
profile: OffersProfile & {
|
||||
background:
|
||||
| (OffersBackground & {
|
||||
experiences: Array<
|
||||
OffersExperience & { company: Company | null }
|
||||
>;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
}
|
||||
>;
|
||||
topOverallOffers: Array<
|
||||
OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern:
|
||||
| (OffersIntern & { monthlySalary: OffersCurrency })
|
||||
| null;
|
||||
profile: OffersProfile & {
|
||||
background:
|
||||
| (OffersBackground & {
|
||||
experiences: Array<
|
||||
OffersExperience & { company: Company | null }
|
||||
>;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
}
|
||||
>;
|
||||
})
|
||||
| null;
|
||||
background:
|
||||
| (OffersBackground & {
|
||||
educations: Array<OffersEducation>;
|
||||
experiences: Array<
|
||||
OffersExperience & {
|
||||
company: Company | null;
|
||||
monthlySalary: OffersCurrency | null;
|
||||
totalCompensation: OffersCurrency | null;
|
||||
}
|
||||
>;
|
||||
specificYoes: Array<OffersSpecificYoe>;
|
||||
})
|
||||
| null;
|
||||
discussion: Array<
|
||||
OffersReply & {
|
||||
replies: Array<OffersReply>;
|
||||
replyingTo: OffersReply | null;
|
||||
user: User | null;
|
||||
}
|
||||
>;
|
||||
offers: Array<
|
||||
OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & {
|
||||
baseSalary: OffersCurrency;
|
||||
bonus: OffersCurrency;
|
||||
stocks: OffersCurrency;
|
||||
totalCompensation: OffersCurrency;
|
||||
})
|
||||
| null;
|
||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||
}
|
||||
>;
|
||||
},
|
||||
inputToken: string | undefined,
|
||||
) => {
|
||||
const profileDto: Profile = {
|
||||
analysis: profileAnalysisDtoMapper(profile.analysis),
|
||||
background: backgroundDtoMapper(profile.background),
|
||||
editToken: null,
|
||||
id: profile.id,
|
||||
isEditable: false,
|
||||
offers: profile.offers.map((offer) => profileOfferDtoMapper(offer)),
|
||||
profileName: profile.profileName,
|
||||
};
|
||||
|
||||
if (inputToken === profile.editToken) {
|
||||
profileDto.editToken = profile.editToken;
|
||||
profileDto.isEditable = true;
|
||||
}
|
||||
|
||||
return profileDto;
|
||||
};
|
||||
|
||||
export const createOfferProfileResponseMapper = (
|
||||
profile: { id: string },
|
||||
token: string,
|
||||
) => {
|
||||
const res: CreateOfferProfileResponse = {
|
||||
id: profile.id,
|
||||
token,
|
||||
};
|
||||
return res;
|
||||
};
|
||||
|
||||
export const addToProfileResponseMapper = (updatedProfile: {
|
||||
id: string;
|
||||
profileName: string;
|
||||
userId?: string | null;
|
||||
}) => {
|
||||
const addToProfileResponse: AddToProfileResponse = {
|
||||
id: updatedProfile.id,
|
||||
profileName: updatedProfile.profileName,
|
||||
userId: updatedProfile.userId ?? '',
|
||||
};
|
||||
|
||||
return addToProfileResponse;
|
||||
};
|
||||
|
||||
export const dashboardOfferDtoMapper = (
|
||||
offer: OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & {
|
||||
baseSalary: OffersCurrency;
|
||||
bonus: OffersCurrency;
|
||||
stocks: OffersCurrency;
|
||||
totalCompensation: OffersCurrency;
|
||||
})
|
||||
| null;
|
||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||
profile: OffersProfile & { background: OffersBackground | null };
|
||||
},
|
||||
) => {
|
||||
const dashboardOfferDto: DashboardOffer = {
|
||||
company: offersCompanyDtoMapper(offer.company),
|
||||
id: offer.id,
|
||||
income: valuationDtoMapper({ currency: '', value: -1 }),
|
||||
monthYearReceived: offer.monthYearReceived,
|
||||
profileId: offer.profileId,
|
||||
title: offer.offersFullTime?.title ?? '',
|
||||
totalYoe: offer.profile.background?.totalYoe ?? -1,
|
||||
};
|
||||
|
||||
if (offer.offersFullTime) {
|
||||
dashboardOfferDto.income = valuationDtoMapper(
|
||||
offer.offersFullTime.totalCompensation,
|
||||
);
|
||||
} else if (offer.offersIntern) {
|
||||
dashboardOfferDto.income = valuationDtoMapper(
|
||||
offer.offersIntern.monthlySalary,
|
||||
);
|
||||
}
|
||||
|
||||
return dashboardOfferDto;
|
||||
};
|
||||
|
||||
export const getOffersResponseMapper = (
|
||||
data: Array<DashboardOffer>,
|
||||
paging: Paging,
|
||||
) => {
|
||||
const getOffersResponse: GetOffersResponse = {
|
||||
data,
|
||||
paging,
|
||||
};
|
||||
return getOffersResponse;
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
function GenerateAnalysis() {
|
||||
const analysisMutation = trpc.useMutation(['offers.analysis.generate']);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{JSON.stringify(
|
||||
analysisMutation.mutate({ profileId: 'cl98ywtbv0000tx1s4p18eol1' }),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GenerateAnalysis;
|
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
function GetAnalysis() {
|
||||
const analysis = trpc.useQuery([
|
||||
'offers.analysis.get',
|
||||
{ profileId: 'cl98ywtbv0000tx1s4p18eol1' },
|
||||
]);
|
||||
|
||||
return <div>{JSON.stringify(analysis.data)}</div>;
|
||||
}
|
||||
|
||||
export default GetAnalysis;
|
@ -0,0 +1,470 @@
|
||||
import { z } from 'zod';
|
||||
import type {
|
||||
Company,
|
||||
OffersBackground,
|
||||
OffersCurrency,
|
||||
OffersFullTime,
|
||||
OffersIntern,
|
||||
OffersOffer,
|
||||
OffersProfile,
|
||||
} from '@prisma/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { profileAnalysisDtoMapper } from '~/mappers/offers-mappers';
|
||||
|
||||
import { createRouter } from '../context';
|
||||
|
||||
const searchOfferPercentile = (
|
||||
offer: OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & {
|
||||
baseSalary: OffersCurrency;
|
||||
bonus: OffersCurrency;
|
||||
stocks: OffersCurrency;
|
||||
totalCompensation: OffersCurrency;
|
||||
})
|
||||
| null;
|
||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||
profile: OffersProfile & { background: OffersBackground | null };
|
||||
},
|
||||
similarOffers: Array<
|
||||
OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & {
|
||||
totalCompensation: OffersCurrency;
|
||||
})
|
||||
| null;
|
||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||
profile: OffersProfile & { background: OffersBackground | null };
|
||||
}
|
||||
>,
|
||||
) => {
|
||||
for (let i = 0; i < similarOffers.length; i++) {
|
||||
if (similarOffers[i].id === offer.id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
|
||||
export const offersAnalysisRouter = createRouter()
|
||||
.query('get', {
|
||||
input: z.object({
|
||||
profileId: z.string(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const analysis = await ctx.prisma.offersAnalysis.findFirst({
|
||||
include: {
|
||||
overallHighestOffer: {
|
||||
include: {
|
||||
company: true,
|
||||
offersFullTime: {
|
||||
include: {
|
||||
totalCompensation: true,
|
||||
},
|
||||
},
|
||||
offersIntern: {
|
||||
include: {
|
||||
monthlySalary: true,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
include: {
|
||||
background: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
topCompanyOffers: {
|
||||
include: {
|
||||
company: true,
|
||||
offersFullTime: {
|
||||
include: {
|
||||
totalCompensation: true,
|
||||
},
|
||||
},
|
||||
offersIntern: {
|
||||
include: {
|
||||
monthlySalary: true,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
include: {
|
||||
background: {
|
||||
include: {
|
||||
experiences: {
|
||||
include: {
|
||||
company: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
topOverallOffers: {
|
||||
include: {
|
||||
company: true,
|
||||
offersFullTime: {
|
||||
include: {
|
||||
totalCompensation: true,
|
||||
},
|
||||
},
|
||||
offersIntern: {
|
||||
include: {
|
||||
monthlySalary: true,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
include: {
|
||||
background: {
|
||||
include: {
|
||||
experiences: {
|
||||
include: {
|
||||
company: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
profileId: input.profileId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!analysis) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'No analysis found on this profile',
|
||||
});
|
||||
}
|
||||
|
||||
return profileAnalysisDtoMapper(analysis);
|
||||
},
|
||||
})
|
||||
.mutation('generate', {
|
||||
input: z.object({
|
||||
profileId: z.string(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
await ctx.prisma.offersAnalysis.deleteMany({
|
||||
where: {
|
||||
profileId: input.profileId,
|
||||
},
|
||||
});
|
||||
|
||||
const offers = await ctx.prisma.offersOffer.findMany({
|
||||
include: {
|
||||
company: true,
|
||||
offersFullTime: {
|
||||
include: {
|
||||
baseSalary: true,
|
||||
bonus: true,
|
||||
stocks: true,
|
||||
totalCompensation: true,
|
||||
},
|
||||
},
|
||||
offersIntern: {
|
||||
include: {
|
||||
monthlySalary: true,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
include: {
|
||||
background: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
offersFullTime: {
|
||||
totalCompensation: {
|
||||
value: 'desc',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
offersIntern: {
|
||||
monthlySalary: {
|
||||
value: 'desc',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
profileId: input.profileId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!offers || offers.length === 0) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'No offers found on this profile',
|
||||
});
|
||||
}
|
||||
|
||||
const overallHighestOffer = offers[0];
|
||||
|
||||
// TODO: Shift yoe out of background to make it mandatory
|
||||
if (
|
||||
!overallHighestOffer.profile.background ||
|
||||
!overallHighestOffer.profile.background.totalYoe
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Cannot analyse without YOE',
|
||||
});
|
||||
}
|
||||
|
||||
const yoe = overallHighestOffer.profile.background.totalYoe as number;
|
||||
|
||||
let similarOffers = await ctx.prisma.offersOffer.findMany({
|
||||
include: {
|
||||
company: true,
|
||||
offersFullTime: {
|
||||
include: {
|
||||
totalCompensation: true,
|
||||
},
|
||||
},
|
||||
offersIntern: {
|
||||
include: {
|
||||
monthlySalary: true,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
include: {
|
||||
background: {
|
||||
include: {
|
||||
experiences: {
|
||||
include: {
|
||||
company: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
offersFullTime: {
|
||||
totalCompensation: {
|
||||
value: 'desc',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
offersIntern: {
|
||||
monthlySalary: {
|
||||
value: 'desc',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
location: overallHighestOffer.location,
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
offersFullTime: {
|
||||
level: overallHighestOffer.offersFullTime?.level,
|
||||
specialization:
|
||||
overallHighestOffer.offersFullTime?.specialization,
|
||||
},
|
||||
offersIntern: {
|
||||
specialization:
|
||||
overallHighestOffer.offersIntern?.specialization,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
profile: {
|
||||
background: {
|
||||
AND: [
|
||||
{
|
||||
totalYoe: {
|
||||
gte: Math.max(yoe - 1, 0),
|
||||
lte: yoe + 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
let similarCompanyOffers = similarOffers.filter(
|
||||
(offer) => offer.companyId === overallHighestOffer.companyId,
|
||||
);
|
||||
|
||||
// CALCULATE PERCENTILES
|
||||
const overallIndex = searchOfferPercentile(
|
||||
overallHighestOffer,
|
||||
similarOffers,
|
||||
);
|
||||
const overallPercentile =
|
||||
similarOffers.length === 0 ? 0 : overallIndex / similarOffers.length;
|
||||
|
||||
const companyIndex = searchOfferPercentile(
|
||||
overallHighestOffer,
|
||||
similarCompanyOffers,
|
||||
);
|
||||
const companyPercentile =
|
||||
similarCompanyOffers.length === 0
|
||||
? 0
|
||||
: companyIndex / similarCompanyOffers.length;
|
||||
|
||||
// FIND TOP >=90 PERCENTILE OFFERS
|
||||
similarOffers = similarOffers.filter(
|
||||
(offer) => offer.id !== overallHighestOffer.id,
|
||||
);
|
||||
similarCompanyOffers = similarCompanyOffers.filter(
|
||||
(offer) => offer.id !== overallHighestOffer.id,
|
||||
);
|
||||
|
||||
const noOfSimilarOffers = similarOffers.length;
|
||||
const similarOffers90PercentileIndex =
|
||||
Math.floor(noOfSimilarOffers * 0.9) - 1;
|
||||
const topPercentileOffers =
|
||||
noOfSimilarOffers > 1
|
||||
? similarOffers.slice(
|
||||
similarOffers90PercentileIndex,
|
||||
similarOffers90PercentileIndex + 2,
|
||||
)
|
||||
: similarOffers;
|
||||
|
||||
const noOfSimilarCompanyOffers = similarCompanyOffers.length;
|
||||
const similarCompanyOffers90PercentileIndex =
|
||||
Math.floor(noOfSimilarCompanyOffers * 0.9) - 1;
|
||||
const topPercentileCompanyOffers =
|
||||
noOfSimilarCompanyOffers > 1
|
||||
? similarCompanyOffers.slice(
|
||||
similarCompanyOffers90PercentileIndex,
|
||||
similarCompanyOffers90PercentileIndex + 2,
|
||||
)
|
||||
: similarCompanyOffers;
|
||||
|
||||
const analysis = await ctx.prisma.offersAnalysis.create({
|
||||
data: {
|
||||
companyPercentile,
|
||||
noOfSimilarCompanyOffers,
|
||||
noOfSimilarOffers,
|
||||
overallHighestOffer: {
|
||||
connect: {
|
||||
id: overallHighestOffer.id,
|
||||
},
|
||||
},
|
||||
overallPercentile,
|
||||
profile: {
|
||||
connect: {
|
||||
id: input.profileId,
|
||||
},
|
||||
},
|
||||
topCompanyOffers: {
|
||||
connect: topPercentileCompanyOffers.map((offer) => {
|
||||
return { id: offer.id };
|
||||
}),
|
||||
},
|
||||
topOverallOffers: {
|
||||
connect: topPercentileOffers.map((offer) => {
|
||||
return { id: offer.id };
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
overallHighestOffer: {
|
||||
include: {
|
||||
company: true,
|
||||
offersFullTime: {
|
||||
include: {
|
||||
totalCompensation: true,
|
||||
},
|
||||
},
|
||||
offersIntern: {
|
||||
include: {
|
||||
monthlySalary: true,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
include: {
|
||||
background: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
topCompanyOffers: {
|
||||
include: {
|
||||
company: true,
|
||||
offersFullTime: {
|
||||
include: {
|
||||
totalCompensation: true,
|
||||
},
|
||||
},
|
||||
offersIntern: {
|
||||
include: {
|
||||
monthlySalary: true,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
include: {
|
||||
background: {
|
||||
include: {
|
||||
experiences: {
|
||||
include: {
|
||||
company: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
topOverallOffers: {
|
||||
include: {
|
||||
company: true,
|
||||
offersFullTime: {
|
||||
include: {
|
||||
totalCompensation: true,
|
||||
},
|
||||
},
|
||||
offersIntern: {
|
||||
include: {
|
||||
monthlySalary: true,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
include: {
|
||||
background: {
|
||||
include: {
|
||||
experiences: {
|
||||
include: {
|
||||
company: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return profileAnalysisDtoMapper(analysis);
|
||||
},
|
||||
});
|
@ -1,225 +1,335 @@
|
||||
import { z } from 'zod';
|
||||
import * as trpc from '@trpc/server';
|
||||
|
||||
import { createProtectedRouter } from '../context';
|
||||
|
||||
export const offersCommentsRouter = createProtectedRouter()
|
||||
.query('getComments', {
|
||||
input: z.object({
|
||||
profileId: z.string()
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const result = await ctx.prisma.offersProfile.findFirst({
|
||||
import { createRouter } from '../context';
|
||||
|
||||
import type { OffersDiscussion, Reply } from '~/types/offers';
|
||||
|
||||
export const offersCommentsRouter = createRouter()
|
||||
.query('getComments', {
|
||||
input: z.object({
|
||||
profileId: z.string(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const profile = await ctx.prisma.offersProfile.findFirst({
|
||||
where: {
|
||||
id: input.profileId,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await ctx.prisma.offersProfile.findFirst({
|
||||
include: {
|
||||
discussion: {
|
||||
include: {
|
||||
replies: {
|
||||
include: {
|
||||
discussion: {
|
||||
include: {
|
||||
replies: true,
|
||||
replyingTo: true,
|
||||
user: true
|
||||
}
|
||||
}
|
||||
user: true,
|
||||
},
|
||||
where: {
|
||||
id: input.profileId
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
})
|
||||
|
||||
if (result) {
|
||||
return result.discussion.filter((x) => x.replyingToId === null)
|
||||
},
|
||||
replyingTo: true,
|
||||
user: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: input.profileId,
|
||||
}
|
||||
})
|
||||
.mutation("create", {
|
||||
input: z.object({
|
||||
message: z.string(),
|
||||
profileId: z.string(),
|
||||
replyingToId: z.string().optional(),
|
||||
userId: z.string().optional()
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const createdReply = await ctx.prisma.offersReply.create({
|
||||
data: {
|
||||
message: input.message,
|
||||
profile: {
|
||||
connect: {
|
||||
id: input.profileId
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (input.replyingToId) {
|
||||
await ctx.prisma.offersReply.update({
|
||||
data: {
|
||||
replyingTo: {
|
||||
connect: {
|
||||
id: input.replyingToId
|
||||
}
|
||||
}
|
||||
},
|
||||
where: {
|
||||
id: createdReply.id
|
||||
}
|
||||
})
|
||||
}
|
||||
const discussions: OffersDiscussion = {
|
||||
data: result?.discussion
|
||||
.filter((x) => {
|
||||
return x.replyingToId === null
|
||||
})
|
||||
.map((x) => {
|
||||
if (x.user == null) {
|
||||
x.user = {
|
||||
email: '',
|
||||
emailVerified: null,
|
||||
id: '',
|
||||
image: '',
|
||||
name: profile?.profileName ?? '<missing name>',
|
||||
};
|
||||
}
|
||||
|
||||
if (input.userId) {
|
||||
await ctx.prisma.offersReply.update({
|
||||
data: {
|
||||
user: {
|
||||
connect: {
|
||||
id: input.userId
|
||||
}
|
||||
}
|
||||
},
|
||||
where: {
|
||||
id: createdReply.id
|
||||
}
|
||||
})
|
||||
}
|
||||
// Get replies
|
||||
const result = await ctx.prisma.offersProfile.findFirst({
|
||||
include: {
|
||||
discussion: {
|
||||
include: {
|
||||
replies: true,
|
||||
replyingTo: true,
|
||||
user: true
|
||||
}
|
||||
}
|
||||
},
|
||||
where: {
|
||||
id: input.profileId
|
||||
x.replies?.map((y) => {
|
||||
if (y.user == null) {
|
||||
y.user = {
|
||||
email: '',
|
||||
emailVerified: null,
|
||||
id: '',
|
||||
image: '',
|
||||
name: profile?.profileName ?? '<missing name>',
|
||||
};
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (result) {
|
||||
return result.discussion.filter((x) => x.replyingToId === null)
|
||||
}
|
||||
const replyType: Reply = {
|
||||
createdAt: x.createdAt,
|
||||
id: x.id,
|
||||
message: x.message,
|
||||
replies: x.replies.map((reply) => {
|
||||
return {
|
||||
createdAt: reply.createdAt,
|
||||
id: reply.id,
|
||||
message: reply.message,
|
||||
replies: [],
|
||||
replyingToId: reply.replyingToId,
|
||||
user: reply.user
|
||||
}
|
||||
}),
|
||||
replyingToId: x.replyingToId,
|
||||
user: x.user
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
.mutation("update", {
|
||||
input: z.object({
|
||||
id: z.string(),
|
||||
message: z.string(),
|
||||
profileId: z.string(),
|
||||
// Have to pass in either userID or token for validation
|
||||
token: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const messageToUpdate = await ctx.prisma.offersReply.findFirst({
|
||||
where: {
|
||||
id: input.id
|
||||
}
|
||||
})
|
||||
const profile = await ctx.prisma.offersProfile.findFirst({
|
||||
where: {
|
||||
return replyType
|
||||
}) ?? []
|
||||
}
|
||||
|
||||
return discussions
|
||||
},
|
||||
})
|
||||
.mutation('create', {
|
||||
input: z.object({
|
||||
message: z.string(),
|
||||
profileId: z.string(),
|
||||
replyingToId: z.string().optional(),
|
||||
token: z.string().optional(),
|
||||
userId: z.string().optional()
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const profile = await ctx.prisma.offersProfile.findFirst({
|
||||
where: {
|
||||
id: input.profileId,
|
||||
},
|
||||
});
|
||||
|
||||
const profileEditToken = profile?.editToken;
|
||||
|
||||
if (input.token === profileEditToken || input.userId) {
|
||||
const createdReply = await ctx.prisma.offersReply.create({
|
||||
data: {
|
||||
message: input.message,
|
||||
profile: {
|
||||
connect: {
|
||||
id: input.profileId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (input.replyingToId) {
|
||||
await ctx.prisma.offersReply.update({
|
||||
data: {
|
||||
replyingTo: {
|
||||
connect: {
|
||||
id: input.replyingToId,
|
||||
},
|
||||
});
|
||||
|
||||
const profileEditToken = profile?.editToken;
|
||||
|
||||
// To validate user editing, OP or correct user
|
||||
// TODO: improve validation process
|
||||
if (profileEditToken === input.token || messageToUpdate?.userId === input.userId) {
|
||||
await ctx.prisma.offersReply.update({
|
||||
data: {
|
||||
message: input.message
|
||||
},
|
||||
where: {
|
||||
id: input.id
|
||||
}
|
||||
})
|
||||
|
||||
const result = await ctx.prisma.offersProfile.findFirst({
|
||||
include: {
|
||||
discussion: {
|
||||
include: {
|
||||
replies: true,
|
||||
replyingTo: true,
|
||||
user: true
|
||||
}
|
||||
}
|
||||
},
|
||||
where: {
|
||||
id: input.profileId
|
||||
}
|
||||
})
|
||||
|
||||
if (result) {
|
||||
return result.discussion.filter((x) => x.replyingToId === null)
|
||||
}
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: createdReply.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
if (input.userId) {
|
||||
await ctx.prisma.offersReply.update({
|
||||
data: {
|
||||
user: {
|
||||
connect: {
|
||||
id: input.userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: createdReply.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new trpc.TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Wrong userId or token.'
|
||||
})
|
||||
const created = await ctx.prisma.offersReply.findFirst({
|
||||
include: {
|
||||
user: true
|
||||
},
|
||||
where: {
|
||||
id: createdReply.id,
|
||||
},
|
||||
});
|
||||
|
||||
const result: Reply = {
|
||||
createdAt: created!.createdAt,
|
||||
id: created!.id,
|
||||
message: created!.message,
|
||||
replies: [], // New message should have no replies
|
||||
replyingToId: created!.replyingToId,
|
||||
user: created!.user ?? {
|
||||
email: '',
|
||||
emailVerified: null,
|
||||
id: '',
|
||||
image: '',
|
||||
name: profile?.profileName ?? '<missing name>',
|
||||
}
|
||||
}
|
||||
})
|
||||
.mutation("delete", {
|
||||
input: z.object({
|
||||
id: z.string(),
|
||||
profileId: z.string(),
|
||||
// Have to pass in either userID or token for validation
|
||||
token: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const messageToDelete = await ctx.prisma.offersReply.findFirst({
|
||||
where: {
|
||||
id: input.id
|
||||
}
|
||||
})
|
||||
const profile = await ctx.prisma.offersProfile.findFirst({
|
||||
where: {
|
||||
id: input.profileId,
|
||||
},
|
||||
});
|
||||
|
||||
const profileEditToken = profile?.editToken;
|
||||
|
||||
// To validate user editing, OP or correct user
|
||||
// TODO: improve validation process
|
||||
if (profileEditToken === input.token || messageToDelete?.userId === input.userId) {
|
||||
await ctx.prisma.offersReply.delete({
|
||||
where: {
|
||||
id: input.id
|
||||
}
|
||||
})
|
||||
const result = await ctx.prisma.offersProfile.findFirst({
|
||||
include: {
|
||||
discussion: {
|
||||
include: {
|
||||
replies: true,
|
||||
replyingTo: true,
|
||||
user: true
|
||||
}
|
||||
}
|
||||
},
|
||||
where: {
|
||||
id: input.profileId
|
||||
}
|
||||
})
|
||||
|
||||
if (result) {
|
||||
return result.discussion.filter((x) => x.replyingToId === null)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
throw new trpc.TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Wrong userId or token.'
|
||||
})
|
||||
throw new trpc.TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Missing userId or wrong token.',
|
||||
});
|
||||
},
|
||||
})
|
||||
.mutation('update', {
|
||||
input: z.object({
|
||||
id: z.string(),
|
||||
message: z.string(),
|
||||
profileId: z.string(),
|
||||
// Have to pass in either userID or token for validation
|
||||
token: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const messageToUpdate = await ctx.prisma.offersReply.findFirst({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
const profile = await ctx.prisma.offersProfile.findFirst({
|
||||
where: {
|
||||
id: input.profileId,
|
||||
},
|
||||
});
|
||||
|
||||
const profileEditToken = profile?.editToken;
|
||||
|
||||
// To validate user editing, OP or correct user
|
||||
// TODO: improve validation process
|
||||
if (
|
||||
profileEditToken === input.token ||
|
||||
messageToUpdate?.userId === input.userId
|
||||
) {
|
||||
const updated = await ctx.prisma.offersReply.update({
|
||||
data: {
|
||||
message: input.message,
|
||||
},
|
||||
include: {
|
||||
replies: {
|
||||
include: {
|
||||
user: true
|
||||
}
|
||||
},
|
||||
user: true
|
||||
},
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
|
||||
const result: Reply = {
|
||||
createdAt: updated!.createdAt,
|
||||
id: updated!.id,
|
||||
message: updated!.message,
|
||||
replies: updated!.replies.map((x) => {
|
||||
return {
|
||||
createdAt: x.createdAt,
|
||||
id: x.id,
|
||||
message: x.message,
|
||||
replies: [],
|
||||
replyingToId: x.replyingToId,
|
||||
user: x.user ?? {
|
||||
email: '',
|
||||
emailVerified: null,
|
||||
id: '',
|
||||
image: '',
|
||||
name: profile?.profileName ?? '<missing name>',
|
||||
}
|
||||
}
|
||||
}),
|
||||
replyingToId: updated!.replyingToId,
|
||||
user: updated!.user ?? {
|
||||
email: '',
|
||||
emailVerified: null,
|
||||
id: '',
|
||||
image: '',
|
||||
name: profile?.profileName ?? '<missing name>',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
throw new trpc.TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Wrong userId or token.',
|
||||
});
|
||||
},
|
||||
})
|
||||
.mutation('delete', {
|
||||
input: z.object({
|
||||
id: z.string(),
|
||||
profileId: z.string(),
|
||||
// Have to pass in either userID or token for validation
|
||||
token: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const messageToDelete = await ctx.prisma.offersReply.findFirst({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
const profile = await ctx.prisma.offersProfile.findFirst({
|
||||
where: {
|
||||
id: input.profileId,
|
||||
},
|
||||
});
|
||||
|
||||
const profileEditToken = profile?.editToken;
|
||||
|
||||
// To validate user editing, OP or correct user
|
||||
// TODO: improve validation process
|
||||
if (
|
||||
profileEditToken === input.token ||
|
||||
messageToDelete?.userId === input.userId
|
||||
) {
|
||||
await ctx.prisma.offersReply.delete({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
await ctx.prisma.offersProfile.findFirst({
|
||||
include: {
|
||||
discussion: {
|
||||
include: {
|
||||
replies: true,
|
||||
replyingTo: true,
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: input.profileId,
|
||||
},
|
||||
});
|
||||
|
||||
// If (result) {
|
||||
// return result.discussion.filter((x) => x.replyingToId === null);
|
||||
// }
|
||||
|
||||
// return result;
|
||||
}
|
||||
|
||||
throw new trpc.TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Wrong userId or token.',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -0,0 +1,135 @@
|
||||
import { z } from 'zod';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { createProtectedRouter } from './context';
|
||||
|
||||
import type { AggregatedQuestionEncounter } from '~/types/questions';
|
||||
|
||||
export const questionsQuestionEncounterRouter = createProtectedRouter()
|
||||
.query('getAggregatedEncounters', {
|
||||
input: z.object({
|
||||
questionId: z.string(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const questionEncountersData = await ctx.prisma.questionsQuestionEncounter.findMany({
|
||||
include: {
|
||||
company : true,
|
||||
},
|
||||
where: {
|
||||
...input,
|
||||
},
|
||||
});
|
||||
|
||||
const companyCounts: Record<string, number> = {};
|
||||
const locationCounts: Record<string, number> = {};
|
||||
const roleCounts:Record<string, number> = {};
|
||||
|
||||
for (let i = 0; i < questionEncountersData.length; i++) {
|
||||
const encounter = questionEncountersData[i];
|
||||
|
||||
if (!(encounter.company!.name in companyCounts)) {
|
||||
companyCounts[encounter.company!.name] = 1;
|
||||
}
|
||||
companyCounts[encounter.company!.name] += 1;
|
||||
|
||||
if (!(encounter.location in locationCounts)) {
|
||||
locationCounts[encounter.location] = 1;
|
||||
}
|
||||
locationCounts[encounter.location] += 1;
|
||||
|
||||
if (!(encounter.role in roleCounts)) {
|
||||
roleCounts[encounter.role] = 1;
|
||||
}
|
||||
roleCounts[encounter.role] += 1;
|
||||
|
||||
}
|
||||
|
||||
const questionEncounter:AggregatedQuestionEncounter = {
|
||||
companyCounts,
|
||||
locationCounts,
|
||||
roleCounts,
|
||||
}
|
||||
return questionEncounter;
|
||||
}
|
||||
})
|
||||
.mutation('create', {
|
||||
input: z.object({
|
||||
companyId: z.string(),
|
||||
location: z.string(),
|
||||
questionId: z.string(),
|
||||
role: z.string(),
|
||||
seenAt: z.date()
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const userId = ctx.session?.user?.id;
|
||||
|
||||
return await ctx.prisma.questionsQuestionEncounter.create({
|
||||
data: {
|
||||
...input,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
.mutation('update', {
|
||||
//
|
||||
input: z.object({
|
||||
companyId: z.string().optional(),
|
||||
id: z.string(),
|
||||
location: z.string().optional(),
|
||||
role: z.string().optional(),
|
||||
seenAt: z.date().optional(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const userId = ctx.session?.user?.id;
|
||||
|
||||
const questionEncounterToUpdate = await ctx.prisma.questionsQuestionEncounter.findUnique({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (questionEncounterToUpdate?.id !== userId) {
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'User have no authorization to record.',
|
||||
});
|
||||
}
|
||||
|
||||
return await ctx.prisma.questionsQuestionEncounter.update({
|
||||
data: {
|
||||
...input,
|
||||
},
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
.mutation('delete', {
|
||||
input: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const userId = ctx.session?.user?.id;
|
||||
|
||||
const questionEncounterToDelete = await ctx.prisma.questionsQuestionEncounter.findUnique({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (questionEncounterToDelete?.id !== userId) {
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'User have no authorization to record.',
|
||||
});
|
||||
}
|
||||
|
||||
return await ctx.prisma.questionsQuestionEncounter.delete({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
@ -0,0 +1,38 @@
|
||||
import { z } from 'zod';
|
||||
import type { ResumesCommentVote } from '@prisma/client';
|
||||
import { Vote } from '@prisma/client';
|
||||
|
||||
import { createRouter } from '../context';
|
||||
|
||||
import type { ResumeCommentVote } from '~/types/resume-comments';
|
||||
|
||||
export const resumesCommentsVotesRouter = createRouter().query('list', {
|
||||
input: z.object({
|
||||
commentId: z.string(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const userId = ctx.session?.user?.id;
|
||||
const { commentId } = input;
|
||||
|
||||
const votes = await ctx.prisma.resumesCommentVote.findMany({
|
||||
where: {
|
||||
commentId,
|
||||
},
|
||||
});
|
||||
|
||||
let userVote: ResumesCommentVote | null = null;
|
||||
let numVotes = 0;
|
||||
|
||||
votes.forEach((vote) => {
|
||||
numVotes += vote.value === Vote.UPVOTE ? 1 : -1;
|
||||
userVote = vote.userId === userId ? vote : null;
|
||||
});
|
||||
|
||||
const resumeCommentVote: ResumeCommentVote = {
|
||||
numVotes,
|
||||
userVote,
|
||||
};
|
||||
|
||||
return resumeCommentVote;
|
||||
},
|
||||
});
|
@ -0,0 +1,45 @@
|
||||
import { z } from 'zod';
|
||||
import { Vote } from '@prisma/client';
|
||||
|
||||
import { createProtectedRouter } from '../context';
|
||||
|
||||
export const resumesCommentsVotesUserRouter = createProtectedRouter()
|
||||
.mutation('upsert', {
|
||||
input: z.object({
|
||||
commentId: z.string(),
|
||||
value: z.nativeEnum(Vote),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const userId = ctx.session.user.id;
|
||||
const { commentId, value } = input;
|
||||
|
||||
await ctx.prisma.resumesCommentVote.upsert({
|
||||
create: {
|
||||
commentId,
|
||||
userId,
|
||||
value,
|
||||
},
|
||||
update: {
|
||||
value,
|
||||
},
|
||||
where: {
|
||||
userId_commentId: { commentId, userId },
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
.mutation('delete', {
|
||||
input: z.object({
|
||||
commentId: z.string(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const userId = ctx.session.user.id;
|
||||
const { commentId } = input;
|
||||
|
||||
await ctx.prisma.resumesCommentVote.delete({
|
||||
where: {
|
||||
userId_commentId: { commentId, userId },
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
@ -1,113 +0,0 @@
|
||||
export type offersProfile = {
|
||||
background?: background | null;
|
||||
createdAt: Date;
|
||||
// Discussions: Array<discussion>;
|
||||
editToken: string;
|
||||
id: string;
|
||||
offers: Array<offer>;
|
||||
profileName: string;
|
||||
userId?: string | null;
|
||||
};
|
||||
|
||||
export type background = {
|
||||
educations: Array<education>;
|
||||
experiences: Array<experience>;
|
||||
id: string;
|
||||
offersProfileId: string;
|
||||
specificYoes: Array<specificYoe>;
|
||||
totalYoe?: number | null;
|
||||
}
|
||||
|
||||
export type experience = {
|
||||
backgroundId: string;
|
||||
company?: company | null;
|
||||
companyId?: string | null;
|
||||
durationInMonths?: number | null;
|
||||
id: string;
|
||||
jobType?: string | null;
|
||||
level?: string | null;
|
||||
monthlySalary?: valuation | null;
|
||||
monthlySalaryId?: string | null;
|
||||
specialization?: string | null;
|
||||
title?: string | null;
|
||||
totalCompensation?: valuation | null;
|
||||
totalCompensationId?: string | null;
|
||||
}
|
||||
|
||||
export type company = {
|
||||
createdAt: Date;
|
||||
description: string | null;
|
||||
id: string;
|
||||
logoUrl: string | null;
|
||||
name: string;
|
||||
slug: string;
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export type valuation = {
|
||||
currency: string;
|
||||
id: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export type education = {
|
||||
backgroundId: string;
|
||||
endDate?: Date | null;
|
||||
field?: string | null;
|
||||
id: string;
|
||||
school?: string | null;
|
||||
startDate?: Date | null;
|
||||
type?: string | null;
|
||||
}
|
||||
|
||||
export type specificYoe = {
|
||||
backgroundId: string;
|
||||
domain: string;
|
||||
id: string;
|
||||
yoe: number;
|
||||
}
|
||||
|
||||
export type offers = {
|
||||
OffersFullTime?: offersFullTime | null;
|
||||
OffersIntern?: offersIntern | null;
|
||||
comments?: string | null;
|
||||
company: company;
|
||||
companyId: string;
|
||||
id: string;
|
||||
jobType: string;
|
||||
location: string;
|
||||
monthYearReceived: string;
|
||||
negotiationStrategy?: string | null;
|
||||
offersFullTimeId?: string | null;
|
||||
offersInternId?: string | null;
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
export type offersFullTime = {
|
||||
baseSalary: valuation;
|
||||
baseSalaryId: string;
|
||||
bonus: valuation;
|
||||
bonusId: string;
|
||||
id: string;
|
||||
level: string;
|
||||
specialization: string;
|
||||
stocks: valuation;
|
||||
stocksId: string;
|
||||
title?: string | null;
|
||||
totalCompensation: valuation;
|
||||
totalCompensationId: string;
|
||||
}
|
||||
|
||||
export type offersIntern = {
|
||||
id: string;
|
||||
internshipCycle: string;
|
||||
monthlySalary: valuation;
|
||||
monthlySalaryId: string;
|
||||
specialization: string;
|
||||
startYear: number;
|
||||
}
|
||||
|
||||
// TODO: fill in next time
|
||||
export type discussion = {
|
||||
id: string;
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
import type { JobType } from '@prisma/client';
|
||||
|
||||
export type Profile = {
|
||||
analysis: ProfileAnalysis?;
|
||||
background: Background?;
|
||||
editToken: string?;
|
||||
id: string;
|
||||
isEditable: boolean;
|
||||
offers: Array<ProfileOffer>;
|
||||
profileName: string;
|
||||
};
|
||||
|
||||
export type Background = {
|
||||
educations: Array<Education>;
|
||||
experiences: Array<Experience>;
|
||||
id: string;
|
||||
specificYoes: Array<SpecificYoe>;
|
||||
totalYoe: number;
|
||||
};
|
||||
|
||||
export type Experience = {
|
||||
company: OffersCompany?;
|
||||
durationInMonths: number?;
|
||||
id: string;
|
||||
jobType: JobType?;
|
||||
level: string?;
|
||||
monthlySalary: Valuation?;
|
||||
specialization: string?;
|
||||
title: string?;
|
||||
totalCompensation: Valuation?;
|
||||
};
|
||||
|
||||
export type OffersCompany = {
|
||||
createdAt: Date;
|
||||
description: string;
|
||||
id: string;
|
||||
logoUrl: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export type Valuation = {
|
||||
currency: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
export type Education = {
|
||||
endDate: Date?;
|
||||
field: string?;
|
||||
id: string;
|
||||
school: string?;
|
||||
startDate: Date?;
|
||||
type: string?;
|
||||
};
|
||||
|
||||
export type SpecificYoe = {
|
||||
domain: string;
|
||||
id: string;
|
||||
yoe: number;
|
||||
};
|
||||
|
||||
export type DashboardOffer = {
|
||||
company: OffersCompany;
|
||||
id: string;
|
||||
income: Valuation;
|
||||
monthYearReceived: Date;
|
||||
profileId: string;
|
||||
title: string;
|
||||
totalYoe: number;
|
||||
};
|
||||
|
||||
export type ProfileOffer = {
|
||||
comments: string;
|
||||
company: OffersCompany;
|
||||
id: string;
|
||||
jobType: JobType;
|
||||
location: string;
|
||||
monthYearReceived: Date;
|
||||
negotiationStrategy: string;
|
||||
offersFullTime: FullTime?;
|
||||
offersIntern: Intern?;
|
||||
};
|
||||
|
||||
export type FullTime = {
|
||||
baseSalary: Valuation;
|
||||
bonus: Valuation;
|
||||
id: string;
|
||||
level: string;
|
||||
specialization: string;
|
||||
stocks: Valuation;
|
||||
title: string;
|
||||
totalCompensation: Valuation;
|
||||
};
|
||||
|
||||
export type Intern = {
|
||||
id: string;
|
||||
internshipCycle: string;
|
||||
monthlySalary: Valuation;
|
||||
specialization: string;
|
||||
startYear: number;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type Reply = {
|
||||
createdAt: Date;
|
||||
id: string;
|
||||
message: string;
|
||||
replies: Array<Reply>?;
|
||||
replyingToId: string?;
|
||||
user: User?;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
email: string?;
|
||||
emailVerified: Date?;
|
||||
id: string;
|
||||
image: string?;
|
||||
name: string?;
|
||||
};
|
||||
|
||||
export type GetOffersResponse = {
|
||||
data: Array<DashboardOffer>;
|
||||
paging: Paging;
|
||||
};
|
||||
|
||||
export type Paging = {
|
||||
currentPage: number;
|
||||
numOfItems: number;
|
||||
numOfPages: number;
|
||||
totalItems: number;
|
||||
};
|
||||
|
||||
export type CreateOfferProfileResponse = {
|
||||
id: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type OffersDiscussion = {
|
||||
data: Array<Reply>;
|
||||
};
|
||||
|
||||
export type ProfileAnalysis = {
|
||||
companyAnalysis: Array<Analysis>;
|
||||
id: string;
|
||||
overallAnalysis: Analysis;
|
||||
overallHighestOffer: AnalysisHighestOffer;
|
||||
profileId: string;
|
||||
};
|
||||
|
||||
export type Analysis = {
|
||||
noOfOffers: number;
|
||||
percentile: number;
|
||||
topPercentileOffers: Array<AnalysisOffer>;
|
||||
};
|
||||
|
||||
export type AnalysisHighestOffer = {
|
||||
company: OffersCompany;
|
||||
id: string;
|
||||
level: string;
|
||||
location: string;
|
||||
specialization: string;
|
||||
totalYoe: number;
|
||||
};
|
||||
|
||||
export type AnalysisOffer = {
|
||||
company: OffersCompany;
|
||||
id: string;
|
||||
income: number;
|
||||
jobType: JobType;
|
||||
level: string;
|
||||
location: string;
|
||||
monthYearReceived: Date;
|
||||
negotiationStrategy: string;
|
||||
previousCompanies: Array<string>;
|
||||
profileName: string;
|
||||
specialization: string;
|
||||
title: string;
|
||||
totalYoe: number;
|
||||
};
|
||||
|
||||
export type AddToProfileResponse = {
|
||||
id: string;
|
||||
profileName: string;
|
||||
userId: string;
|
||||
};
|