diff --git a/apps/portal/package.json b/apps/portal/package.json index 88ad7dfd..f1bdb9e7 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -30,6 +30,7 @@ "next-auth": "~4.10.3", "react": "18.2.0", "react-dom": "18.2.0", + "react-dropzone": "^14.2.3", "react-hook-form": "^7.36.1", "react-pdf": "^5.7.2", "react-query": "^3.39.2", diff --git a/apps/portal/prisma/migrations/20221010055218_update_question_encounter_to_use_company_id/migration.sql b/apps/portal/prisma/migrations/20221010055218_update_question_encounter_to_use_company_id/migration.sql new file mode 100644 index 00000000..c79e0b9c --- /dev/null +++ b/apps/portal/prisma/migrations/20221010055218_update_question_encounter_to_use_company_id/migration.sql @@ -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; diff --git a/apps/portal/prisma/migrations/20221014192315_/migration.sql b/apps/portal/prisma/migrations/20221014192315_/migration.sql new file mode 100644 index 00000000..20352390 --- /dev/null +++ b/apps/portal/prisma/migrations/20221014192315_/migration.sql @@ -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; diff --git a/apps/portal/prisma/migrations/20221014205230_/migration.sql b/apps/portal/prisma/migrations/20221014205230_/migration.sql new file mode 100644 index 00000000..aec827dc --- /dev/null +++ b/apps/portal/prisma/migrations/20221014205230_/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "OffersAnalysis" ALTER COLUMN "overallPercentile" SET DATA TYPE DOUBLE PRECISION, +ALTER COLUMN "companyPercentile" SET DATA TYPE DOUBLE PRECISION; diff --git a/apps/portal/prisma/migrations/20221014211740_/migration.sql b/apps/portal/prisma/migrations/20221014211740_/migration.sql new file mode 100644 index 00000000..1c960010 --- /dev/null +++ b/apps/portal/prisma/migrations/20221014211740_/migration.sql @@ -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; diff --git a/apps/portal/prisma/migrations/20221019104025_update_offers_remove_optional/migration.sql b/apps/portal/prisma/migrations/20221019104025_update_offers_remove_optional/migration.sql new file mode 100644 index 00000000..ec31acd3 --- /dev/null +++ b/apps/portal/prisma/migrations/20221019104025_update_offers_remove_optional/migration.sql @@ -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; diff --git a/apps/portal/prisma/migrations/20221020101123_add_resume_comment_parent/migration.sql b/apps/portal/prisma/migrations/20221020101123_add_resume_comment_parent/migration.sql new file mode 100644 index 00000000..018ad4cd --- /dev/null +++ b/apps/portal/prisma/migrations/20221020101123_add_resume_comment_parent/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "ResumesComment" ADD COLUMN "parentId" TEXT; + +-- AddForeignKey +ALTER TABLE "ResumesComment" ADD CONSTRAINT "ResumesComment_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "ResumesComment"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/portal/prisma/migrations/20221020115540_/migration.sql b/apps/portal/prisma/migrations/20221020115540_/migration.sql new file mode 100644 index 00000000..a70f2aa5 --- /dev/null +++ b/apps/portal/prisma/migrations/20221020115540_/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "OffersExperience" ADD COLUMN "location" TEXT; diff --git a/apps/portal/prisma/migrations/20221021150358_add_vote_count_and_last_seen/migration.sql b/apps/portal/prisma/migrations/20221021150358_add_vote_count_and_last_seen/migration.sql new file mode 100644 index 00000000..a6319a17 --- /dev/null +++ b/apps/portal/prisma/migrations/20221021150358_add_vote_count_and_last_seen/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `upvotes` to the `QuestionsQuestion` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "QuestionsQuestion" ADD COLUMN "lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "upvotes" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "QuestionsQuestionEncounter" ADD COLUMN "netVotes" INTEGER NOT NULL DEFAULT 0; diff --git a/apps/portal/prisma/migrations/20221021151424_delete_extra_encounter_fields/migration.sql b/apps/portal/prisma/migrations/20221021151424_delete_extra_encounter_fields/migration.sql new file mode 100644 index 00000000..ef9e4229 --- /dev/null +++ b/apps/portal/prisma/migrations/20221021151424_delete_extra_encounter_fields/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `netVotes` on the `QuestionsQuestionEncounter` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "QuestionsQuestion" ALTER COLUMN "lastSeenAt" DROP DEFAULT, +ALTER COLUMN "upvotes" SET DEFAULT 0; + +-- AlterTable +ALTER TABLE "QuestionsQuestionEncounter" DROP COLUMN "netVotes"; diff --git a/apps/portal/prisma/migrations/20221021155717_add_sorting_index/migration.sql b/apps/portal/prisma/migrations/20221021155717_add_sorting_index/migration.sql new file mode 100644 index 00000000..6ae3366a --- /dev/null +++ b/apps/portal/prisma/migrations/20221021155717_add_sorting_index/migration.sql @@ -0,0 +1,5 @@ +-- CreateIndex +CREATE INDEX "QuestionsQuestion_lastSeenAt_id_idx" ON "QuestionsQuestion"("lastSeenAt", "id"); + +-- CreateIndex +CREATE INDEX "QuestionsQuestion_upvotes_id_idx" ON "QuestionsQuestion"("upvotes", "id"); diff --git a/apps/portal/prisma/migrations/20221021231817_/migration.sql b/apps/portal/prisma/migrations/20221021231817_/migration.sql new file mode 100644 index 00000000..3820338d --- /dev/null +++ b/apps/portal/prisma/migrations/20221021231817_/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `baseValue` to the `OffersCurrency` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `OffersCurrency` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "OffersCurrency" ADD COLUMN "baseCurrency" TEXT NOT NULL DEFAULT 'USD', +ADD COLUMN "baseValue" INTEGER NOT NULL, +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; diff --git a/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql b/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql new file mode 100644 index 00000000..089e963d --- /dev/null +++ b/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "OffersCurrency" ALTER COLUMN "value" SET DATA TYPE DOUBLE PRECISION, +ALTER COLUMN "baseValue" SET DATA TYPE DOUBLE PRECISION; diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index 61e45c6c..2b4aec91 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -1,105 +1,107 @@ // Refer to the Prisma schema docs: https://pris.ly/d/prisma-schema generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" } datasource db { - provider = "postgresql" - url = env("DATABASE_URL") + provider = "postgresql" + url = env("DATABASE_URL") } // Necessary for NextAuth. model Account { - id String @id @default(cuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? @db.Text - access_token String? @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? @db.Text - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([provider, providerAccountId]) + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) } model Session { - id String @id @default(cuid()) - sessionToken String @unique - userId String - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? - accounts Account[] - sessions Session[] - todos Todo[] - resumesResumes ResumesResume[] - resumesStars ResumesStar[] - resumesComments ResumesComment[] - resumesCommentVotes ResumesCommentVote[] - questionsQuestions QuestionsQuestion[] - questionsQuestionEncounters QuestionsQuestionEncounter[] - questionsQuestionVotes QuestionsQuestionVote[] - questionsQuestionComments QuestionsQuestionComment[] - questionsQuestionCommentVotes QuestionsQuestionCommentVote[] - questionsAnswers QuestionsAnswer[] - questionsAnswerVotes QuestionsAnswerVote[] - questionsAnswerComments QuestionsAnswerComment[] - questionsAnswerCommentVotes QuestionsAnswerCommentVote[] - OffersProfile OffersProfile[] - offersDiscussion OffersReply[] + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] + todos Todo[] + resumesResumes ResumesResume[] + resumesStars ResumesStar[] + resumesComments ResumesComment[] + resumesCommentVotes ResumesCommentVote[] + questionsQuestions QuestionsQuestion[] + questionsQuestionEncounters QuestionsQuestionEncounter[] + questionsQuestionVotes QuestionsQuestionVote[] + questionsQuestionComments QuestionsQuestionComment[] + questionsQuestionCommentVotes QuestionsQuestionCommentVote[] + questionsAnswers QuestionsAnswer[] + questionsAnswerVotes QuestionsAnswerVote[] + questionsAnswerComments QuestionsAnswerComment[] + questionsAnswerCommentVotes QuestionsAnswerCommentVote[] + OffersProfile OffersProfile[] + offersDiscussion OffersReply[] } enum Vote { - UPVOTE - DOWNVOTE + UPVOTE + DOWNVOTE } model VerificationToken { - identifier String - token String @unique - expires DateTime + identifier String + token String @unique + expires DateTime - @@unique([identifier, token]) + @@unique([identifier, token]) } model Todo { - id String @id @default(cuid()) - userId String - text String @db.Text - status TodoStatus @default(INCOMPLETE) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + userId String + text String @db.Text + status TodoStatus @default(INCOMPLETE) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) } enum TodoStatus { - INCOMPLETE - COMPLETE + INCOMPLETE + COMPLETE } model Company { - id String @id @default(cuid()) - name String @db.Text - slug String @unique - description String? @db.Text - logoUrl String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - OffersExperience OffersExperience[] - OffersOffer OffersOffer[] + id String @id @default(cuid()) + name String @db.Text + slug String @unique + description String? @db.Text + logoUrl String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + questionsQuestionEncounter QuestionsQuestionEncounter[] + OffersExperience OffersExperience[] + OffersOffer OffersOffer[] } // Start of Resumes project models. @@ -107,65 +109,68 @@ model Company { // use camelCase for field names, and try to name them consistently // across all models in this file. model ResumesResume { - id String @id @default(cuid()) - userId String - title String @db.Text - // TODO: Update role, experience, location to use Enums - role String @db.Text - experience String @db.Text - location String @db.Text - url String - additionalInfo String? @db.Text - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - stars ResumesStar[] - comments ResumesComment[] + id String @id @default(cuid()) + userId String + title String @db.Text + // TODO: Update role, experience, location to use Enums + role String @db.Text + experience String @db.Text + location String @db.Text + url String + additionalInfo String? @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + stars ResumesStar[] + comments ResumesComment[] } model ResumesStar { - id String @id @default(cuid()) - userId String - resumeId String - createdAt DateTime @default(now()) - resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + userId String + resumeId String + createdAt DateTime @default(now()) + resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@unique([userId, resumeId]) + @@unique([userId, resumeId]) } model ResumesComment { - id String @id @default(cuid()) - userId String - resumeId String - description String @db.Text - section ResumesSection - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade) - votes ResumesCommentVote[] - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + userId String + resumeId String + parentId String? + description String @db.Text + section ResumesSection + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade) + votes ResumesCommentVote[] + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + parent ResumesComment? @relation("parentComment", fields: [parentId], references: [id]) + children ResumesComment[] @relation("parentComment") } enum ResumesSection { - GENERAL - EDUCATION - EXPERIENCE - PROJECTS - SKILLS + GENERAL + EDUCATION + EXPERIENCE + PROJECTS + SKILLS } model ResumesCommentVote { - id String @id @default(cuid()) - userId String - commentId String - value Vote - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - comment ResumesComment @relation(fields: [commentId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + userId String + commentId String + value Vote + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + comment ResumesComment @relation(fields: [commentId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@unique([userId, commentId]) + @@unique([userId, commentId]) } // End of Resumes project models. @@ -176,176 +181,209 @@ model ResumesCommentVote { // across all models in this file. model OffersProfile { - id String @id @default(cuid()) - profileName String @unique - createdAt DateTime @default(now()) + id String @id @default(cuid()) + profileName String @unique + createdAt DateTime @default(now()) + + background OffersBackground? - background OffersBackground? + editToken String - editToken String + discussion OffersReply[] - discussion OffersReply[] + offers OffersOffer[] - offers OffersOffer[] + user User? @relation(fields: [userId], references: [id]) + userId String? - user User? @relation(fields: [userId], references: [id]) - userId String? + analysis OffersAnalysis? } model OffersBackground { - id String @id @default(cuid()) + id String @id @default(cuid()) - totalYoe Int? - specificYoes OffersSpecificYoe[] + totalYoe Int + specificYoes OffersSpecificYoe[] - experiences OffersExperience[] // For extensibility in the future + experiences OffersExperience[] - educations OffersEducation[] // For extensibility in the future + educations OffersEducation[] - profile OffersProfile @relation(fields: [offersProfileId], references: [id], onDelete: Cascade) - offersProfileId String @unique + profile OffersProfile @relation(fields: [offersProfileId], references: [id], onDelete: Cascade) + offersProfileId String @unique } model OffersSpecificYoe { - id String @id @default(cuid()) + id String @id @default(cuid()) - yoe Int - domain String + yoe Int + domain String - background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) - backgroundId String + background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) + backgroundId String } model OffersExperience { - id String @id @default(cuid()) + id String @id @default(cuid()) - company Company? @relation(fields: [companyId], references: [id]) - companyId String? + company Company? @relation(fields: [companyId], references: [id]) + companyId String? - jobType JobType? - title String? + jobType JobType? + title String? - // Add more fields - durationInMonths Int? - specialization String? + // Add more fields + durationInMonths Int? + specialization String? + location String? - // FULLTIME fields - level String? - totalCompensation OffersCurrency? @relation("ExperienceTotalCompensation", fields: [totalCompensationId], references: [id]) - totalCompensationId String? @unique + // 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 + // INTERN fields + monthlySalary OffersCurrency? @relation("ExperienceMonthlySalary", fields: [monthlySalaryId], references: [id]) + monthlySalaryId String? @unique - background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) - backgroundId String + background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) + backgroundId String } model OffersCurrency { - id String @id @default(cuid()) - value Int - currency String + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + value Float + currency String + + baseValue Float + baseCurrency String @default("USD") - // Experience - OffersExperienceTotalCompensation OffersExperience? @relation("ExperienceTotalCompensation") - OffersExperienceMonthlySalary OffersExperience? @relation("ExperienceMonthlySalary") + // 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") + // Full Time + OffersTotalCompensation OffersFullTime? @relation("OfferTotalCompensation") + OffersBaseSalary OffersFullTime? @relation("OfferBaseSalary") + OffersBonus OffersFullTime? @relation("OfferBonus") + OffersStocks OffersFullTime? @relation("OfferStocks") - // Intern - OffersMonthlySalary OffersIntern? + // Intern + OffersMonthlySalary OffersIntern? } enum JobType { - INTERN - FULLTIME + INTERN + FULLTIME } model OffersEducation { - id String @id @default(cuid()) - type String? - field String? + id String @id @default(cuid()) + type String? + field String? - school String? - startDate DateTime? - endDate DateTime? + school String? + startDate DateTime? + endDate DateTime? - background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) - backgroundId String + background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) + backgroundId String } model OffersReply { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - message String + id String @id @default(cuid()) + createdAt DateTime @default(now()) + message String - replyingToId String? - replyingTo OffersReply? @relation("ReplyThread", fields: [replyingToId], references: [id]) - replies OffersReply[] @relation("ReplyThread") + replyingToId String? + replyingTo OffersReply? @relation("ReplyThread", fields: [replyingToId], references: [id]) + replies OffersReply[] @relation("ReplyThread") - profile OffersProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) - profileId String + profile OffersProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) + profileId String - user User? @relation(fields: [userId], references: [id]) - userId String? + user User? @relation(fields: [userId], references: [id]) + userId String? } model OffersOffer { - id String @id @default(cuid()) + id String @id @default(cuid()) - profile OffersProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) - profileId String + profile OffersProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) + profileId String - company Company @relation(fields: [companyId], references: [id]) - companyId String + company Company @relation(fields: [companyId], references: [id]) + companyId String - monthYearReceived DateTime - location String - negotiationStrategy String? - comments String? + monthYearReceived DateTime + location String + negotiationStrategy String + comments String - jobType JobType + jobType JobType - OffersIntern OffersIntern? @relation(fields: [offersInternId], references: [id], onDelete: Cascade) - offersInternId String? @unique + offersIntern OffersIntern? @relation(fields: [offersInternId], references: [id], onDelete: Cascade) + offersInternId String? @unique - OffersFullTime OffersFullTime? @relation(fields: [offersFullTimeId], references: [id], onDelete: Cascade) - offersFullTimeId String? @unique + offersFullTime OffersFullTime? @relation(fields: [offersFullTimeId], references: [id], onDelete: Cascade) + offersFullTimeId String? @unique + + OffersAnalysis OffersAnalysis? @relation("HighestOverallOffer") + OffersAnalysisTopOverallOffers OffersAnalysis[] @relation("TopOverallOffers") + OffersAnalysisTopCompanyOffers OffersAnalysis[] @relation("TopCompanyOffers") } model OffersIntern { - id String @id @default(cuid()) + id String @id @default(cuid()) - title String - specialization String - internshipCycle String - startYear Int - monthlySalary OffersCurrency @relation(fields: [monthlySalaryId], references: [id], onDelete: Cascade) - monthlySalaryId String @unique + title String + specialization String + internshipCycle String + startYear Int + monthlySalary OffersCurrency @relation(fields: [monthlySalaryId], references: [id], onDelete: Cascade) + monthlySalaryId String @unique - OffersOffer OffersOffer? + OffersOffer OffersOffer? } model OffersFullTime { - id String @id @default(cuid()) - title String - specialization String - level String - totalCompensation OffersCurrency @relation("OfferTotalCompensation", fields: [totalCompensationId], references: [id], onDelete: Cascade) - totalCompensationId String @unique - baseSalary OffersCurrency @relation("OfferBaseSalary", fields: [baseSalaryId], references: [id], onDelete: Cascade) - baseSalaryId String @unique - bonus OffersCurrency @relation("OfferBonus", fields: [bonusId], references: [id], onDelete: Cascade) - bonusId String @unique - stocks OffersCurrency @relation("OfferStocks", fields: [stocksId], references: [id], onDelete: Cascade) - stocksId String @unique - - OffersOffer OffersOffer? + id String @id @default(cuid()) + title String + specialization String + level String + totalCompensation OffersCurrency @relation("OfferTotalCompensation", fields: [totalCompensationId], references: [id], onDelete: Cascade) + totalCompensationId String @unique + baseSalary OffersCurrency @relation("OfferBaseSalary", fields: [baseSalaryId], references: [id], onDelete: Cascade) + baseSalaryId String @unique + bonus OffersCurrency @relation("OfferBonus", fields: [bonusId], references: [id], onDelete: Cascade) + bonusId String @unique + stocks OffersCurrency @relation("OfferStocks", fields: [stocksId], references: [id], onDelete: Cascade) + stocksId String @unique + + OffersOffer OffersOffer? +} + +model OffersAnalysis { + id String @id @default(cuid()) + + profile OffersProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) + profileId String @unique + + overallHighestOffer OffersOffer @relation("HighestOverallOffer", fields: [offerId], references: [id], onDelete: Cascade) + offerId String @unique + + // OVERALL + overallPercentile Float + noOfSimilarOffers Int + topOverallOffers OffersOffer[] @relation("TopOverallOffers") + + // Company + companyPercentile Float + noOfSimilarCompanyOffers Int + topCompanyOffers OffersOffer[] @relation("TopCompanyOffers") } // End of Offers project models. @@ -356,140 +394,145 @@ model OffersFullTime { // across all models in this file. enum QuestionsQuestionType { - CODING - SYSTEM_DESIGN - BEHAVIORAL + CODING + SYSTEM_DESIGN + BEHAVIORAL } model QuestionsQuestion { - id String @id @default(cuid()) - userId String? - content String @db.Text - questionType QuestionsQuestionType - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + userId String? + content String @db.Text + questionType QuestionsQuestionType + lastSeenAt DateTime + upvotes Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - encounters QuestionsQuestionEncounter[] - votes QuestionsQuestionVote[] - comments QuestionsQuestionComment[] - answers QuestionsAnswer[] + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + encounters QuestionsQuestionEncounter[] + votes QuestionsQuestionVote[] + comments QuestionsQuestionComment[] + answers QuestionsAnswer[] - contentSearch Unsupported("TSVECTOR")? + contentSearch Unsupported("TSVECTOR")? - @@index([contentSearch]) + @@index([contentSearch]) + @@index([lastSeenAt, id]) + @@index([upvotes, id]) } model QuestionsQuestionEncounter { - id String @id @default(cuid()) - questionId String - userId String? - // TODO: sync with models - company String @db.Text - location String @db.Text - role String @db.Text - seenAt DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + questionId String + userId String? + // TODO: sync with models (location, role) + companyId String + location String @db.Text + role String @db.Text + seenAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) } model QuestionsQuestionVote { - id String @id @default(cuid()) - questionId String - userId String? - vote Vote - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + questionId String + userId String? + vote Vote + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) - @@unique([questionId, userId]) + @@unique([questionId, userId]) } model QuestionsQuestionComment { - id String @id @default(cuid()) - questionId String - userId String? - content String @db.Text - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + questionId String + userId String? + content String @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) - votes QuestionsQuestionCommentVote[] + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + votes QuestionsQuestionCommentVote[] } model QuestionsQuestionCommentVote { - id String @id @default(cuid()) - questionCommentId String - userId String? - vote Vote - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + questionCommentId String + userId String? + vote Vote + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - comment QuestionsQuestionComment @relation(fields: [questionCommentId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + comment QuestionsQuestionComment @relation(fields: [questionCommentId], references: [id], onDelete: Cascade) - @@unique([questionCommentId, userId]) + @@unique([questionCommentId, userId]) } model QuestionsAnswer { - id String @id @default(cuid()) - questionId String - userId String? - content String @db.Text - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + questionId String + userId String? + content String @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) - votes QuestionsAnswerVote[] - comments QuestionsAnswerComment[] + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + votes QuestionsAnswerVote[] + comments QuestionsAnswerComment[] } model QuestionsAnswerVote { - id String @id @default(cuid()) - answerId String - userId String? - vote Vote - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + answerId String + userId String? + vote Vote + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - answer QuestionsAnswer @relation(fields: [answerId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + answer QuestionsAnswer @relation(fields: [answerId], references: [id], onDelete: Cascade) - @@unique([answerId, userId]) + @@unique([answerId, userId]) } model QuestionsAnswerComment { - id String @id @default(cuid()) - answerId String - userId String? - content String @db.Text - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + answerId String + userId String? + content String @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - answer QuestionsAnswer @relation(fields: [answerId], references: [id], onDelete: Cascade) - votes QuestionsAnswerCommentVote[] + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + answer QuestionsAnswer @relation(fields: [answerId], references: [id], onDelete: Cascade) + votes QuestionsAnswerCommentVote[] } model QuestionsAnswerCommentVote { - id String @id @default(cuid()) - answerCommentId String - userId String? - vote Vote - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + answerCommentId String + userId String? + vote Vote + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - comment QuestionsAnswerComment @relation(fields: [answerCommentId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + comment QuestionsAnswerComment @relation(fields: [answerCommentId], references: [id], onDelete: Cascade) - @@unique([answerCommentId, userId]) + @@unique([answerCommentId, userId]) } // End of Questions project models. diff --git a/apps/portal/prisma/seed.ts b/apps/portal/prisma/seed.ts index c31d6705..0e736d19 100644 --- a/apps/portal/prisma/seed.ts +++ b/apps/portal/prisma/seed.ts @@ -35,34 +35,6 @@ 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([ @@ -73,13 +45,6 @@ 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.'); } diff --git a/apps/portal/src/components/offers/constants.ts b/apps/portal/src/components/offers/constants.ts index e360a505..63a57d0e 100644 --- a/apps/portal/src/components/offers/constants.ts +++ b/apps/portal/src/components/offers/constants.ts @@ -5,43 +5,20 @@ export const emptyOption = '----'; // TODO: use enums export const titleOptions = [ { - label: 'Software engineer', - value: 'Software engineer', + label: 'Software Engineer', + value: 'Software Engineer', }, { - label: 'Frontend engineer', - value: 'Frontend engineer', + label: 'Frontend Engineer', + value: 'Frontend Engineer', }, { - label: 'Backend engineer', - value: 'Backend engineer', + label: 'Backend Engineer', + value: 'Backend Engineer', }, { - label: 'Full-stack engineer', - value: 'Full-stack engineer', - }, -]; - -export const companyOptions = [ - { - label: 'Amazon', - value: 'cl93patjt0000txewdi601mub', - }, - { - label: 'Microsoft', - value: 'cl93patjt0001txewkglfjsro', - }, - { - label: 'Apple', - value: 'cl93patjt0002txewf3ug54m8', - }, - { - label: 'Google', - value: 'cl93patjt0003txewyiaky7xx', - }, - { - label: 'Meta', - value: 'cl93patjt0004txew88wkcqpu', + label: 'Full-stack Engineer', + value: 'Full-stack Engineer', }, ]; @@ -86,26 +63,26 @@ export const internshipCycleOptions = [ export const yearOptions = [ { label: '2021', - value: '2021', + value: 2021, }, { label: '2022', - value: '2022', + value: 2022, }, { label: '2023', - value: '2023', + value: 2023, }, { label: '2024', - value: '2024', + value: 2024, }, ]; export const educationLevelOptions = Object.entries( EducationBackgroundType, -).map(([key, value]) => ({ - label: key, +).map(([, value]) => ({ + label: value, value, })); @@ -118,10 +95,18 @@ export const educationFieldOptions = [ label: 'Information Security', value: 'Information Security', }, + { + label: 'Information Systems', + value: 'Information Systems', + }, { label: 'Business Analytics', value: 'Business Analytics', }, + { + label: 'Data Science and Analytics', + value: 'Data Science and Analytics', + }, ]; export enum FieldError { @@ -129,3 +114,5 @@ export enum FieldError { Number = 'Please fill in a number in this field.', Required = 'Please fill in this field.', } + +export const OVERALL_TAB = 'Overall'; diff --git a/apps/portal/src/components/offers/forms/components/FormMonthYearPicker.tsx b/apps/portal/src/components/offers/forms/FormMonthYearPicker.tsx similarity index 91% rename from apps/portal/src/components/offers/forms/components/FormMonthYearPicker.tsx rename to apps/portal/src/components/offers/forms/FormMonthYearPicker.tsx index 2b6c414a..ca036b0e 100644 --- a/apps/portal/src/components/offers/forms/components/FormMonthYearPicker.tsx +++ b/apps/portal/src/components/offers/forms/FormMonthYearPicker.tsx @@ -4,7 +4,7 @@ import { useFormContext, useWatch } from 'react-hook-form'; import MonthYearPicker from '~/components/shared/MonthYearPicker'; -import { getCurrentMonth, getCurrentYear } from '../../../../utils/offers/time'; +import { getCurrentMonth, getCurrentYear } from '../../../utils/offers/time'; type MonthYearPickerProps = ComponentProps; diff --git a/apps/portal/src/components/offers/forms/components/FormRadioList.tsx b/apps/portal/src/components/offers/forms/FormRadioList.tsx similarity index 100% rename from apps/portal/src/components/offers/forms/components/FormRadioList.tsx rename to apps/portal/src/components/offers/forms/FormRadioList.tsx diff --git a/apps/portal/src/components/offers/forms/components/FormSelect.tsx b/apps/portal/src/components/offers/forms/FormSelect.tsx similarity index 100% rename from apps/portal/src/components/offers/forms/components/FormSelect.tsx rename to apps/portal/src/components/offers/forms/FormSelect.tsx diff --git a/apps/portal/src/components/offers/forms/components/FormTextArea.tsx b/apps/portal/src/components/offers/forms/FormTextArea.tsx similarity index 100% rename from apps/portal/src/components/offers/forms/components/FormTextArea.tsx rename to apps/portal/src/components/offers/forms/FormTextArea.tsx diff --git a/apps/portal/src/components/offers/forms/components/FormTextInput.tsx b/apps/portal/src/components/offers/forms/FormTextInput.tsx similarity index 100% rename from apps/portal/src/components/offers/forms/components/FormTextInput.tsx rename to apps/portal/src/components/offers/forms/FormTextInput.tsx diff --git a/apps/portal/src/components/offers/forms/OfferAnalysis.tsx b/apps/portal/src/components/offers/forms/OfferAnalysis.tsx deleted file mode 100644 index c3625d6d..00000000 --- a/apps/portal/src/components/offers/forms/OfferAnalysis.tsx +++ /dev/null @@ -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 ( -

