Merge branch 'main' into hongpo/update-to-reference-company-table

pull/351/head
hpkoh 3 years ago committed by GitHub
commit fe3016358e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,204 @@
-- CreateEnum
CREATE TYPE "JobType" AS ENUM ('INTERN', 'FULLTIME');
-- CreateTable
CREATE TABLE "OffersProfile" (
"id" TEXT NOT NULL,
"profileName" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"editToken" TEXT NOT NULL,
"userId" TEXT,
CONSTRAINT "OffersProfile_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersBackground" (
"id" TEXT NOT NULL,
"totalYoe" INTEGER,
"offersProfileId" TEXT NOT NULL,
CONSTRAINT "OffersBackground_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersSpecificYoe" (
"id" TEXT NOT NULL,
"yoe" INTEGER NOT NULL,
"domain" TEXT NOT NULL,
"backgroundId" TEXT NOT NULL,
CONSTRAINT "OffersSpecificYoe_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersExperience" (
"id" TEXT NOT NULL,
"companyId" TEXT,
"jobType" "JobType",
"title" TEXT,
"durationInMonths" INTEGER,
"specialization" TEXT,
"level" TEXT,
"totalCompensationId" TEXT,
"monthlySalaryId" TEXT,
"backgroundId" TEXT NOT NULL,
CONSTRAINT "OffersExperience_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersCurrency" (
"id" TEXT NOT NULL,
"value" INTEGER NOT NULL,
"currency" TEXT NOT NULL,
CONSTRAINT "OffersCurrency_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersEducation" (
"id" TEXT NOT NULL,
"type" TEXT,
"field" TEXT,
"isAttending" BOOLEAN,
"school" TEXT,
"startDate" TIMESTAMP(3),
"endDate" TIMESTAMP(3),
"backgroundId" TEXT NOT NULL,
CONSTRAINT "OffersEducation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersReply" (
"id" TEXT NOT NULL,
"creator" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"message" TEXT NOT NULL,
"replyingToId" TEXT,
"profileId" TEXT NOT NULL,
CONSTRAINT "OffersReply_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersOffer" (
"id" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"companyId" TEXT NOT NULL,
"monthYearReceived" TIMESTAMP(3) NOT NULL,
"location" TEXT NOT NULL,
"negotiationStrategy" TEXT,
"comments" TEXT,
"jobType" "JobType" NOT NULL,
CONSTRAINT "OffersOffer_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersIntern" (
"offerId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"specialization" TEXT NOT NULL,
"internshipCycle" TEXT NOT NULL,
"startYear" INTEGER NOT NULL,
"monthlySalaryId" TEXT NOT NULL,
CONSTRAINT "OffersIntern_pkey" PRIMARY KEY ("offerId")
);
-- CreateTable
CREATE TABLE "OffersFullTime" (
"offerId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"specialization" TEXT NOT NULL,
"level" TEXT NOT NULL,
"totalCompensationId" TEXT NOT NULL,
"baseSalaryId" TEXT NOT NULL,
"bonusId" TEXT NOT NULL,
"stocksId" TEXT NOT NULL,
CONSTRAINT "OffersFullTime_pkey" PRIMARY KEY ("offerId")
);
-- CreateIndex
CREATE UNIQUE INDEX "OffersBackground_offersProfileId_key" ON "OffersBackground"("offersProfileId");
-- CreateIndex
CREATE UNIQUE INDEX "OffersExperience_totalCompensationId_key" ON "OffersExperience"("totalCompensationId");
-- CreateIndex
CREATE UNIQUE INDEX "OffersExperience_monthlySalaryId_key" ON "OffersExperience"("monthlySalaryId");
-- CreateIndex
CREATE UNIQUE INDEX "OffersIntern_monthlySalaryId_key" ON "OffersIntern"("monthlySalaryId");
-- CreateIndex
CREATE UNIQUE INDEX "OffersFullTime_totalCompensationId_key" ON "OffersFullTime"("totalCompensationId");
-- CreateIndex
CREATE UNIQUE INDEX "OffersFullTime_baseSalaryId_key" ON "OffersFullTime"("baseSalaryId");
-- CreateIndex
CREATE UNIQUE INDEX "OffersFullTime_bonusId_key" ON "OffersFullTime"("bonusId");
-- CreateIndex
CREATE UNIQUE INDEX "OffersFullTime_stocksId_key" ON "OffersFullTime"("stocksId");
-- AddForeignKey
ALTER TABLE "OffersProfile" ADD CONSTRAINT "OffersProfile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersBackground" ADD CONSTRAINT "OffersBackground_offersProfileId_fkey" FOREIGN KEY ("offersProfileId") REFERENCES "OffersProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersSpecificYoe" ADD CONSTRAINT "OffersSpecificYoe_backgroundId_fkey" FOREIGN KEY ("backgroundId") REFERENCES "OffersBackground"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersExperience" ADD CONSTRAINT "OffersExperience_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersExperience" ADD CONSTRAINT "OffersExperience_totalCompensationId_fkey" FOREIGN KEY ("totalCompensationId") REFERENCES "OffersCurrency"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersExperience" ADD CONSTRAINT "OffersExperience_monthlySalaryId_fkey" FOREIGN KEY ("monthlySalaryId") REFERENCES "OffersCurrency"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersExperience" ADD CONSTRAINT "OffersExperience_backgroundId_fkey" FOREIGN KEY ("backgroundId") REFERENCES "OffersBackground"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersEducation" ADD CONSTRAINT "OffersEducation_backgroundId_fkey" FOREIGN KEY ("backgroundId") REFERENCES "OffersBackground"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersReply" ADD CONSTRAINT "OffersReply_replyingToId_fkey" FOREIGN KEY ("replyingToId") REFERENCES "OffersReply"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersReply" ADD CONSTRAINT "OffersReply_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "OffersProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersOffer" ADD CONSTRAINT "OffersOffer_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "OffersProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersOffer" ADD CONSTRAINT "OffersOffer_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersIntern" ADD CONSTRAINT "OffersIntern_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "OffersOffer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersIntern" ADD CONSTRAINT "OffersIntern_monthlySalaryId_fkey" FOREIGN KEY ("monthlySalaryId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "OffersOffer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_totalCompensationId_fkey" FOREIGN KEY ("totalCompensationId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_baseSalaryId_fkey" FOREIGN KEY ("baseSalaryId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_bonusId_fkey" FOREIGN KEY ("bonusId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_stocksId_fkey" FOREIGN KEY ("stocksId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

@ -58,6 +58,7 @@ model User {
questionsAnswerVotes QuestionsAnswerVote[]
questionsAnswerComments QuestionsAnswerComment[]
questionsAnswerCommentVotes QuestionsAnswerCommentVote[]
OffersProfile OffersProfile[]
}
enum Vote {
@ -98,6 +99,8 @@ model Company {
updatedAt DateTime @updatedAt
questionsQuestionEncounter QuestionsQuestionEncounter[]
OffersExperience OffersExperience[]
OffersOffer OffersOffer[]
}
// Start of Resumes project models.
@ -172,6 +175,179 @@ model ResumesCommentVote {
// Add Offers project models here, prefix all models with "Offer",
// use camelCase for field names, and try to name them consistently
// across all models in this file.
model OffersProfile {
id String @id @default(cuid())
profileName String @unique
createdAt DateTime @default(now())
background OffersBackground?
editToken String
discussion OffersReply[]
offers OffersOffer[]
user User? @relation(fields: [userId], references: [id])
userId String?
}
model OffersBackground {
id String @id @default(cuid())
totalYoe Int?
specificYoes OffersSpecificYoe[]
experiences OffersExperience[] // For extensibility in the future
educations OffersEducation[] // For extensibility in the future
profile OffersProfile @relation(fields: [offersProfileId], references: [id])
offersProfileId String @unique
}
model OffersSpecificYoe {
id String @id @default(cuid())
yoe Int
domain String
background OffersBackground @relation(fields: [backgroundId], references: [id])
backgroundId String
}
model OffersExperience {
id String @id @default(cuid())
company Company? @relation(fields: [companyId], references: [id])
companyId String?
jobType JobType?
title String?
// Add more fields
durationInMonths Int?
specialization String?
// FULLTIME fields
level String?
totalCompensation OffersCurrency? @relation("ExperienceTotalCompensation", fields: [totalCompensationId], references: [id])
totalCompensationId String? @unique
// INTERN fields
monthlySalary OffersCurrency? @relation("ExperienceMonthlySalary", fields: [monthlySalaryId], references: [id])
monthlySalaryId String? @unique
background OffersBackground @relation(fields: [backgroundId], references: [id])
backgroundId String
}
model OffersCurrency {
id String @id @default(cuid())
value Int
currency String
// Experience
OffersExperienceTotalCompensation OffersExperience? @relation("ExperienceTotalCompensation")
OffersExperienceMonthlySalary OffersExperience? @relation("ExperienceMonthlySalary")
// Full Time
OffersTotalCompensation OffersFullTime? @relation("OfferTotalCompensation")
OffersBaseSalary OffersFullTime? @relation("OfferBaseSalary")
OffersBonus OffersFullTime? @relation("OfferBonus")
OffersStocks OffersFullTime? @relation("OfferStocks")
// Intern
OffersMonthlySalary OffersIntern?
}
enum JobType {
INTERN
FULLTIME
}
model OffersEducation {
id String @id @default(cuid())
type String?
field String?
// Add more fields
school String?
startDate DateTime?
endDate DateTime?
background OffersBackground @relation(fields: [backgroundId], references: [id])
backgroundId String
}
model OffersReply {
id String @id @default(cuid())
creator String
createdAt DateTime @default(now())
message String
replyingToId String?
replyingTo OffersReply? @relation("ReplyThread", fields: [replyingToId], references: [id])
replies OffersReply[] @relation("ReplyThread")
profile OffersProfile @relation(fields: [profileId], references: [id])
profileId String
}
model OffersOffer {
id String @id @default(cuid())
profile OffersProfile @relation(fields: [profileId], references: [id])
profileId String
company Company @relation(fields: [companyId], references: [id])
companyId String
monthYearReceived DateTime
location String
negotiationStrategy String?
comments String?
jobType JobType
OffersIntern OffersIntern? @relation(fields: [offersInternId], references: [id])
offersInternId String? @unique
OffersFullTime OffersFullTime? @relation(fields: [offersFullTimeId], references: [id])
offersFullTimeId String? @unique
}
model OffersIntern {
id String @id @default(cuid())
title String
specialization String
internshipCycle String
startYear Int
monthlySalary OffersCurrency @relation(fields: [monthlySalaryId], references: [id])
monthlySalaryId String @unique
OffersOffer OffersOffer?
}
model OffersFullTime {
id String @id @default(cuid())
title String
specialization String
level String
totalCompensation OffersCurrency @relation("OfferTotalCompensation", fields: [totalCompensationId], references: [id])
totalCompensationId String @unique
baseSalary OffersCurrency @relation("OfferBaseSalary", fields: [baseSalaryId], references: [id])
baseSalaryId String @unique
bonus OffersCurrency @relation("OfferBonus", fields: [bonusId], references: [id])
bonusId String @unique
stocks OffersCurrency @relation("OfferStocks", fields: [stocksId], references: [id])
stocksId String @unique
OffersOffer OffersOffer?
}
// End of Offers project models.
// Start of Questions project models.

@ -35,9 +35,37 @@ const COMPANIES = [
},
];
const OFFER_PROFILES = [
{
id: 'cl91v97ex000109mt7fka5rto',
profileName: 'battery-horse-stable-cow',
editToken: 'cl91ulmhg000009l86o45aspt',
},
{
id: 'cl91v9iw2000209mtautgdnxq',
profileName: 'house-zebra-fast-giraffe',
editToken: 'cl91umigc000109l80f1tcqe8',
},
{
id: 'cl91v9m3y000309mt1ctw55wi',
profileName: 'keyboard-mouse-lazy-cat',
editToken: 'cl91ummoa000209l87q2b8hl7',
},
{
id: 'cl91v9p09000409mt5rvoasf1',
profileName: 'router-hen-bright-pig',
editToken: 'cl91umqa3000309l87jyefe9k',
},
{
id: 'cl91v9uda000509mt5i5fez3v',
profileName: 'screen-ant-dirty-bird',
editToken: 'cl91umuj9000409l87ez85vmg',
},
];
async function main() {
console.log('Seeding started...');
await Promise.all(
await Promise.all([
COMPANIES.map(async (company) => {
await prisma.company.upsert({
where: { slug: company.slug },
@ -45,7 +73,14 @@ async function main() {
create: company,
});
}),
);
OFFER_PROFILES.map(async (offerProfile) => {
await prisma.offersProfile.upsert({
where: { profileName: offerProfile.profileName },
update: offerProfile,
create: offerProfile,
});
}),
]);
console.log('Seeding completed.');
}

@ -7,6 +7,11 @@ const navigation: ProductNavigationItems = [
name: 'Browse',
},
{ children: [], href: '/resumes/submit', name: 'Submit for review' },
{
children: [],
href: 'https://www.techinterviewhandbook.org/resume/',
name: 'Resume Guide',
},
];
const config = {

@ -0,0 +1,24 @@
import { signIn } from 'next-auth/react';
type Props = Readonly<{
text: string;
}>;
export default function SignInButton({ text }: Props) {
return (
<div className="flex justify-center pt-4">
<p>
<a
className="text-primary-800 hover:text-primary-500"
href="/api/auth/signin"
onClick={(event) => {
event.preventDefault();
signIn();
}}>
Sign in
</a>{' '}
{text}
</p>
</div>
);
}

@ -14,7 +14,7 @@ type Props = Readonly<{
export default function BrowseListItem({ href, resumeInfo }: Props) {
return (
<Link href={href}>
<div className="grid grid-cols-8 border-b border-slate-200 p-4">
<div className="grid grid-cols-8 gap-4 border-b border-slate-200 p-4">
<div className="col-span-4">
{resumeInfo.title}
<div className="mt-2 flex items-center justify-start text-xs text-indigo-500">

@ -19,17 +19,15 @@ export default function ResumeListItems({ isLoading, resumes }: Props) {
}
return (
<div className="col-span-10 pr-8">
<ul role="list">
{resumes.map((resumeObj: Resume) => (
<li key={resumeObj.id}>
<ResumseListItem
href={`resumes/${resumeObj.id}`}
resumeInfo={resumeObj}
/>
</li>
))}
</ul>
</div>
<ul role="list">
{resumes.map((resumeObj: Resume) => (
<li key={resumeObj.id}>
<ResumseListItem
href={`resumes/${resumeObj.id}`}
resumeInfo={resumeObj}
/>
</li>
))}
</ul>
);
}

@ -1,11 +1,13 @@
import { useSession } from 'next-auth/react';
import { useState } from 'react';
import { Tabs } from '@tih/ui';
import { Button } from '@tih/ui';
import { trpc } from '~/utils/trpc';
import CommentListItems from './CommentListItems';
import CommentsListButton from './CommentsListButton';
import { COMMENTS_SECTIONS } from './constants';
import SignInButton from '../SignInButton';
type CommentsListProps = Readonly<{
resumeId: string;
@ -16,13 +18,27 @@ export default function CommentsList({
resumeId,
setShowCommentsForm,
}: CommentsListProps) {
const { data: sessionData } = useSession();
const [tab, setTab] = useState(COMMENTS_SECTIONS[0].value);
const commentsQuery = trpc.useQuery(['resumes.reviews.list', { resumeId }]);
const renderButton = () => {
if (sessionData === null) {
return <SignInButton text="to join discussion" />;
}
return (
<Button
display="block"
label="Add your review"
variant="tertiary"
onClick={() => setShowCommentsForm(true)}
/>
);
};
return (
<div className="space-y-3">
<CommentsListButton setShowCommentsForm={setShowCommentsForm} />
{renderButton()}
<Tabs
label="comments"
tabs={COMMENTS_SECTIONS}

@ -1,48 +0,0 @@
import { signIn, useSession } from 'next-auth/react';
import { Button } from '@tih/ui';
type CommentsListButtonProps = {
setShowCommentsForm: (show: boolean) => void;
};
export default function CommentsListButton({
setShowCommentsForm,
}: CommentsListButtonProps) {
const { data: session, status } = useSession();
const isSessionLoading = status === 'loading';
// Don't render anything
if (isSessionLoading) {
return null;
}
// Not signed in
if (session == null) {
return (
<div className="flex justify-center">
<p>
<a
className="text-primary-800 hover:text-primary-500"
href="/api/auth/signin"
onClick={(event) => {
event.preventDefault();
signIn();
}}>
Sign in
</a>{' '}
to join discussion
</p>
</div>
);
}
// Signed in. Return Add review button
return (
<Button
display="block"
label="Add your review"
variant="tertiary"
onClick={() => setShowCommentsForm(true)}
/>
);
}

@ -0,0 +1,26 @@
import React from 'react';
import { trpc } from '~/utils/trpc';
function test() {
const data = trpc.useQuery([
'offers.list',
{
limit: 3,
location: 'Singapore, Singapore',
offset: 0,
sortBy: '-monthYearReceived',
yoeCategory: 0,
},
]);
return (
<ul>
{data.data?.map((x) => {
return <li key={x.id}>{JSON.stringify(x)}</li>;
})}
</ul>
);
}
export default test;

@ -0,0 +1,144 @@
import React, { useState } from 'react';
import { trpc } from '~/utils/trpc';
function Test() {
// F const data = trpc.useQuery([
// 'offers.profile.',
// {
// limit: 3,
// location: 'Singapore, Singapore',
// offset: 0,
// yoeCategory: 0,
// },
// ]);
const [createdData, setCreatedData] = useState("")
const createMutation = trpc.useMutation(['offers.profile.create'], {
onError(error: any) {
alert(error)
},
onSuccess(data) {
setCreatedData(JSON.stringify(data))
},
});
const handleClick = () => {
createMutation.mutate({
"background": {
"educations": [
{
"endDate": new Date("2018-09-30T07:58:54.000Z"),
"field": "Computer Science",
"school": "National University of Singapore",
"startDate": new Date("2014-09-30T07:58:54.000Z"),
"type": "Bachelors"
}
],
"experiences": [
{
"companyId": "cl92ly8xm0000w3mwh5ymyqmx",
"durationInMonths": 24,
"jobType": "FULLTIME",
"level": "Junior",
// "monthlySalary": undefined,
"specialization": "Front End",
"title": "Software Engineer",
"totalCompensation": {
"currency": "SGD",
"value": 104100
}
}
],
"specificYoes": [
{
"domain": "Front End",
"yoe": 2
},
{
"domain": "Full Stack",
"yoe": 2
}
],
"totalYoe": 4
},
"offers": [
{
"comments": "",
"companyId": "cl92ly8xm0000w3mwh5ymyqmx",
"job": {
"base": {
"currency": "SGD",
"value": 84000
},
"bonus": {
"currency": "SGD",
"value": 20000
},
"level": "Junior",
"specialization": "Front End",
"stocks": {
"currency": "SGD",
"value": 100
},
"title": "Software Engineer",
"totalCompensation": {
"currency": "SGD",
"value": 104100
}
},
"jobType": "FULLTIME",
"location": "Singapore, Singapore",
"monthYearReceived": new Date("2022-09-30T07:58:54.000Z"),
"negotiationStrategy": "Leveraged having multiple offers"
},
{
"comments": "",
"companyId": "cl92ly8xm0000w3mwh5ymyqmx",
"job": {
"base": {
"currency": "SGD",
"value": 84000
},
"bonus": {
"currency": "SGD",
"value": 20000
},
"level": "Junior",
"specialization": "Front End",
"stocks": {
"currency": "SGD",
"value": 100
},
"title": "Software Engineer",
"totalCompensation": {
"currency": "SGD",
"value": 104100
}
},
"jobType": "FULLTIME",
"location": "Singapore, Singapore",
"monthYearReceived": new Date("2022-09-30T07:58:54.000Z"),
"negotiationStrategy": "Leveraged having multiple offers"
}
]
});
};
return (
// <ul>
// {createdData.map((x) => {
// return <li key={x.id}>{JSON.stringify(x)}</li>;
// })}
// </ul>
<>
<div>
{createdData}
</div>
<button type="button" onClick={handleClick}>Click me</button>
</>
);
}
export default Test;

@ -23,6 +23,7 @@ import {
import FilterPill from '~/components/resumes/browse/FilterPill';
import ResumeListItems from '~/components/resumes/browse/ResumeListItems';
import ResumeReviewsTitle from '~/components/resumes/ResumeReviewsTitle';
import SignInButton from '~/components/resumes/SignInButton';
import { trpc } from '~/utils/trpc';
@ -52,29 +53,44 @@ export default function ResumeHomePage() {
const [tabsValue, setTabsValue] = useState(BROWSE_TABS_VALUES.ALL);
const [searchValue, setSearchValue] = useState('');
const [resumes, setResumes] = useState<Array<Resume>>([]);
const [renderSignInButton, setRenderSignInButton] = useState(false);
const [signInButtonText, setSignInButtonText] = useState('');
const allResumesQuery = trpc.useQuery(['resumes.resume.findAll'], {
enabled: tabsValue === BROWSE_TABS_VALUES.ALL,
onSuccess: (data) => {
setResumes(data);
setRenderSignInButton(false);
},
});
const starredResumesQuery = trpc.useQuery(
['resumes.resume.user.findUserStarred'],
{
enabled: tabsValue === BROWSE_TABS_VALUES.STARRED,
onError: () => {
setResumes([]);
setRenderSignInButton(true);
setSignInButtonText('to view starred resumes');
},
onSuccess: (data) => {
setResumes(data);
},
retry: false,
},
);
const myResumesQuery = trpc.useQuery(
['resumes.resume.user.findUserCreated'],
{
enabled: tabsValue === BROWSE_TABS_VALUES.MY,
onError: () => {
setResumes([]);
setRenderSignInButton(true);
setSignInButtonText('to view your submitted resumes');
},
onSuccess: (data) => {
setResumes(data);
},
retry: false,
},
);
@ -270,14 +286,17 @@ export default function ResumeHomePage() {
</form>
</div>
</div>
<ResumeListItems
isLoading={
allResumesQuery.isFetching ||
starredResumesQuery.isFetching ||
myResumesQuery.isFetching
}
resumes={resumes}
/>
<div className="col-span-10 pr-8">
{renderSignInButton && <SignInButton text={signInButtonText} />}
<ResumeListItems
isLoading={
allResumesQuery.isFetching ||
starredResumesQuery.isFetching ||
myResumesQuery.isFetching
}
resumes={resumes}
/>
</div>
</div>
</div>
</div>

@ -2,6 +2,8 @@ import superjson from 'superjson';
import { companiesRouter } from './companies-router';
import { createRouter } from './context';
import { offersRouter } from './offers';
import { offersProfileRouter } from './offers-profile-router';
import { protectedExampleRouter } from './protected-example-router';
import { questionsAnswerCommentRouter } from './questions-answer-comment-router';
import { questionsAnswerRouter } from './questions-answer-router';
@ -32,7 +34,9 @@ export const appRouter = createRouter()
.merge('questions.answers.comments.', questionsAnswerCommentRouter)
.merge('questions.answers.', questionsAnswerRouter)
.merge('questions.questions.comments.', questionsQuestionCommentRouter)
.merge('questions.questions.', questionsQuestionRouter);
.merge('questions.questions.', questionsQuestionRouter)
.merge('offers.', offersRouter)
.merge('offers.profile.', offersProfileRouter);
// Export type definition of API
export type AppRouter = typeof appRouter;

@ -0,0 +1,269 @@
import crypto, { randomUUID } from "crypto";
import { z } from "zod";
import { Prisma } from "@prisma/client";
import { createRouter } from "./context";
const valuation = z.object({
currency: z.string(),
value: z.number(),
})
// TODO: handle both full time and intern
const offer = z.object({
comments: z.string(),
companyId: z.string(),
job: z.object({
base: valuation.optional(), // Full time
bonus: valuation.optional(), // Full time
internshipCycle: z.string().optional(), // Intern
level: z.string().optional(), // Full time
monthlySalary: valuation.optional(), // Intern
specialization: z.string(),
startYear: z.number().optional(), // Intern
stocks: valuation.optional(), // Full time
title: z.string(),
totalCompensation: valuation.optional(), // Full time
}),
jobType: z.string(),
location: z.string(),
monthYearReceived: z.date(),
negotiationStrategy: z.string(),
})
const experience = z.object({
companyId: z.string().optional(),
durationInMonths: z.number().optional(),
jobType: z.string().optional(),
level: z.string().optional(),
monthlySalary: valuation.optional(),
specialization: z.string().optional(),
title: z.string().optional(),
totalCompensation: valuation.optional(),
})
const education = z.object({
endDate: z.date().optional(),
field: z.string().optional(),
school: z.string().optional(),
startDate: z.date().optional(),
type: z.string().optional(),
})
export const offersProfileRouter = createRouter().mutation(
'create',
{
input: z.object({
background: z.object({
educations: z.array(education),
experiences: z.array(experience),
specificYoes: z.array(z.object({
domain: z.string(),
yoe: z.number()
})),
totalYoe: z.number().optional(),
}),
offers: z.array(offer)
}),
async resolve({ ctx, input }) {
// TODO: add more
const token = crypto
.createHash("sha256")
.update(Date.now().toString())
.digest("hex");
const profile = await ctx.prisma.offersProfile.create({
data: {
background: {
create: {
educations: {
create:
input.background.educations.map((x) => ({
endDate: x.endDate,
field: x.field,
school: x.school,
startDate: x.startDate,
type: x.type
}))
},
experiences: {
create:
input.background.experiences.map((x) => {
if (x.jobType === "FULLTIME" && x.totalCompensation?.currency !== undefined && x.totalCompensation.value !== undefined) {
return {
company: {
connect: {
id: x.companyId
}
},
durationInMonths: x.durationInMonths,
jobType: x.jobType,
level: x.level,
specialization: x.specialization,
title: x.title,
totalCompensation: {
create: {
currency: x.totalCompensation?.currency,
value: x.totalCompensation?.value,
}
},
}
}
if (x.jobType === "INTERN" && x.monthlySalary?.currency !== undefined && x.monthlySalary.value !== undefined) {
return {
company: {
connect: {
id: x.companyId
}
},
durationInMonths: x.durationInMonths,
jobType: x.jobType,
monthlySalary: {
create: {
currency: x.monthlySalary?.currency,
value: x.monthlySalary?.value
}
},
specialization: x.specialization,
title: x.title,
}
}
throw Prisma.PrismaClientKnownRequestError
})
},
specificYoes: {
create:
input.background.specificYoes.map((x) => ({
domain: x.domain,
yoe: x.yoe
}))
},
totalYoe: input.background.totalYoe,
}
},
editToken: token,
offers: {
create:
input.offers.map((x) => {
if (x.jobType === "INTERN" && x.job.internshipCycle !== undefined && x.job.monthlySalary?.currency !== undefined && x.job.monthlySalary.value !== undefined && x.job.startYear !== undefined) {
return {
OffersIntern: {
create: {
internshipCycle: x.job.internshipCycle,
monthlySalary: {
create: {
currency: x.job.monthlySalary?.currency,
value: x.job.monthlySalary?.value
}
},
specialization: x.job.specialization,
startYear: x.job.startYear,
title: x.job.title,
}
},
comments: x.comments,
company: {
connect: {
id: x.companyId
}
},
jobType: x.jobType,
location: x.location,
monthYearReceived: x.monthYearReceived,
negotiationStrategy: x.negotiationStrategy
}
}
if (x.jobType === "FULLTIME" && x.job.base?.currency !== undefined && x.job.base?.value !== undefined && x.job.bonus?.currency !== undefined && x.job.bonus?.value !== undefined && x.job.stocks?.currency !== undefined && x.job.stocks?.value !== undefined && x.job.totalCompensation?.currency !== undefined && x.job.totalCompensation?.value !== undefined && x.job.level !== undefined) {
return {
OffersFullTime: {
create: {
baseSalary: {
create: {
currency: x.job.base?.currency,
value: x.job.base?.value
}
},
bonus: {
create: {
currency: x.job.bonus?.currency,
value: x.job.bonus?.value
}
},
level: x.job.level,
specialization: x.job.specialization,
stocks: {
create: {
currency: x.job.stocks?.currency,
value: x.job.stocks?.value,
}
},
title: x.job.title,
totalCompensation: {
create: {
currency: x.job.totalCompensation?.currency,
value: x.job.totalCompensation?.value,
}
},
}
},
comments: x.comments,
company: {
connect: {
id: x.companyId
}
},
jobType: x.jobType,
location: x.location,
monthYearReceived: x.monthYearReceived,
negotiationStrategy: x.negotiationStrategy
}
}
// Throw error
throw Prisma.PrismaClientKnownRequestError
})
},
profileName: randomUUID(),
},
include: {
background: {
include: {
educations: true,
experiences: {
include: {
company: true,
monthlySalary: true,
totalCompensation: true
}
},
specificYoes: true
}
},
offers: {
include: {
OffersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true
}
},
OffersIntern: {
include: {
monthlySalary: true
}
}
}
}
},
});
// TODO: add analysis to profile object then return
return profile
}
},
);

@ -0,0 +1,297 @@
import assert from 'assert';
import { z } from 'zod';
import { createRouter } from './context';
const yoeCategoryMap: Record<number, string> = {
0: 'Internship',
1: 'Fresh Grad',
2: 'Mid',
3: 'Senior',
};
const getYoeRange = (yoeCategory: number) => {
return yoeCategoryMap[yoeCategory] === 'Fresh Grad'
? { maxYoe: 3, minYoe: 0 }
: yoeCategoryMap[yoeCategory] === 'Mid'
? { maxYoe: 7, minYoe: 4 }
: yoeCategoryMap[yoeCategory] === 'Senior'
? { maxYoe: null, minYoe: 8 }
: null;
};
const ascOrder = '+';
const descOrder = '-';
const sortingKeys = ['monthYearReceived', 'totalCompensation', 'yoe'];
const createSortByValidationRegex = () => {
const startsWithPlusOrMinusOnly = '^[+-]{1}';
const sortingKeysRegex = sortingKeys.join('|');
return new RegExp(startsWithPlusOrMinusOnly + '(' + sortingKeysRegex + ')');
};
export const offersRouter = createRouter().query('list', {
input: z.object({
company: z.string().nullish(),
dateEnd: z.date().nullish(),
dateStart: z.date().nullish(),
limit: z.number().nonnegative(),
location: z.string(),
offset: z.number().nonnegative(),
salaryMax: z.number().nullish(),
salaryMin: z.number().nonnegative().nullish(),
sortBy: z.string().regex(createSortByValidationRegex()).nullish(),
title: z.string().nullish(),
yoeCategory: z.number().min(0).max(3),
}),
async resolve({ ctx, input }) {
const yoeRange = getYoeRange(input.yoeCategory);
let data = !yoeRange
? await ctx.prisma.offersOffer.findMany({
// Internship
include: {
OffersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
},
skip: input.limit * input.offset,
take: input.limit,
where: {
AND: [
{
location: input.location,
},
{
OffersIntern: {
isNot: null,
},
},
{
OffersFullTime: {
is: null,
},
},
],
},
})
: yoeRange.maxYoe
? await ctx.prisma.offersOffer.findMany({
include: {
OffersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
},
// Junior, Mid
skip: input.limit * input.offset,
take: input.limit,
where: {
AND: [
{
location: input.location,
},
{
OffersIntern: {
is: null,
},
},
{
OffersFullTime: {
isNot: null,
},
},
{
profile: {
background: {
totalYoe: {
gte: yoeRange.minYoe,
},
},
},
},
{
profile: {
background: {
totalYoe: {
gte: yoeRange.maxYoe,
},
},
},
},
],
},
})
: await ctx.prisma.offersOffer.findMany({
// Senior
include: {
OffersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
},
skip: input.limit * input.offset,
take: input.limit,
where: {
AND: [
{
location: input.location,
},
{
OffersIntern: {
is: null,
},
},
{
OffersFullTime: {
isNot: null,
},
},
{
profile: {
background: {
totalYoe: {
gte: yoeRange.minYoe,
},
},
},
},
],
},
});
data = data.filter((offer) => {
let validRecord = true;
if (input.company) {
validRecord = validRecord && offer.company.name === input.company;
}
if (input.title) {
validRecord =
validRecord &&
(offer.OffersFullTime?.title === input.title ||
offer.OffersIntern?.title === input.title);
}
if (input.dateStart && input.dateEnd) {
validRecord =
validRecord &&
offer.monthYearReceived.getTime() >= input.dateStart.getTime() &&
offer.monthYearReceived.getTime() <= input.dateEnd.getTime();
}
if (input.salaryMin && input.salaryMax) {
const salary = offer.OffersFullTime?.totalCompensation.value
? offer.OffersFullTime?.totalCompensation.value
: offer.OffersIntern?.monthlySalary.value;
assert(salary);
validRecord =
validRecord && salary >= input.salaryMin && salary <= input.salaryMax;
}
return validRecord;
});
data = data.sort((offer1, offer2) => {
const defaultReturn =
offer2.monthYearReceived.getTime() - offer1.monthYearReceived.getTime();
if (!input.sortBy) {
return defaultReturn;
}
const order = input.sortBy.charAt(0);
const sortingKey = input.sortBy.substring(1);
if (order === ascOrder) {
return (() => {
if (sortingKey === 'monthYearReceived') {
return (
offer1.monthYearReceived.getTime() -
offer2.monthYearReceived.getTime()
);
}
if (sortingKey === 'totalCompensation') {
const salary1 = offer1.OffersFullTime?.totalCompensation.value
? offer1.OffersFullTime?.totalCompensation.value
: offer1.OffersIntern?.monthlySalary.value;
const salary2 = offer2.OffersFullTime?.totalCompensation.value
? offer2.OffersFullTime?.totalCompensation.value
: offer2.OffersIntern?.monthlySalary.value;
if (salary1 && salary2) {
return salary1 - salary2;
}
}
return defaultReturn;
})();
}
if (order === descOrder) {
return (() => {
if (sortingKey === 'monthYearReceived') {
return (
offer2.monthYearReceived.getTime() -
offer1.monthYearReceived.getTime()
);
}
if (sortingKey === 'totalCompensation') {
const salary1 = offer1.OffersFullTime?.totalCompensation.value
? offer1.OffersFullTime?.totalCompensation.value
: offer1.OffersIntern?.monthlySalary.value;
const salary2 = offer2.OffersFullTime?.totalCompensation.value
? offer2.OffersFullTime?.totalCompensation.value
: offer2.OffersIntern?.monthlySalary.value;
if (salary1 && salary2) {
return salary2 - salary1;
}
}
return defaultReturn;
})();
}
return defaultReturn;
});
return data;
},
});

@ -8,6 +8,7 @@ export type SlideOutEnterFrom = 'end' | 'start';
type Props = Readonly<{
children: React.ReactNode;
className: string;
enterFrom?: SlideOutEnterFrom;
isShown?: boolean;
onClose?: () => void;
@ -40,6 +41,7 @@ const enterFromClasses: Record<
export default function SlideOut({
children,
className,
enterFrom = 'end',
isShown = false,
size,
@ -50,7 +52,10 @@ export default function SlideOut({
return (
<Transition.Root as={Fragment} show={isShown}>
<Dialog as="div" className="relative z-40" onClose={() => onClose?.()}>
<Dialog
as="div"
className={clsx('relative z-40', className)}
onClose={() => onClose?.()}>
<Transition.Child
as={Fragment}
enter="transition-opacity ease-linear duration-300"

Loading…
Cancel
Save