- 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. -

- ); -} - -function OfferProfileCard() { - return ( -
-
-
- -
-
-

profile-name

-

Previous company: Meta, Singapore

-

YOE: 4 years

-
-
- - -
-
-

Software engineer

-

Company: Google, Singapore

-

Level: G4

-
-
-

Sept 2022

-

$125,000 / year

-
-
-
- ); -} - -function TopOfferProfileList() { - return ( - <> - - - - ); -} - -function OfferAnalysisContent() { - return ( - <> - - - - ); -} - -export default function OfferAnalysis() { - const [tab, setTab] = useState('Overall'); - - return ( -
-
- Result -
-
- - - -
-
- ); -} diff --git a/apps/portal/src/components/offers/forms/OfferProfileSave.tsx b/apps/portal/src/components/offers/offersSubmission/OfferProfileSave.tsx similarity index 67% rename from apps/portal/src/components/offers/forms/OfferProfileSave.tsx rename to apps/portal/src/components/offers/offersSubmission/OfferProfileSave.tsx index 866fa8e7..071da82a 100644 --- a/apps/portal/src/components/offers/forms/OfferProfileSave.tsx +++ b/apps/portal/src/components/offers/offersSubmission/OfferProfileSave.tsx @@ -1,13 +1,30 @@ +import { useRouter } from 'next/router'; import { useState } from 'react'; import { setTimeout } from 'timers'; import { CheckIcon, DocumentDuplicateIcon } from '@heroicons/react/20/solid'; import { BookmarkSquareIcon, EyeIcon } from '@heroicons/react/24/outline'; import { Button, TextInput } from '@tih/ui'; -export default function OfferProfileSave() { +import { + copyProfileLink, + getProfileLink, + getProfilePath, +} from '~/utils/offers/link'; + +type OfferProfileSaveProps = Readonly<{ + profileId: string; + token?: string; +}>; + +export default function OfferProfileSave({ + profileId, + token, +}: OfferProfileSaveProps) { const [linkCopied, setLinkCopied] = useState(false); const [isSaving, setSaving] = useState(false); const [isSaved, setSaved] = useState(false); + const router = useRouter(); + const saveProfile = () => { setSaving(true); setTimeout(() => { @@ -27,13 +44,13 @@ export default function OfferProfileSave() { To keep you offer profile strictly anonymous, only people who have the link below can edit it.

-
+
-
+
{linkCopied && (

Link copied to clipboard!

)} @@ -52,20 +71,26 @@ export default function OfferProfileSave() {

If you do not want to keep the edit link, you can opt to save this - profile under your user accont. It will still only be editable by you. + profile under your user account. It will still only be editable by + you.

-
diff --git a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx new file mode 100644 index 00000000..10f92618 --- /dev/null +++ b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx @@ -0,0 +1,243 @@ +import { useRef, useState } from 'react'; +import type { SubmitHandler } from 'react-hook-form'; +import { FormProvider, useForm } from 'react-hook-form'; +import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid'; +import { Button } from '@tih/ui'; + +import { Breadcrumbs } from '~/components/offers/Breadcrumb'; +import OfferAnalysis from '~/components/offers/offersSubmission/analysis/OfferAnalysis'; +import OfferProfileSave from '~/components/offers/offersSubmission/OfferProfileSave'; +import BackgroundForm from '~/components/offers/offersSubmission/submissionForm/BackgroundForm'; +import OfferDetailsForm from '~/components/offers/offersSubmission/submissionForm/OfferDetailsForm'; +import type { + OfferFormData, + OffersProfileFormData, +} from '~/components/offers/types'; +import { JobType } from '~/components/offers/types'; +import type { Month } from '~/components/shared/MonthYearPicker'; + +import { cleanObject, removeInvalidMoneyData } from '~/utils/offers/form'; +import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time'; +import { trpc } from '~/utils/trpc'; + +import type { CreateOfferProfileResponse } from '~/types/offers'; + +const defaultOfferValues = { + comments: '', + companyId: '', + jobType: JobType.FullTime, + location: '', + monthYearReceived: { + month: getCurrentMonth() as Month, + year: getCurrentYear(), + }, + negotiationStrategy: '', +}; + +export const defaultFullTimeOfferValues = { + ...defaultOfferValues, + jobType: JobType.FullTime, +}; + +export const defaultInternshipOfferValues = { + ...defaultOfferValues, + jobType: JobType.Intern, +}; + +const defaultOfferProfileValues = { + background: { + educations: [], + experiences: [{ jobType: JobType.FullTime }], + specificYoes: [], + totalYoe: 0, + }, + offers: [defaultOfferValues], +}; + +type FormStep = { + component: JSX.Element; + hasNext: boolean; + hasPrevious: boolean; + label: string; +}; + +type Props = Readonly<{ + initialOfferProfileValues?: OffersProfileFormData; + profileId?: string; + token?: string; +}>; + +export default function OffersSubmissionForm({ + initialOfferProfileValues = defaultOfferProfileValues, + profileId, + token, +}: Props) { + const [formStep, setFormStep] = useState(0); + const [createProfileResponse, setCreateProfileResponse] = + useState({ + id: profileId || '', + token: token || '', + }); + + const pageRef = useRef(null); + const scrollToTop = () => + pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 }); + const formMethods = useForm({ + defaultValues: initialOfferProfileValues, + mode: 'all', + }); + const { handleSubmit, trigger } = formMethods; + + const formSteps: Array = [ + { + component: , + hasNext: true, + hasPrevious: false, + label: 'Offer details', + }, + { + component: , + hasNext: false, + hasPrevious: true, + label: 'Background', + }, + { + component: , + hasNext: true, + hasPrevious: false, + label: 'Analysis', + }, + { + component: ( + + ), + hasNext: false, + hasPrevious: false, + label: 'Save', + }, + ]; + + const formStepsLabels = formSteps.map((step) => step.label); + + const nextStep = async (currStep: number) => { + if (currStep === 0) { + const result = await trigger('offers'); + if (!result) { + return; + } + } + setFormStep(formStep + 1); + scrollToTop(); + }; + + const previousStep = () => { + setFormStep(formStep - 1); + scrollToTop(); + }; + + const generateAnalysisMutation = trpc.useMutation( + ['offers.analysis.generate'], + { + onError(error) { + console.error(error.message); + }, + }, + ); + + const mutationpath = + profileId && token ? 'offers.profile.update' : 'offers.profile.create'; + + const createOrUpdateMutation = trpc.useMutation([mutationpath], { + onError(error) { + console.error(error.message); + }, + onSuccess(data) { + generateAnalysisMutation.mutate({ + profileId: data?.id || '', + }); + setCreateProfileResponse(data); + setFormStep(formStep + 1); + scrollToTop(); + }, + }); + + const onSubmit: SubmitHandler = async (data) => { + const result = await trigger(); + if (!result) { + return; + } + + data = removeInvalidMoneyData(data); + + const background = cleanObject(data.background); + background.specificYoes = data.background.specificYoes.filter( + (specificYoe) => specificYoe.domain && specificYoe.yoe > 0, + ); + if (Object.entries(background.experiences[0]).length === 1) { + background.experiences = []; + } + + const offers = data.offers.map((offer: OfferFormData) => ({ + ...offer, + monthYearReceived: new Date( + offer.monthYearReceived.year, + offer.monthYearReceived.month - 1, // Convert month to monthIndex + ), + })); + + if (profileId && token) { + createOrUpdateMutation.mutate({ + background, + id: profileId, + offers, + token, + }); + } else { + createOrUpdateMutation.mutate({ background, offers }); + } + }; + + return ( +
+
+
+
+ +
+ +
+ {formSteps[formStep].component} + {/*
{JSON.stringify(formMethods.watch(), null, 2)}
*/} + {formSteps[formStep].hasNext && ( +
+
+ )} + {formStep === 1 && ( +
+
+ )} +
+
+
+
+
+ ); +} diff --git a/apps/portal/src/components/offers/offersSubmission/analysis/OfferAnalysis.tsx b/apps/portal/src/components/offers/offersSubmission/analysis/OfferAnalysis.tsx new file mode 100644 index 00000000..4b45b3f2 --- /dev/null +++ b/apps/portal/src/components/offers/offersSubmission/analysis/OfferAnalysis.tsx @@ -0,0 +1,135 @@ +import { useEffect } from 'react'; +import { useState } from 'react'; +import { HorizontalDivider, Spinner, Tabs } from '@tih/ui'; + +import { trpc } from '~/utils/trpc'; + +import OfferPercentileAnalysis from './OfferPercentileAnalysis'; +import OfferProfileCard from './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 ( +

+ You are the first to submit an offer for these companies! Check back + later when there are more submissions. +

+ ); + } + return ( + <> + + {offerAnalysis.topPercentileOffers.map((topPercentileOffer) => ( + + ))} + + ); +} + +type OfferAnalysisProps = Readonly<{ + profileId?: string; +}>; + +export default function OfferAnalysis({ profileId }: OfferAnalysisProps) { + const [tab, setTab] = useState(OVERALL_TAB); + const [allAnalysis, setAllAnalysis] = useState(null); + const [analysis, setAnalysis] = useState(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 ( + analysis && ( +
+
+ Result +
+ {getAnalysisResult.isError && ( +

+ An error occurred while generating profile analysis. +

+ )} + {getAnalysisResult.isLoading && ( + + )} + {!getAnalysisResult.isError && !getAnalysisResult.isLoading && ( +
+ + + +
+ )} +
+ ) + ); +} diff --git a/apps/portal/src/components/offers/offersSubmission/analysis/OfferPercentileAnalysis.tsx b/apps/portal/src/components/offers/offersSubmission/analysis/OfferPercentileAnalysis.tsx new file mode 100644 index 00000000..1f4107ca --- /dev/null +++ b/apps/portal/src/components/offers/offersSubmission/analysis/OfferPercentileAnalysis.tsx @@ -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' ? ( +

+ 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. +

+ ) : ( +

+ 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. +

+ ); +} diff --git a/apps/portal/src/components/offers/offersSubmission/analysis/OfferProfileCard.tsx b/apps/portal/src/components/offers/offersSubmission/analysis/OfferProfileCard.tsx new file mode 100644 index 00000000..8d087a0c --- /dev/null +++ b/apps/portal/src/components/offers/offersSubmission/analysis/OfferProfileCard.tsx @@ -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 ( +
+
+
+ +
+
+

{profileName}

+

Previous company: {previousCompanies[0]}

+

YOE: {totalYoe} year(s)

+
+
+ + +
+
+

{title}

+

+ Company: {company.name}, {location} +

+

Level: {level}

+
+
+

{formatDate(monthYearReceived)}

+

+ {jobType === JobType.FullTime + ? `$${income} / year` + : `$${income} / month`} +

+
+
+
+ ); +} diff --git a/apps/portal/src/components/offers/forms/BackgroundForm.tsx b/apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx similarity index 65% rename from apps/portal/src/components/offers/forms/BackgroundForm.tsx rename to apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx index 61a1c3fe..534108b5 100644 --- a/apps/portal/src/components/offers/forms/BackgroundForm.tsx +++ b/apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx @@ -2,21 +2,28 @@ import { useFormContext, useWatch } from 'react-hook-form'; import { Collapsible, RadioList } from '@tih/ui'; import { - companyOptions, educationFieldOptions, educationLevelOptions, + emptyOption, + FieldError, locationOptions, titleOptions, } from '~/components/offers/constants'; -import FormRadioList from '~/components/offers/forms/components/FormRadioList'; -import FormSelect from '~/components/offers/forms/components/FormSelect'; -import FormTextInput from '~/components/offers/forms/components/FormTextInput'; +import type { BackgroundPostData } from '~/components/offers/types'; import { JobType } from '~/components/offers/types'; +import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; import { CURRENCY_OPTIONS } from '~/utils/offers/currency/CurrencyEnum'; +import FormRadioList from '../../forms/FormRadioList'; +import FormSelect from '../../forms/FormSelect'; +import FormTextInput from '../../forms/FormTextInput'; + function YoeSection() { - const { register } = useFormContext(); + const { register, formState } = useFormContext<{ + background: BackgroundPostData; + }>(); + const backgroundFields = formState.errors.background; return ( <>
@@ -26,53 +33,62 @@ function YoeSection() {
-
- -
- - -
-
- - -
-
-
+ +
+ + +
+
+ + +
+
); } function FullTimeJobFields() { - const { register } = useFormContext(); + const { register, setValue, formState } = useFormContext<{ + background: BackgroundPostData; + }>(); + const experiencesField = formState.errors.background?.experiences?.[0]; return ( <>
@@ -80,14 +96,16 @@ function FullTimeJobFields() { display="block" label="Title" options={titleOptions} + placeholder={emptyOption} {...register(`background.experiences.0.title`)} /> - +
+ + setValue(`background.experiences.0.companyId`, value) + } + /> +
} endAddOnType="element" + errorMessage={experiencesField?.totalCompensation?.value?.message} label="Total Compensation (Annual)" placeholder="0.00" startAddOn="$" startAddOnType="label" type="number" {...register(`background.experiences.0.totalCompensation.value`, { + min: { message: FieldError.NonNegativeNumber, value: 0 }, valueAsNumber: true, })} /> @@ -134,9 +154,11 @@ function FullTimeJobFields() { {...register(`background.experiences.0.location`)} /> @@ -147,7 +169,11 @@ function FullTimeJobFields() { } function InternshipJobFields() { - const { register } = useFormContext(); + const { register, setValue, formState } = useFormContext<{ + background: BackgroundPostData; + }>(); + const experiencesField = formState.errors.background?.experiences?.[0]; + return ( <>
@@ -155,14 +181,16 @@ function InternshipJobFields() { display="block" label="Title" options={titleOptions} + placeholder={emptyOption} {...register(`background.experiences.0.title`)} /> - +
+ + setValue(`background.experiences.0.companyId`, value) + } + /> +
} endAddOnType="element" + errorMessage={experiencesField?.monthlySalary?.value?.message} label="Salary (Monthly)" placeholder="0.00" startAddOn="$" startAddOnType="label" type="number" - {...register(`background.experiences.0.monthlySalary.value`)} + {...register(`background.experiences.0.monthlySalary.value`, { + min: { message: FieldError.NonNegativeNumber, value: 0 }, + valueAsNumber: true, + })} />
@@ -195,6 +227,7 @@ function InternshipJobFields() { display="block" label="Location" options={locationOptions} + placeholder={emptyOption} {...register(`background.experiences.0.location`)} />
@@ -231,7 +264,7 @@ function CurrentJobSection() {
@@ -258,12 +291,14 @@ function EducationSection() { display="block" label="Education Level" options={educationLevelOptions} + placeholder={emptyOption} {...register(`background.educations.0.type`)} /> @@ -287,9 +322,9 @@ export default function BackgroundForm() {
Help us better gauge your offers
-
- This section is optional, but your background information helps us - benchmark your offers. +
+ This section is mostly optional, but your background information helps + us benchmark your offers.
diff --git a/apps/portal/src/components/offers/forms/OfferDetailsForm.tsx b/apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx similarity index 68% rename from apps/portal/src/components/offers/forms/OfferDetailsForm.tsx rename to apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx index 9ff722fb..d380002c 100644 --- a/apps/portal/src/components/offers/forms/OfferDetailsForm.tsx +++ b/apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx @@ -1,5 +1,9 @@ import { useEffect, useState } from 'react'; -import type { FieldValues, UseFieldArrayReturn } from 'react-hook-form'; +import type { + FieldValues, + UseFieldArrayRemove, + UseFieldArrayReturn, +} from 'react-hook-form'; import { useWatch } from 'react-hook-form'; import { useFormContext } from 'react-hook-form'; import { useFieldArray } from 'react-hook-form'; @@ -7,54 +11,54 @@ import { PlusIcon } from '@heroicons/react/20/solid'; import { TrashIcon } from '@heroicons/react/24/outline'; import { Button, Dialog } from '@tih/ui'; +import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; + import { defaultFullTimeOfferValues, defaultInternshipOfferValues, -} from '~/pages/offers/submit'; - -import FormMonthYearPicker from './components/FormMonthYearPicker'; -import FormSelect from './components/FormSelect'; -import FormTextArea from './components/FormTextArea'; -import FormTextInput from './components/FormTextInput'; +} from '../OffersSubmissionForm'; import { - companyOptions, emptyOption, FieldError, internshipCycleOptions, locationOptions, titleOptions, yearOptions, -} from '../constants'; -import type { - FullTimeOfferDetailsFormData, - InternshipOfferDetailsFormData, -} from '../types'; -import { JobTypeLabel } from '../types'; -import { JobType } from '../types'; -import { CURRENCY_OPTIONS } from '../../../utils/offers/currency/CurrencyEnum'; +} from '../../constants'; +import FormMonthYearPicker from '../../forms/FormMonthYearPicker'; +import FormSelect from '../../forms/FormSelect'; +import FormTextArea from '../../forms/FormTextArea'; +import FormTextInput from '../../forms/FormTextInput'; +import type { OfferFormData } from '../../types'; +import { JobTypeLabel } from '../../types'; +import { JobType } from '../../types'; +import { CURRENCY_OPTIONS } from '../../../../utils/offers/currency/CurrencyEnum'; type FullTimeOfferDetailsFormProps = Readonly<{ index: number; - setDialogOpen: (isOpen: boolean) => void; + remove: UseFieldArrayRemove; }>; function FullTimeOfferDetailsForm({ index, - setDialogOpen, + remove, }: FullTimeOfferDetailsFormProps) { const { register, formState, setValue } = useFormContext<{ - offers: Array; + offers: Array; }>(); const offerFields = formState.errors.offers?.[index]; const watchCurrency = useWatch({ - name: `offers.${index}.job.totalCompensation.currency`, + name: `offers.${index}.offersFullTime.totalCompensation.currency`, }); useEffect(() => { - setValue(`offers.${index}.job.base.currency`, watchCurrency); - setValue(`offers.${index}.job.bonus.currency`, watchCurrency); - setValue(`offers.${index}.job.stocks.currency`, watchCurrency); + setValue( + `offers.${index}.offersFullTime.baseSalary.currency`, + watchCurrency, + ); + setValue(`offers.${index}.offersFullTime.bonus.currency`, watchCurrency); + setValue(`offers.${index}.offersFullTime.stocks.currency`, watchCurrency); }, [watchCurrency, index, setValue]); return ( @@ -62,48 +66,44 @@ function FullTimeOfferDetailsForm({
-
- +
+
+ + setValue(`offers.${index}.companyId`, value) + } + /> +
-
+
} endAddOnType="element" - errorMessage={offerFields?.job?.totalCompensation?.value?.message} + errorMessage={ + offerFields?.offersFullTime?.totalCompensation?.value?.message + } label="Total Compensation (Annual)" placeholder="0" required={true} startAddOn="$" startAddOnType="label" type="number" - {...register(`offers.${index}.job.totalCompensation.value`, { - min: { message: FieldError.NonNegativeNumber, value: 0 }, - required: FieldError.Required, - valueAsNumber: true, - })} + {...register( + `offers.${index}.offersFullTime.totalCompensation.value`, + { + min: { message: FieldError.NonNegativeNumber, value: 0 }, + required: FieldError.Required, + valueAsNumber: true, + }, + )} />
@@ -160,20 +168,23 @@ function FullTimeOfferDetailsForm({ isLabelHidden={true} label="Currency" options={CURRENCY_OPTIONS} - {...register(`offers.${index}.job.base.currency`, { - required: FieldError.Required, - })} + {...register( + `offers.${index}.offersFullTime.baseSalary.currency`, + { + required: FieldError.Required, + }, + )} /> } endAddOnType="element" - errorMessage={offerFields?.job?.base?.value?.message} + errorMessage={offerFields?.offersFullTime?.baseSalary?.value?.message} label="Base Salary (Annual)" placeholder="0" required={true} startAddOn="$" startAddOnType="label" type="number" - {...register(`offers.${index}.job.base.value`, { + {...register(`offers.${index}.offersFullTime.baseSalary.value`, { min: { message: FieldError.NonNegativeNumber, value: 0 }, required: FieldError.Required, valueAsNumber: true, @@ -186,20 +197,20 @@ function FullTimeOfferDetailsForm({ isLabelHidden={true} label="Currency" options={CURRENCY_OPTIONS} - {...register(`offers.${index}.job.bonus.currency`, { + {...register(`offers.${index}.offersFullTime.bonus.currency`, { required: FieldError.Required, })} /> } endAddOnType="element" - errorMessage={offerFields?.job?.bonus?.value?.message} + errorMessage={offerFields?.offersFullTime?.bonus?.value?.message} label="Bonus (Annual)" placeholder="0" required={true} startAddOn="$" startAddOnType="label" type="number" - {...register(`offers.${index}.job.bonus.value`, { + {...register(`offers.${index}.offersFullTime.bonus.value`, { min: { message: FieldError.NonNegativeNumber, value: 0 }, required: FieldError.Required, valueAsNumber: true, @@ -214,20 +225,20 @@ function FullTimeOfferDetailsForm({ isLabelHidden={true} label="Currency" options={CURRENCY_OPTIONS} - {...register(`offers.${index}.job.stocks.currency`, { + {...register(`offers.${index}.offersFullTime.stocks.currency`, { required: FieldError.Required, })} /> } endAddOnType="element" - errorMessage={offerFields?.job?.stocks?.value?.message} + errorMessage={offerFields?.offersFullTime?.stocks?.value?.message} label="Stocks (Annual)" placeholder="0" required={true} startAddOn="$" startAddOnType="label" type="number" - {...register(`offers.${index}.job.stocks.value`, { + {...register(`offers.${index}.offersFullTime.stocks.value`, { min: { message: FieldError.NonNegativeNumber, value: 0 }, required: FieldError.Required, valueAsNumber: true, @@ -254,7 +265,7 @@ function FullTimeOfferDetailsForm({ icon={TrashIcon} label="Delete" variant="secondary" - onClick={() => setDialogOpen(true)} + onClick={() => remove(index)} /> )}
@@ -264,15 +275,15 @@ function FullTimeOfferDetailsForm({ type InternshipOfferDetailsFormProps = Readonly<{ index: number; - setDialogOpen: (isOpen: boolean) => void; + remove: UseFieldArrayRemove; }>; function InternshipOfferDetailsForm({ index, - setDialogOpen, + remove, }: InternshipOfferDetailsFormProps) { - const { register, formState } = useFormContext<{ - offers: Array; + const { register, formState, setValue } = useFormContext<{ + offers: Array; }>(); const offerFields = formState.errors.offers?.[index]; @@ -282,39 +293,35 @@ function InternshipOfferDetailsForm({
- +
+ + setValue(`offers.${index}.companyId`, value) + } + /> +
@@ -369,20 +377,25 @@ function InternshipOfferDetailsForm({ isLabelHidden={true} label="Currency" options={CURRENCY_OPTIONS} - {...register(`offers.${index}.job.monthlySalary.currency`, { - required: FieldError.Required, - })} + {...register( + `offers.${index}.offersIntern.monthlySalary.currency`, + { + required: FieldError.Required, + }, + )} /> } endAddOnType="element" - errorMessage={offerFields?.job?.monthlySalary?.value?.message} + errorMessage={ + offerFields?.offersIntern?.monthlySalary?.value?.message + } label="Salary (Monthly)" placeholder="0" required={true} startAddOn="$" startAddOnType="label" type="number" - {...register(`offers.${index}.job.monthlySalary.value`, { + {...register(`offers.${index}.offersIntern.monthlySalary.value`, { min: { message: FieldError.NonNegativeNumber, value: 0 }, required: FieldError.Required, valueAsNumber: true, @@ -410,7 +423,7 @@ function InternshipOfferDetailsForm({ label="Delete" variant="secondary" onClick={() => { - setDialogOpen(true); + remove(index); }} /> )} @@ -429,7 +442,6 @@ function OfferDetailsFormArray({ jobType, }: OfferDetailsFormArrayProps) { const { append, remove, fields } = fieldArrayValues; - const [isDialogOpen, setDialogOpen] = useState(false); return (
@@ -437,44 +449,10 @@ function OfferDetailsFormArray({ return (
{jobType === JobType.FullTime ? ( - + ) : ( - + )} - { - remove(index); - setDialogOpen(false); - }} - /> - } - secondaryButton={ -
); })} @@ -501,22 +479,21 @@ export default function OfferDetailsForm() { const [isDialogOpen, setDialogOpen] = useState(false); const { control } = useFormContext(); const fieldArrayValues = useFieldArray({ control, name: 'offers' }); + const { append, remove } = fieldArrayValues; const toggleJobType = () => { - fieldArrayValues.remove(); + remove(); if (jobType === JobType.FullTime) { - setJobType(JobType.Internship); - fieldArrayValues.append(defaultInternshipOfferValues); + setJobType(JobType.Intern); + append(defaultInternshipOfferValues); } else { setJobType(JobType.FullTime); - fieldArrayValues.append(defaultFullTimeOfferValues); + append(defaultFullTimeOfferValues); } }; const switchJobTypeLabel = () => - jobType === JobType.FullTime - ? JobTypeLabel.INTERNSHIP - : JobTypeLabel.FULLTIME; + jobType === JobType.FullTime ? JobTypeLabel.INTERN : JobTypeLabel.FULLTIME; return (
@@ -541,11 +518,11 @@ export default function OfferDetailsForm() {
-

- Discussions feature coming soon -

- {/*