Merge branch 'main' into hongpo/add-question-text-match
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
60
apps/portal/prisma/migrations/20221014192315_/migration.sql
Normal file
@@ -0,0 +1,60 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "OffersAnalysis" (
|
||||
"id" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"offerId" TEXT NOT NULL,
|
||||
"overallPercentile" INTEGER NOT NULL,
|
||||
"noOfSimilarOffers" INTEGER NOT NULL,
|
||||
"companyPercentile" INTEGER NOT NULL,
|
||||
"noOfSimilarCompanyOffers" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "OffersAnalysis_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_TopOverallOffers" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_TopCompanyOffers" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OffersAnalysis_profileId_key" ON "OffersAnalysis"("profileId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OffersAnalysis_offerId_key" ON "OffersAnalysis"("offerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_TopOverallOffers_AB_unique" ON "_TopOverallOffers"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_TopOverallOffers_B_index" ON "_TopOverallOffers"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_TopCompanyOffers_AB_unique" ON "_TopCompanyOffers"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_TopCompanyOffers_B_index" ON "_TopCompanyOffers"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OffersAnalysis" ADD CONSTRAINT "OffersAnalysis_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "OffersProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OffersAnalysis" ADD CONSTRAINT "OffersAnalysis_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "OffersOffer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_TopOverallOffers" ADD CONSTRAINT "_TopOverallOffers_A_fkey" FOREIGN KEY ("A") REFERENCES "OffersAnalysis"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_TopOverallOffers" ADD CONSTRAINT "_TopOverallOffers_B_fkey" FOREIGN KEY ("B") REFERENCES "OffersOffer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_TopCompanyOffers" ADD CONSTRAINT "_TopCompanyOffers_A_fkey" FOREIGN KEY ("A") REFERENCES "OffersAnalysis"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_TopCompanyOffers" ADD CONSTRAINT "_TopCompanyOffers_B_fkey" FOREIGN KEY ("B") REFERENCES "OffersOffer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "OffersAnalysis" ALTER COLUMN "overallPercentile" SET DATA TYPE DOUBLE PRECISION,
|
||||
ALTER COLUMN "companyPercentile" SET DATA TYPE DOUBLE PRECISION;
|
||||
11
apps/portal/prisma/migrations/20221014211740_/migration.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "OffersAnalysis" DROP CONSTRAINT "OffersAnalysis_offerId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "OffersAnalysis" DROP CONSTRAINT "OffersAnalysis_profileId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OffersAnalysis" ADD CONSTRAINT "OffersAnalysis_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "OffersProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OffersAnalysis" ADD CONSTRAINT "OffersAnalysis_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "OffersOffer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Made the column `totalYoe` on table `OffersBackground` required. This step will fail if there are existing NULL values in that column.
|
||||
- Made the column `negotiationStrategy` on table `OffersOffer` required. This step will fail if there are existing NULL values in that column.
|
||||
- Made the column `comments` on table `OffersOffer` required. This step will fail if there are existing NULL values in that column.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "OffersBackground" ALTER COLUMN "totalYoe" SET NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "OffersOffer" ALTER COLUMN "negotiationStrategy" SET NOT NULL,
|
||||
ALTER COLUMN "comments" SET NOT NULL;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ResumesComment" ADD COLUMN "parentId" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ResumesComment" ADD CONSTRAINT "ResumesComment_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "ResumesComment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "OffersExperience" ADD COLUMN "location" TEXT;
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `upvotes` to the `QuestionsQuestion` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "QuestionsQuestion" ADD COLUMN "lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "upvotes" INTEGER NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "QuestionsQuestionEncounter" ADD COLUMN "netVotes" INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `netVotes` on the `QuestionsQuestionEncounter` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "QuestionsQuestion" ALTER COLUMN "lastSeenAt" DROP DEFAULT,
|
||||
ALTER COLUMN "upvotes" SET DEFAULT 0;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "QuestionsQuestionEncounter" DROP COLUMN "netVotes";
|
||||
@@ -0,0 +1,5 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "QuestionsQuestion_lastSeenAt_id_idx" ON "QuestionsQuestion"("lastSeenAt", "id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "QuestionsQuestion_upvotes_id_idx" ON "QuestionsQuestion"("upvotes", "id");
|
||||
12
apps/portal/prisma/migrations/20221021231817_/migration.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `baseValue` to the `OffersCurrency` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `updatedAt` to the `OffersCurrency` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "OffersCurrency" ADD COLUMN "baseCurrency" TEXT NOT NULL DEFAULT 'USD',
|
||||
ADD COLUMN "baseValue" INTEGER NOT NULL,
|
||||
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "OffersCurrency" ALTER COLUMN "value" SET DATA TYPE DOUBLE PRECISION,
|
||||
ALTER COLUMN "baseValue" SET DATA TYPE DOUBLE PRECISION;
|
||||
@@ -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)
|
||||
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])
|
||||
@@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
|
||||
|
||||
// Experience
|
||||
OffersExperienceTotalCompensation OffersExperience? @relation("ExperienceTotalCompensation")
|
||||
OffersExperienceMonthlySalary OffersExperience? @relation("ExperienceMonthlySalary")
|
||||
value Float
|
||||
currency String
|
||||
|
||||
// Full Time
|
||||
OffersTotalCompensation OffersFullTime? @relation("OfferTotalCompensation")
|
||||
OffersBaseSalary OffersFullTime? @relation("OfferBaseSalary")
|
||||
OffersBonus OffersFullTime? @relation("OfferBonus")
|
||||
OffersStocks OffersFullTime? @relation("OfferStocks")
|
||||
baseValue Float
|
||||
baseCurrency String @default("USD")
|
||||
|
||||
// Intern
|
||||
OffersMonthlySalary OffersIntern?
|
||||
// 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
|
||||
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
|
||||
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?
|
||||
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
|
||||
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
|
||||
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
||||
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.
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<typeof MonthYearPicker>;
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { UserCircleIcon } from '@heroicons/react/20/solid';
|
||||
import { HorizontalDivider, Tabs } from '@tih/ui';
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: 'Overall',
|
||||
value: 'overall',
|
||||
},
|
||||
{
|
||||
label: 'Shopee',
|
||||
value: 'company-id',
|
||||
},
|
||||
];
|
||||
|
||||
function OfferPercentileAnalysis() {
|
||||
const result = {
|
||||
company: 'Shopee',
|
||||
numberOfOffers: 105,
|
||||
percentile: 56,
|
||||
};
|
||||
|
||||
return (
|
||||
<p>
|
||||
Your highest offer is from {result.company}, which is {result.percentile}{' '}
|
||||
percentile out of {result.numberOfOffers} offers received in Singapore for
|
||||
the same job type, same level, and same YOE in the last year.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function OfferProfileCard() {
|
||||
return (
|
||||
<div className="my-5 block rounded-lg border p-4">
|
||||
<div className="grid grid-flow-col grid-cols-12 gap-x-10">
|
||||
<div className="col-span-1">
|
||||
<UserCircleIcon width={50} />
|
||||
</div>
|
||||
<div className="col-span-10">
|
||||
<p className="text-sm font-semibold">profile-name</p>
|
||||
<p className="text-xs ">Previous company: Meta, Singapore</p>
|
||||
<p className="text-xs ">YOE: 4 years</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HorizontalDivider />
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-x-10">
|
||||
<div className="col-span-1 row-span-3">
|
||||
<p className="text-sm font-semibold">Software engineer</p>
|
||||
<p className="text-xs ">Company: Google, Singapore</p>
|
||||
<p className="text-xs ">Level: G4</p>
|
||||
</div>
|
||||
<div className="col-span-1 row-span-3">
|
||||
<p className="text-end text-sm">Sept 2022</p>
|
||||
<p className="text-end text-xl">$125,000 / year</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TopOfferProfileList() {
|
||||
return (
|
||||
<>
|
||||
<OfferProfileCard />
|
||||
<OfferProfileCard />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function OfferAnalysisContent() {
|
||||
return (
|
||||
<>
|
||||
<OfferPercentileAnalysis />
|
||||
<TopOfferProfileList />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OfferAnalysis() {
|
||||
const [tab, setTab] = useState('Overall');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
|
||||
Result
|
||||
</h5>
|
||||
<div>
|
||||
<Tabs
|
||||
label="Result Navigation"
|
||||
tabs={tabs}
|
||||
value={tab}
|
||||
onChange={setTab}
|
||||
/>
|
||||
<HorizontalDivider className="mb-5" />
|
||||
<OfferAnalysisContent />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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.
|
||||
</p>
|
||||
<div className="mb-20 grid grid-cols-12 gap-4">
|
||||
<div className="mb-5 grid grid-cols-12 gap-4">
|
||||
<div className="col-span-11">
|
||||
<TextInput
|
||||
disabled={true}
|
||||
isLabelHidden={true}
|
||||
label="Edit link"
|
||||
value="link.myprofile-auto-generate..."
|
||||
value={getProfileLink(profileId, token)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
@@ -41,10 +58,12 @@ export default function OfferProfileSave() {
|
||||
isLabelHidden={true}
|
||||
label="Copy"
|
||||
variant="primary"
|
||||
onClick={() => setLinkCopied(true)}
|
||||
onClick={() => {
|
||||
copyProfileLink(profileId, token), setLinkCopied(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<div className="mb-20">
|
||||
{linkCopied && (
|
||||
<p className="text-purple-700">Link copied to clipboard!</p>
|
||||
)}
|
||||
@@ -52,20 +71,26 @@ export default function OfferProfileSave() {
|
||||
|
||||
<p className="mb-5 text-gray-900">
|
||||
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.
|
||||
</p>
|
||||
<div className="mb-20">
|
||||
<Button
|
||||
disabled={isSaved}
|
||||
icon={isSaved ? CheckIcon : BookmarkSquareIcon}
|
||||
isLoading={isSaving}
|
||||
label="Save to user profile"
|
||||
label={isSaved ? 'Saved to user profile' : 'Save to user profile'}
|
||||
variant="primary"
|
||||
onClick={saveProfile}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-10">
|
||||
<Button icon={EyeIcon} label="View your profile" variant="special" />
|
||||
<Button
|
||||
icon={EyeIcon}
|
||||
label="View your profile"
|
||||
variant="special"
|
||||
onClick={() => router.push(getProfilePath(profileId, token))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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<CreateOfferProfileResponse>({
|
||||
id: profileId || '',
|
||||
token: token || '',
|
||||
});
|
||||
|
||||
const pageRef = useRef<HTMLDivElement>(null);
|
||||
const scrollToTop = () =>
|
||||
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
|
||||
const formMethods = useForm<OffersProfileFormData>({
|
||||
defaultValues: initialOfferProfileValues,
|
||||
mode: 'all',
|
||||
});
|
||||
const { handleSubmit, trigger } = formMethods;
|
||||
|
||||
const formSteps: Array<FormStep> = [
|
||||
{
|
||||
component: <OfferDetailsForm key={0} />,
|
||||
hasNext: true,
|
||||
hasPrevious: false,
|
||||
label: 'Offer details',
|
||||
},
|
||||
{
|
||||
component: <BackgroundForm key={1} />,
|
||||
hasNext: false,
|
||||
hasPrevious: true,
|
||||
label: 'Background',
|
||||
},
|
||||
{
|
||||
component: <OfferAnalysis key={2} profileId={createProfileResponse.id} />,
|
||||
hasNext: true,
|
||||
hasPrevious: false,
|
||||
label: 'Analysis',
|
||||
},
|
||||
{
|
||||
component: (
|
||||
<OfferProfileSave
|
||||
key={3}
|
||||
profileId={createProfileResponse.id || ''}
|
||||
token={createProfileResponse.token}
|
||||
/>
|
||||
),
|
||||
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<OffersProfileFormData> = 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 (
|
||||
<div ref={pageRef} className="fixed h-full w-full overflow-y-scroll">
|
||||
<div className="mb-20 flex justify-center">
|
||||
<div className="my-5 block w-full max-w-screen-md rounded-lg bg-white py-10 px-10 shadow-lg">
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Breadcrumbs currentStep={formStep} stepLabels={formStepsLabels} />
|
||||
</div>
|
||||
<FormProvider {...formMethods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{formSteps[formStep].component}
|
||||
{/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */}
|
||||
{formSteps[formStep].hasNext && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
disabled={false}
|
||||
icon={ArrowRightIcon}
|
||||
label="Next"
|
||||
variant="secondary"
|
||||
onClick={() => nextStep(formStep)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{formStep === 1 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
icon={ArrowLeftIcon}
|
||||
label="Previous"
|
||||
variant="secondary"
|
||||
onClick={previousStep}
|
||||
/>
|
||||
<Button label="Submit" type="submit" variant="primary" />{' '}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<p className="m-10">
|
||||
You are the first to submit an offer for these companies! Check back
|
||||
later when there are more submissions.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<OfferPercentileAnalysis
|
||||
companyName={offer.company.name}
|
||||
offerAnalysis={offerAnalysis}
|
||||
tab={tab}
|
||||
/>
|
||||
{offerAnalysis.topPercentileOffers.map((topPercentileOffer) => (
|
||||
<OfferProfileCard
|
||||
key={topPercentileOffer.id}
|
||||
offerProfile={topPercentileOffer}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type OfferAnalysisProps = Readonly<{
|
||||
profileId?: string;
|
||||
}>;
|
||||
|
||||
export default function OfferAnalysis({ profileId }: OfferAnalysisProps) {
|
||||
const [tab, setTab] = useState(OVERALL_TAB);
|
||||
const [allAnalysis, setAllAnalysis] = useState<ProfileAnalysis | null>(null);
|
||||
const [analysis, setAnalysis] = useState<OfferAnalysisData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === OVERALL_TAB) {
|
||||
setAnalysis({
|
||||
offer: allAnalysis?.overallHighestOffer,
|
||||
offerAnalysis: allAnalysis?.overallAnalysis,
|
||||
});
|
||||
} else {
|
||||
setAnalysis({
|
||||
offer: allAnalysis?.overallHighestOffer,
|
||||
offerAnalysis: allAnalysis?.companyAnalysis[0],
|
||||
});
|
||||
}
|
||||
}, [tab, allAnalysis]);
|
||||
|
||||
if (!profileId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getAnalysisResult = trpc.useQuery(
|
||||
['offers.analysis.get', { profileId }],
|
||||
{
|
||||
onError(error) {
|
||||
console.error(error.message);
|
||||
},
|
||||
onSuccess(data) {
|
||||
setAllAnalysis(data);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const tabOptions = [
|
||||
{
|
||||
label: OVERALL_TAB,
|
||||
value: OVERALL_TAB,
|
||||
},
|
||||
{
|
||||
label: allAnalysis?.overallHighestOffer.company.name || '',
|
||||
value: allAnalysis?.overallHighestOffer.company.id || '',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
analysis && (
|
||||
<div>
|
||||
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
|
||||
Result
|
||||
</h5>
|
||||
{getAnalysisResult.isError && (
|
||||
<p className="m-10 text-center">
|
||||
An error occurred while generating profile analysis.
|
||||
</p>
|
||||
)}
|
||||
{getAnalysisResult.isLoading && (
|
||||
<Spinner className="m-10" display="block" size="lg" />
|
||||
)}
|
||||
{!getAnalysisResult.isError && !getAnalysisResult.isLoading && (
|
||||
<div>
|
||||
<Tabs
|
||||
label="Result Navigation"
|
||||
tabs={tabOptions}
|
||||
value={tab}
|
||||
onChange={setTab}
|
||||
/>
|
||||
<HorizontalDivider className="mb-5" />
|
||||
<OfferAnalysisContent analysis={analysis} tab={tab} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Analysis } from '~/types/offers';
|
||||
|
||||
type OfferPercentileAnalysisProps = Readonly<{
|
||||
companyName: string;
|
||||
offerAnalysis: Analysis;
|
||||
tab: string;
|
||||
}>;
|
||||
|
||||
export default function OfferPercentileAnalysis({
|
||||
tab,
|
||||
companyName,
|
||||
offerAnalysis: { noOfOffers, percentile },
|
||||
}: OfferPercentileAnalysisProps) {
|
||||
return tab === 'Overall' ? (
|
||||
<p>
|
||||
Your highest offer is from {companyName}, which is {percentile} percentile
|
||||
out of {noOfOffers} offers received for the same job type, same level, and
|
||||
same YOE(+/-1) in the last year.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Your offer from {companyName} is {percentile} percentile out of{' '}
|
||||
{noOfOffers} offers received in {companyName} for the same job type, same
|
||||
level, and same YOE(+/-1) in the last year.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { UserCircleIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
import { HorizontalDivider } from '~/../../../packages/ui/dist';
|
||||
import { formatDate } from '~/utils/offers/time';
|
||||
|
||||
import { JobType } from '../../types';
|
||||
|
||||
import type { AnalysisOffer } from '~/types/offers';
|
||||
|
||||
type OfferProfileCardProps = Readonly<{
|
||||
offerProfile: AnalysisOffer;
|
||||
}>;
|
||||
|
||||
export default function OfferProfileCard({
|
||||
offerProfile: {
|
||||
company,
|
||||
income,
|
||||
profileName,
|
||||
totalYoe,
|
||||
level,
|
||||
monthYearReceived,
|
||||
jobType,
|
||||
location,
|
||||
title,
|
||||
previousCompanies,
|
||||
},
|
||||
}: OfferProfileCardProps) {
|
||||
return (
|
||||
<div className="my-5 block rounded-lg border p-4">
|
||||
<div className="grid grid-flow-col grid-cols-12 gap-x-10">
|
||||
<div className="col-span-1">
|
||||
<UserCircleIcon width={50} />
|
||||
</div>
|
||||
<div className="col-span-10">
|
||||
<p className="text-sm font-semibold">{profileName}</p>
|
||||
<p className="text-xs ">Previous company: {previousCompanies[0]}</p>
|
||||
<p className="text-xs ">YOE: {totalYoe} year(s)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HorizontalDivider />
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-x-10">
|
||||
<div className="col-span-1 row-span-3">
|
||||
<p className="text-sm font-semibold">{title}</p>
|
||||
<p className="text-xs ">
|
||||
Company: {company.name}, {location}
|
||||
</p>
|
||||
<p className="text-xs ">Level: {level}</p>
|
||||
</div>
|
||||
<div className="col-span-1 row-span-3">
|
||||
<p className="text-end text-sm">{formatDate(monthYearReceived)}</p>
|
||||
<p className="text-end text-xl">
|
||||
{jobType === JobType.FullTime
|
||||
? `$${income} / year`
|
||||
: `$${income} / month`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<h6 className="mb-2 text-left text-xl font-medium text-gray-400">
|
||||
@@ -26,53 +33,62 @@ function YoeSection() {
|
||||
<div className="mb-5 rounded-lg border border-gray-200 px-10 py-5">
|
||||
<div className="mb-2 grid grid-cols-3 space-x-3">
|
||||
<FormTextInput
|
||||
errorMessage={backgroundFields?.totalYoe?.message}
|
||||
label="Total YOE"
|
||||
placeholder="0"
|
||||
required={true}
|
||||
type="number"
|
||||
{...register(`background.totalYoe`, {
|
||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
||||
required: FieldError.Required,
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 space-x-3">
|
||||
<Collapsible label="Add specific YOEs by domain">
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormTextInput
|
||||
label="Specific YOE 1"
|
||||
type="number"
|
||||
{...register(`background.specificYoes.0.yoe`, {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<FormTextInput
|
||||
label="Specific Domain 1"
|
||||
placeholder="e.g. Frontend"
|
||||
{...register(`background.specificYoes.0.domain`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormTextInput
|
||||
label="Specific YOE 2"
|
||||
type="number"
|
||||
{...register(`background.specificYoes.1.yoe`, {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<FormTextInput
|
||||
label="Specific Domain 2"
|
||||
placeholder="e.g. Backend"
|
||||
{...register(`background.specificYoes.1.domain`)}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
<Collapsible label="Add specific YOEs by domain">
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormTextInput
|
||||
errorMessage={backgroundFields?.specificYoes?.[0]?.yoe?.message}
|
||||
label="Specific YOE 1"
|
||||
type="number"
|
||||
{...register(`background.specificYoes.0.yoe`, {
|
||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<FormTextInput
|
||||
label="Specific Domain 1"
|
||||
placeholder="e.g. Frontend"
|
||||
{...register(`background.specificYoes.0.domain`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormTextInput
|
||||
errorMessage={backgroundFields?.specificYoes?.[1]?.yoe?.message}
|
||||
label="Specific YOE 2"
|
||||
type="number"
|
||||
{...register(`background.specificYoes.1.yoe`, {
|
||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<FormTextInput
|
||||
label="Specific Domain 2"
|
||||
placeholder="e.g. Backend"
|
||||
{...register(`background.specificYoes.1.domain`)}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FullTimeJobFields() {
|
||||
const { register } = useFormContext();
|
||||
const { register, setValue, formState } = useFormContext<{
|
||||
background: BackgroundPostData;
|
||||
}>();
|
||||
const experiencesField = formState.errors.background?.experiences?.[0];
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
@@ -80,14 +96,16 @@ function FullTimeJobFields() {
|
||||
display="block"
|
||||
label="Title"
|
||||
options={titleOptions}
|
||||
placeholder={emptyOption}
|
||||
{...register(`background.experiences.0.title`)}
|
||||
/>
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Company"
|
||||
options={companyOptions}
|
||||
{...register(`background.experiences.0.companyId`)}
|
||||
/>
|
||||
<div>
|
||||
<CompaniesTypeahead
|
||||
onSelect={({ value }) =>
|
||||
setValue(`background.experiences.0.companyId`, value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-1 space-x-3">
|
||||
<FormTextInput
|
||||
@@ -103,12 +121,14 @@ function FullTimeJobFields() {
|
||||
/>
|
||||
}
|
||||
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`)}
|
||||
/>
|
||||
<FormTextInput
|
||||
errorMessage={experiencesField?.durationInMonths?.message}
|
||||
label="Duration (months)"
|
||||
type="number"
|
||||
{...register(`background.experiences.0.durationInMonths`, {
|
||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
@@ -155,14 +181,16 @@ function InternshipJobFields() {
|
||||
display="block"
|
||||
label="Title"
|
||||
options={titleOptions}
|
||||
placeholder={emptyOption}
|
||||
{...register(`background.experiences.0.title`)}
|
||||
/>
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Company"
|
||||
options={companyOptions}
|
||||
{...register(`background.experiences.0.company`)}
|
||||
/>
|
||||
<div>
|
||||
<CompaniesTypeahead
|
||||
onSelect={({ value }) =>
|
||||
setValue(`background.experiences.0.companyId`, value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-1 space-x-3">
|
||||
<FormTextInput
|
||||
@@ -176,12 +204,16 @@ function InternshipJobFields() {
|
||||
/>
|
||||
}
|
||||
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,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<Collapsible label="Add more details">
|
||||
@@ -195,6 +227,7 @@ function InternshipJobFields() {
|
||||
display="block"
|
||||
label="Location"
|
||||
options={locationOptions}
|
||||
placeholder={emptyOption}
|
||||
{...register(`background.experiences.0.location`)}
|
||||
/>
|
||||
</div>
|
||||
@@ -231,7 +264,7 @@ function CurrentJobSection() {
|
||||
<RadioList.Item
|
||||
key="Internship"
|
||||
label="Internship"
|
||||
value={JobType.Internship}
|
||||
value={JobType.Intern}
|
||||
/>
|
||||
</FormRadioList>
|
||||
</div>
|
||||
@@ -258,12 +291,14 @@ function EducationSection() {
|
||||
display="block"
|
||||
label="Education Level"
|
||||
options={educationLevelOptions}
|
||||
placeholder={emptyOption}
|
||||
{...register(`background.educations.0.type`)}
|
||||
/>
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Field"
|
||||
options={educationFieldOptions}
|
||||
placeholder={emptyOption}
|
||||
{...register(`background.educations.0.field`)}
|
||||
/>
|
||||
</div>
|
||||
@@ -287,9 +322,9 @@ export default function BackgroundForm() {
|
||||
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
|
||||
Help us better gauge your offers
|
||||
</h5>
|
||||
<h6 className="mx-10 mb-8 text-center text-lg font-light text-gray-600">
|
||||
This section is optional, but your background information helps us
|
||||
benchmark your offers.
|
||||
<h6 className="text-md mx-10 mb-8 text-center font-light text-gray-600">
|
||||
This section is mostly optional, but your background information helps
|
||||
us benchmark your offers.
|
||||
</h6>
|
||||
<div>
|
||||
<YoeSection />
|
||||
@@ -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<FullTimeOfferDetailsFormData>;
|
||||
offers: Array<OfferFormData>;
|
||||
}>();
|
||||
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({
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.job?.title?.message}
|
||||
errorMessage={offerFields?.offersFullTime?.title?.message}
|
||||
label="Title"
|
||||
options={titleOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.title`, {
|
||||
{...register(`offers.${index}.offersFullTime.title`, {
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
<FormTextInput
|
||||
errorMessage={offerFields?.job?.specialization?.message}
|
||||
errorMessage={offerFields?.offersFullTime?.specialization?.message}
|
||||
label="Focus / Specialization"
|
||||
placeholder="e.g. Front End"
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.specialization`, {
|
||||
{...register(`offers.${index}.offersFullTime.specialization`, {
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.companyId?.message}
|
||||
label="Company"
|
||||
options={companyOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.companyId`, {
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
<div className="mb-5 flex grid grid-cols-2 space-x-3">
|
||||
<div>
|
||||
<CompaniesTypeahead
|
||||
onSelect={({ value }) =>
|
||||
setValue(`offers.${index}.companyId`, value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<FormTextInput
|
||||
errorMessage={offerFields?.job?.level?.message}
|
||||
errorMessage={offerFields?.offersFullTime?.level?.message}
|
||||
label="Level"
|
||||
placeholder="e.g. L4, Junior"
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.level`, {
|
||||
{...register(`offers.${index}.offersFullTime.level`, {
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<div className="mb-5 flex grid grid-cols-2 items-start space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.location?.message}
|
||||
@@ -132,24 +132,32 @@ function FullTimeOfferDetailsForm({
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`offers.${index}.job.totalCompensation.currency`, {
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
{...register(
|
||||
`offers.${index}.offersFullTime.totalCompensation.currency`,
|
||||
{
|
||||
required: FieldError.Required,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
}
|
||||
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,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
@@ -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)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -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<InternshipOfferDetailsFormData>;
|
||||
const { register, formState, setValue } = useFormContext<{
|
||||
offers: Array<OfferFormData>;
|
||||
}>();
|
||||
|
||||
const offerFields = formState.errors.offers?.[index];
|
||||
@@ -282,39 +293,35 @@ function InternshipOfferDetailsForm({
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.job?.title?.message}
|
||||
errorMessage={offerFields?.offersIntern?.title?.message}
|
||||
label="Title"
|
||||
options={titleOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.title`, {
|
||||
{...register(`offers.${index}.offersIntern.title`, {
|
||||
minLength: 1,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
<FormTextInput
|
||||
errorMessage={offerFields?.job?.specialization?.message}
|
||||
errorMessage={offerFields?.offersIntern?.specialization?.message}
|
||||
label="Focus / Specialization"
|
||||
placeholder="e.g. Front End"
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.specialization`, {
|
||||
{...register(`offers.${index}.offersIntern.specialization`, {
|
||||
minLength: 1,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.companyId?.message}
|
||||
label="Company"
|
||||
options={companyOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.companyId`, {
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
<div>
|
||||
<CompaniesTypeahead
|
||||
onSelect={({ value }) =>
|
||||
setValue(`offers.${index}.companyId`, value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.location?.message}
|
||||
@@ -330,24 +337,25 @@ function InternshipOfferDetailsForm({
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.job?.internshipCycle?.message}
|
||||
errorMessage={offerFields?.offersIntern?.internshipCycle?.message}
|
||||
label="Internship Cycle"
|
||||
options={internshipCycleOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.internshipCycle`, {
|
||||
{...register(`offers.${index}.offersIntern.internshipCycle`, {
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.job?.startYear?.message}
|
||||
errorMessage={offerFields?.offersIntern?.startYear?.message}
|
||||
label="Internship Year"
|
||||
options={yearOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.startYear`, {
|
||||
{...register(`offers.${index}.offersIntern.startYear`, {
|
||||
required: FieldError.Required,
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
@@ -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 (
|
||||
<div>
|
||||
@@ -437,44 +449,10 @@ function OfferDetailsFormArray({
|
||||
return (
|
||||
<div key={item.id}>
|
||||
{jobType === JobType.FullTime ? (
|
||||
<FullTimeOfferDetailsForm
|
||||
index={index}
|
||||
setDialogOpen={setDialogOpen}
|
||||
/>
|
||||
<FullTimeOfferDetailsForm index={index} remove={remove} />
|
||||
) : (
|
||||
<InternshipOfferDetailsForm
|
||||
index={index}
|
||||
setDialogOpen={setDialogOpen}
|
||||
/>
|
||||
<InternshipOfferDetailsForm index={index} remove={remove} />
|
||||
)}
|
||||
<Dialog
|
||||
isShown={isDialogOpen}
|
||||
primaryButton={
|
||||
<Button
|
||||
display="block"
|
||||
label="OK"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
setDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
secondaryButton={
|
||||
<Button
|
||||
display="block"
|
||||
label="Cancel"
|
||||
variant="tertiary"
|
||||
onClick={() => setDialogOpen(false)}
|
||||
/>
|
||||
}
|
||||
title="Remove this offer"
|
||||
onClose={() => setDialogOpen(false)}>
|
||||
<p>
|
||||
Are you sure you want to remove this offer? This action cannot
|
||||
be reversed.
|
||||
</p>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -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 (
|
||||
<div className="mb-5">
|
||||
@@ -541,11 +518,11 @@ export default function OfferDetailsForm() {
|
||||
<div className="mx-5 w-1/3">
|
||||
<Button
|
||||
display="block"
|
||||
label={JobTypeLabel.INTERNSHIP}
|
||||
label={JobTypeLabel.INTERN}
|
||||
size="md"
|
||||
variant={jobType === JobType.Internship ? 'secondary' : 'tertiary'}
|
||||
variant={jobType === JobType.Intern ? 'secondary' : 'tertiary'}
|
||||
onClick={() => {
|
||||
if (jobType === JobType.Internship) {
|
||||
if (jobType === JobType.Intern) {
|
||||
return;
|
||||
}
|
||||
setDialogOpen(true);
|
||||
@@ -1,21 +1,90 @@
|
||||
import { signIn, useSession } from 'next-auth/react';
|
||||
import { useState } from 'react';
|
||||
import { ClipboardDocumentIcon, ShareIcon } from '@heroicons/react/24/outline';
|
||||
import { Button, Spinner } from '@tih/ui';
|
||||
import { Button, HorizontalDivider, Spinner, TextArea } from '@tih/ui';
|
||||
|
||||
import ExpandableCommentCard from '~/components/offers/profile/comments/ExpandableCommentCard';
|
||||
|
||||
import { copyProfileLink } from '~/utils/offers/link';
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import type { OffersDiscussion, Reply } from '~/types/offers';
|
||||
|
||||
type ProfileHeaderProps = Readonly<{
|
||||
handleCopyEditLink: () => void;
|
||||
handleCopyPublicLink: () => void;
|
||||
isDisabled: boolean;
|
||||
isEditable: boolean;
|
||||
isLoading: boolean;
|
||||
profileId: string;
|
||||
profileName?: string;
|
||||
token?: string;
|
||||
}>;
|
||||
|
||||
export default function ProfileComments({
|
||||
handleCopyEditLink,
|
||||
handleCopyPublicLink,
|
||||
isDisabled,
|
||||
isEditable,
|
||||
isLoading,
|
||||
profileId,
|
||||
profileName,
|
||||
token,
|
||||
}: ProfileHeaderProps) {
|
||||
const { data: session, status } = useSession();
|
||||
const [currentReply, setCurrentReply] = useState<string>('');
|
||||
const [replies, setReplies] = useState<Array<Reply>>();
|
||||
|
||||
const commentsQuery = trpc.useQuery(
|
||||
['offers.comments.getComments', { profileId }],
|
||||
{
|
||||
onSuccess(response: OffersDiscussion) {
|
||||
setReplies(response.data);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const trpcContext = trpc.useContext();
|
||||
const createCommentMutation = trpc.useMutation(['offers.comments.create'], {
|
||||
onSuccess() {
|
||||
trpcContext.invalidateQueries([
|
||||
'offers.comments.getComments',
|
||||
{ profileId },
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
function handleComment(message: string) {
|
||||
if (isEditable) {
|
||||
// If it is with edit permission, send comment to API with username = null
|
||||
createCommentMutation.mutate(
|
||||
{
|
||||
message,
|
||||
profileId,
|
||||
token,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setCurrentReply('');
|
||||
},
|
||||
},
|
||||
);
|
||||
} else if (status === 'authenticated') {
|
||||
// If not the OP and logged in, send comment to API
|
||||
createCommentMutation.mutate(
|
||||
{
|
||||
message,
|
||||
profileId,
|
||||
userId: session.user?.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setCurrentReply('');
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// If not the OP and not logged in, direct users to log in
|
||||
signIn();
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="col-span-10 pt-4">
|
||||
@@ -24,7 +93,7 @@ export default function ProfileComments({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="m-4">
|
||||
<div className="m-4 h-full">
|
||||
<div className="flex-end flex justify-end space-x-4">
|
||||
{isEditable && (
|
||||
<Button
|
||||
@@ -35,7 +104,7 @@ export default function ProfileComments({
|
||||
label="Copy profile edit link"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={handleCopyEditLink}
|
||||
onClick={() => copyProfileLink(profileId, token)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
@@ -46,13 +115,47 @@ export default function ProfileComments({
|
||||
label="Copy public link"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={handleCopyPublicLink}
|
||||
onClick={() => copyProfileLink(profileId)}
|
||||
/>
|
||||
</div>
|
||||
<h2 className="mt-2 text-2xl font-bold">
|
||||
Discussions feature coming soon
|
||||
</h2>
|
||||
{/* <TextArea label="Comment" placeholder="Type your comment here" /> */}
|
||||
<h2 className="mt-2 mb-6 text-2xl font-bold">Discussions</h2>
|
||||
<div>
|
||||
<TextArea
|
||||
label={`Comment as ${
|
||||
isEditable ? profileName : session?.user?.name ?? 'anonymous'
|
||||
}`}
|
||||
placeholder="Type your comment here"
|
||||
value={currentReply}
|
||||
onChange={(value) => setCurrentReply(value)}
|
||||
/>
|
||||
<div className="mt-2 flex w-full justify-end">
|
||||
<div className="w-fit">
|
||||
<Button
|
||||
disabled={commentsQuery.isLoading}
|
||||
display="block"
|
||||
isLabelHidden={false}
|
||||
isLoading={createCommentMutation.isLoading}
|
||||
label="Comment"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onClick={() => handleComment(currentReply)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HorizontalDivider />
|
||||
</div>
|
||||
<div className="h-full overflow-y-scroll">
|
||||
<div className="h-content mb-96 w-full">
|
||||
{replies?.map((reply: Reply) => (
|
||||
<ExpandableCommentCard
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
profileId={profileId}
|
||||
token={isEditable ? token : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,10 +27,10 @@ export default function ProfileDetails({
|
||||
);
|
||||
}
|
||||
if (selectedTab === 'offers') {
|
||||
if (offers && offers.length !== 0) {
|
||||
if (offers.length !== 0) {
|
||||
return (
|
||||
<>
|
||||
{[...offers].map((offer) => (
|
||||
{offers.map((offer) => (
|
||||
<OfferCard key={offer.id} offer={offer} />
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
BookmarkSquareIcon,
|
||||
@@ -11,6 +12,8 @@ import { Button, Dialog, Spinner, Tabs } from '@tih/ui';
|
||||
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
|
||||
import type { BackgroundCard } from '~/components/offers/types';
|
||||
|
||||
import { getProfileEditPath } from '~/utils/offers/link';
|
||||
|
||||
type ProfileHeaderProps = Readonly<{
|
||||
background?: BackgroundCard;
|
||||
handleDelete: () => void;
|
||||
@@ -29,6 +32,12 @@ export default function ProfileHeader({
|
||||
setSelectedTab,
|
||||
}: ProfileHeaderProps) {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const { offerProfileId = '', token = '' } = router.query;
|
||||
|
||||
const handleEditClick = () => {
|
||||
router.push(getProfileEditPath(offerProfileId as string, token as string));
|
||||
};
|
||||
|
||||
function renderActionList() {
|
||||
return (
|
||||
@@ -48,6 +57,7 @@ export default function ProfileHeader({
|
||||
label="Edit"
|
||||
size="md"
|
||||
variant="tertiary"
|
||||
onClick={handleEditClick}
|
||||
/>
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
@@ -119,9 +129,11 @@ export default function ProfileHeader({
|
||||
<div className="flex flex-row">
|
||||
<BuildingOffice2Icon className="mr-2.5 h-5" />
|
||||
<span className="mr-2 font-bold">Current:</span>
|
||||
<span>{`${background?.experiences[0].companyName ?? '-'} ${
|
||||
background?.experiences[0].jobLevel
|
||||
} ${background?.experiences[0].jobTitle}`}</span>
|
||||
<span>
|
||||
{`${background?.experiences[0]?.companyName ?? '-'} ${
|
||||
background?.experiences[0]?.jobLevel || ''
|
||||
} ${background?.experiences[0]?.jobTitle || ''}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<CalendarDaysIcon className="mr-2.5 h-5" />
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { signIn, useSession } from 'next-auth/react';
|
||||
import { useState } from 'react';
|
||||
import { ChatBubbleBottomCenterIcon } from '@heroicons/react/24/outline';
|
||||
import { Button, HorizontalDivider, TextArea } from '@tih/ui';
|
||||
|
||||
import { timeSinceNow } from '~/utils/offers/time';
|
||||
|
||||
import { trpc } from '../../../../utils/trpc';
|
||||
|
||||
import type { Reply } from '~/types/offers';
|
||||
|
||||
type Props = Readonly<{
|
||||
comment: Reply;
|
||||
disableReply?: boolean;
|
||||
handleExpanded?: () => void;
|
||||
isExpanded?: boolean;
|
||||
profileId: string;
|
||||
replyLength?: number;
|
||||
token?: string;
|
||||
}>;
|
||||
|
||||
export default function CommentCard({
|
||||
comment: { createdAt, id, message, user },
|
||||
disableReply,
|
||||
handleExpanded,
|
||||
isExpanded,
|
||||
profileId,
|
||||
token = '',
|
||||
replyLength = 0,
|
||||
}: Props) {
|
||||
const { data: session, status } = useSession();
|
||||
const [isReplying, setIsReplying] = useState(false);
|
||||
const [currentReply, setCurrentReply] = useState<string>('');
|
||||
|
||||
const trpcContext = trpc.useContext();
|
||||
const createCommentMutation = trpc.useMutation(['offers.comments.create'], {
|
||||
onSuccess() {
|
||||
trpcContext.invalidateQueries([
|
||||
'offers.comments.getComments',
|
||||
{ profileId },
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
function handleReply() {
|
||||
if (token && token.length > 0) {
|
||||
// If it is with edit permission, send comment to API with username = null
|
||||
createCommentMutation.mutate(
|
||||
{
|
||||
message: currentReply,
|
||||
profileId,
|
||||
replyingToId: id,
|
||||
token,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setCurrentReply('');
|
||||
setIsReplying(false);
|
||||
if (!isExpanded) {
|
||||
handleExpanded?.();
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
} else if (status === 'authenticated') {
|
||||
// If not the OP and logged in, send comment to API
|
||||
createCommentMutation.mutate(
|
||||
{
|
||||
message: currentReply,
|
||||
profileId,
|
||||
replyingToId: id,
|
||||
userId: session.user?.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setCurrentReply('');
|
||||
setIsReplying(false);
|
||||
if (!isExpanded) {
|
||||
handleExpanded?.();
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// If not the OP and not logged in, direct users to log in
|
||||
signIn();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex pl-2">
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="flex flex-row font-bold">
|
||||
{user?.name ?? 'unknown user'}
|
||||
</div>
|
||||
<div className="mt-2 mb-2 flex flex-row ">{message}</div>
|
||||
<div className="flex flex-row items-center justify-start space-x-4 ">
|
||||
<div className="flex flex-col text-sm font-light text-gray-400">{`${timeSinceNow(
|
||||
createdAt,
|
||||
)} ago`}</div>
|
||||
{replyLength > 0 && (
|
||||
<div
|
||||
className="flex cursor-pointer flex-col text-sm text-purple-600 hover:underline"
|
||||
onClick={handleExpanded}>
|
||||
{isExpanded ? `Hide replies` : `View replies (${replyLength})`}
|
||||
</div>
|
||||
)}
|
||||
{!disableReply && (
|
||||
<div className="flex flex-col">
|
||||
<Button
|
||||
icon={ChatBubbleBottomCenterIcon}
|
||||
isLabelHidden={true}
|
||||
label="Reply"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onClick={() => setIsReplying(!isReplying)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!disableReply && isReplying && (
|
||||
<div className="mt-2 mr-2">
|
||||
<TextArea
|
||||
isLabelHidden={true}
|
||||
label="Comment"
|
||||
placeholder="Type your comment here"
|
||||
resize="none"
|
||||
value={currentReply}
|
||||
onChange={(value) => setCurrentReply(value)}
|
||||
/>
|
||||
<div className="mt-2 flex w-full justify-end">
|
||||
<div className="w-fit">
|
||||
<Button
|
||||
display="block"
|
||||
isLabelHidden={false}
|
||||
isLoading={createCommentMutation.isLoading}
|
||||
label="Reply"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onClick={handleReply}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<HorizontalDivider />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import CommentCard from '~/components/offers/profile/comments/CommentCard';
|
||||
|
||||
import type { Reply } from '~/types/offers';
|
||||
|
||||
type Props = Readonly<{
|
||||
comment: Reply;
|
||||
profileId: string;
|
||||
token?: string;
|
||||
}>;
|
||||
|
||||
export default function ExpandableCommentCard({
|
||||
comment,
|
||||
profileId,
|
||||
token = '',
|
||||
}: Props) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
return (
|
||||
<div>
|
||||
<CommentCard
|
||||
comment={comment}
|
||||
handleExpanded={() => setIsExpanded(!isExpanded)}
|
||||
isExpanded={isExpanded}
|
||||
profileId={profileId}
|
||||
replyLength={comment.replies?.length ?? 0}
|
||||
token={token}
|
||||
/>
|
||||
{comment.replies && (
|
||||
<div className="pl-8">
|
||||
{isExpanded &&
|
||||
comment.replies.map((reply) => (
|
||||
<CommentCard
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
disableReply={true}
|
||||
profileId={profileId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import type { OfferTableRowData } from '~/components/offers/table/types';
|
||||
import { convertMoneyToString } from '~/utils/offers/currency';
|
||||
import { formatDate } from '~/utils/offers/time';
|
||||
|
||||
export type OfferTableRowProps = Readonly<{ row: OfferTableRowData }>;
|
||||
import type { DashboardOffer } from '~/types/offers';
|
||||
|
||||
export type OfferTableRowProps = Readonly<{ row: DashboardOffer }>;
|
||||
|
||||
export default function OfferTableRow({
|
||||
row: { company, date, id, profileId, salary, title, yoe },
|
||||
row: { company, id, income, monthYearReceived, profileId, title, totalYoe },
|
||||
}: OfferTableRowProps) {
|
||||
return (
|
||||
<tr
|
||||
@@ -14,12 +17,12 @@ export default function OfferTableRow({
|
||||
<th
|
||||
className="whitespace-nowrap py-4 px-6 font-medium text-gray-900 dark:text-white"
|
||||
scope="row">
|
||||
{company}
|
||||
{company.name}
|
||||
</th>
|
||||
<td className="py-4 px-6">{title}</td>
|
||||
<td className="py-4 px-6">{yoe}</td>
|
||||
<td className="py-4 px-6">{salary}</td>
|
||||
<td className="py-4 px-6">{date}</td>
|
||||
<td className="py-4 px-6">{totalYoe}</td>
|
||||
<td className="py-4 px-6">{convertMoneyToString(income)}</td>
|
||||
<td className="py-4 px-6">{formatDate(monthYearReceived)}</td>
|
||||
<td className="space-x-4 py-4 px-6">
|
||||
<Link
|
||||
className="font-medium text-indigo-600 hover:underline dark:text-indigo-500"
|
||||
|
||||
@@ -2,18 +2,21 @@ import { useEffect, useState } from 'react';
|
||||
import { HorizontalDivider, Select, Spinner, Tabs } from '@tih/ui';
|
||||
|
||||
import OffersTablePagination from '~/components/offers/table/OffersTablePagination';
|
||||
import type {
|
||||
OfferTableRowData,
|
||||
PaginationType,
|
||||
import {
|
||||
OfferTableFilterOptions,
|
||||
OfferTableSortBy,
|
||||
OfferTableTabOptions,
|
||||
YOE_CATEGORY,
|
||||
} from '~/components/offers/table/types';
|
||||
import { YOE_CATEGORY } from '~/components/offers/table/types';
|
||||
|
||||
import { Currency } from '~/utils/offers/currency/CurrencyEnum';
|
||||
import CurrencySelector from '~/utils/offers/currency/CurrencySelector';
|
||||
import { formatDate } from '~/utils/offers/time';
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import OffersRow from './OffersRow';
|
||||
|
||||
import type { DashboardOffer, GetOffersResponse, Paging } from '~/types/offers';
|
||||
|
||||
const NUMBER_OF_OFFERS_IN_PAGE = 10;
|
||||
export type OffersTableProps = Readonly<{
|
||||
companyFilter: string;
|
||||
@@ -23,61 +26,47 @@ export default function OffersTable({
|
||||
companyFilter,
|
||||
jobTitleFilter,
|
||||
}: OffersTableProps) {
|
||||
const [currency, setCurrency] = useState('SGD'); // TODO: Detect location
|
||||
const [currency, setCurrency] = useState(Currency.SGD.toString()); // TODO: Detect location
|
||||
const [selectedTab, setSelectedTab] = useState(YOE_CATEGORY.ENTRY);
|
||||
const [pagination, setPagination] = useState<PaginationType>({
|
||||
currentPage: 1,
|
||||
numOfItems: 1,
|
||||
const [pagination, setPagination] = useState<Paging>({
|
||||
currentPage: 0,
|
||||
numOfItems: 0,
|
||||
numOfPages: 0,
|
||||
totalItems: 0,
|
||||
});
|
||||
const [offers, setOffers] = useState<Array<OfferTableRowData>>([]);
|
||||
|
||||
const [offers, setOffers] = useState<Array<DashboardOffer>>([]);
|
||||
const [selectedFilter, setSelectedFilter] = useState(
|
||||
OfferTableFilterOptions[0].value,
|
||||
);
|
||||
useEffect(() => {
|
||||
setPagination({
|
||||
currentPage: 1,
|
||||
numOfItems: 1,
|
||||
currentPage: 0,
|
||||
numOfItems: 0,
|
||||
numOfPages: 0,
|
||||
totalItems: 0,
|
||||
});
|
||||
}, [selectedTab]);
|
||||
}, [selectedTab, currency]);
|
||||
const offersQuery = trpc.useQuery(
|
||||
[
|
||||
'offers.list',
|
||||
{
|
||||
companyId: companyFilter,
|
||||
currency,
|
||||
limit: NUMBER_OF_OFFERS_IN_PAGE,
|
||||
location: 'Singapore, Singapore', // TODO: Geolocation
|
||||
offset: pagination.currentPage - 1,
|
||||
sortBy: '-monthYearReceived',
|
||||
offset: pagination.currentPage,
|
||||
sortBy: OfferTableSortBy[selectedFilter] ?? '-monthYearReceived',
|
||||
title: jobTitleFilter,
|
||||
yoeCategory: selectedTab,
|
||||
},
|
||||
],
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
const filteredData = response.data.map((res) => {
|
||||
return {
|
||||
company: res.company.name,
|
||||
date: formatDate(res.monthYearReceived),
|
||||
id: res.OffersFullTime
|
||||
? res.OffersFullTime!.id
|
||||
: res.OffersIntern!.id,
|
||||
profileId: res.profileId,
|
||||
salary: res.OffersFullTime
|
||||
? res.OffersFullTime?.totalCompensation.value
|
||||
: res.OffersIntern?.monthlySalary.value,
|
||||
title: res.OffersFullTime ? res.OffersFullTime?.level : '',
|
||||
yoe: 100,
|
||||
};
|
||||
});
|
||||
setOffers(filteredData);
|
||||
setPagination({
|
||||
currentPage: (response.paging.currPage as number) + 1,
|
||||
numOfItems: response.paging.numOfItemsInPage,
|
||||
numOfPages: response.paging.numOfPages,
|
||||
totalItems: response.paging.totalNumberOfOffers,
|
||||
});
|
||||
onError: (err) => {
|
||||
alert(err);
|
||||
},
|
||||
onSuccess: (response: GetOffersResponse) => {
|
||||
setOffers(response.data);
|
||||
setPagination(response.paging);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -88,24 +77,7 @@ export default function OffersTable({
|
||||
<div className="w-fit">
|
||||
<Tabs
|
||||
label="Table Navigation"
|
||||
tabs={[
|
||||
{
|
||||
label: 'Fresh Grad (0-3 YOE)',
|
||||
value: YOE_CATEGORY.ENTRY,
|
||||
},
|
||||
{
|
||||
label: 'Mid (4-7 YOE)',
|
||||
value: YOE_CATEGORY.MID,
|
||||
},
|
||||
{
|
||||
label: 'Senior (8+ YOE)',
|
||||
value: YOE_CATEGORY.SENIOR,
|
||||
},
|
||||
{
|
||||
label: 'Internship',
|
||||
value: YOE_CATEGORY.INTERN,
|
||||
},
|
||||
]}
|
||||
tabs={OfferTableTabOptions}
|
||||
value={selectedTab}
|
||||
onChange={(value) => setSelectedTab(value)}
|
||||
/>
|
||||
@@ -125,16 +97,11 @@ export default function OffersTable({
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
disabled={true}
|
||||
isLabelHidden={true}
|
||||
label=""
|
||||
options={[
|
||||
{
|
||||
label: 'Latest Submitted',
|
||||
value: 'latest-submitted',
|
||||
},
|
||||
]}
|
||||
value="latest-submitted"
|
||||
options={OfferTableFilterOptions}
|
||||
value={selectedFilter}
|
||||
onChange={(value) => setSelectedFilter(value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -162,7 +129,9 @@ export default function OffersTable({
|
||||
}
|
||||
|
||||
const handlePageChange = (currPage: number) => {
|
||||
setPagination({ ...pagination, currentPage: currPage });
|
||||
if (0 < currPage && currPage < pagination.numOfPages) {
|
||||
setPagination({ ...pagination, currentPage: currPage });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -187,14 +156,11 @@ export default function OffersTable({
|
||||
)}
|
||||
<OffersTablePagination
|
||||
endNumber={
|
||||
(pagination.currentPage - 1) * NUMBER_OF_OFFERS_IN_PAGE +
|
||||
offers.length
|
||||
pagination.currentPage * NUMBER_OF_OFFERS_IN_PAGE + offers.length
|
||||
}
|
||||
handlePageChange={handlePageChange}
|
||||
pagination={pagination}
|
||||
startNumber={
|
||||
(pagination.currentPage - 1) * NUMBER_OF_OFFERS_IN_PAGE + 1
|
||||
}
|
||||
startNumber={pagination.currentPage * NUMBER_OF_OFFERS_IN_PAGE + 1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Pagination } from '@tih/ui';
|
||||
|
||||
import type { PaginationType } from '~/components/offers/table/types';
|
||||
import type { Paging } from '~/types/offers';
|
||||
|
||||
type OffersTablePaginationProps = Readonly<{
|
||||
endNumber: number;
|
||||
handlePageChange: (page: number) => void;
|
||||
pagination: PaginationType;
|
||||
pagination: Paging;
|
||||
startNumber: number;
|
||||
}>;
|
||||
|
||||
@@ -30,13 +30,13 @@ export default function OffersTablePagination({
|
||||
</span>
|
||||
</span>
|
||||
<Pagination
|
||||
current={pagination.currentPage}
|
||||
current={pagination.currentPage + 1}
|
||||
end={pagination.numOfPages}
|
||||
label="Pagination"
|
||||
pagePadding={1}
|
||||
pagePadding={2}
|
||||
start={1}
|
||||
onSelect={(currPage) => {
|
||||
handlePageChange(currPage);
|
||||
handlePageChange(currPage - 1);
|
||||
}}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
export type OfferTableRowData = {
|
||||
company: string;
|
||||
date: string;
|
||||
id: string;
|
||||
profileId: string;
|
||||
salary: number | undefined;
|
||||
title: string;
|
||||
yoe: number;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
export enum YOE_CATEGORY {
|
||||
INTERN = 0,
|
||||
@@ -16,9 +6,47 @@ export enum YOE_CATEGORY {
|
||||
SENIOR = 3,
|
||||
}
|
||||
|
||||
export type PaginationType = {
|
||||
currentPage: number;
|
||||
numOfItems: number;
|
||||
numOfPages: number;
|
||||
totalItems: number;
|
||||
export const OfferTableTabOptions = [
|
||||
{
|
||||
label: 'Fresh Grad (0-2 YOE)',
|
||||
value: YOE_CATEGORY.ENTRY,
|
||||
},
|
||||
{
|
||||
label: 'Mid (3-5 YOE)',
|
||||
value: YOE_CATEGORY.MID,
|
||||
},
|
||||
{
|
||||
label: 'Senior (6+ YOE)',
|
||||
value: YOE_CATEGORY.SENIOR,
|
||||
},
|
||||
{
|
||||
label: 'Internship',
|
||||
value: YOE_CATEGORY.INTERN,
|
||||
},
|
||||
];
|
||||
|
||||
export const OfferTableFilterOptions = [
|
||||
{
|
||||
label: 'Latest Submitted',
|
||||
value: 'latest-submitted',
|
||||
},
|
||||
{
|
||||
label: 'Highest Salary',
|
||||
value: 'highest-salary',
|
||||
},
|
||||
{
|
||||
label: 'Highest YOE first',
|
||||
value: 'highest-yoe-first',
|
||||
},
|
||||
{
|
||||
label: 'Lowest YOE first',
|
||||
value: 'lowest-yoe-first',
|
||||
},
|
||||
];
|
||||
|
||||
export const OfferTableSortBy: Record<string, string> = {
|
||||
'highest-salary': '-totalCompensation',
|
||||
'highest-yoe-first': '-totalYoe',
|
||||
'latest-submitted': '-monthYearReceived',
|
||||
'lowest-yoe-first': '+totalYoe',
|
||||
};
|
||||
|
||||
@@ -6,12 +6,12 @@ import type { MonthYear } from '~/components/shared/MonthYearPicker';
|
||||
|
||||
export enum JobType {
|
||||
FullTime = 'FULLTIME',
|
||||
Internship = 'INTERNSHIP',
|
||||
Intern = 'INTERN',
|
||||
}
|
||||
|
||||
export const JobTypeLabel = {
|
||||
FULLTIME: 'Full-time',
|
||||
INTERNSHIP: 'Internship',
|
||||
INTERN: 'Internship',
|
||||
};
|
||||
|
||||
export enum EducationBackgroundType {
|
||||
@@ -20,17 +20,72 @@ export enum EducationBackgroundType {
|
||||
Masters = 'Masters',
|
||||
PhD = 'PhD',
|
||||
Professional = 'Professional',
|
||||
Seconday = 'Secondary',
|
||||
Secondary = 'Secondary',
|
||||
SelfTaught = 'Self-taught',
|
||||
}
|
||||
|
||||
export type Money = {
|
||||
currency: string;
|
||||
value: number;
|
||||
export type OffersProfilePostData = {
|
||||
background: BackgroundPostData;
|
||||
offers: Array<OfferPostData>;
|
||||
};
|
||||
|
||||
type FullTimeJobData = {
|
||||
base: Money;
|
||||
export type OffersProfileFormData = {
|
||||
background: BackgroundPostData;
|
||||
offers: Array<OfferFormData>;
|
||||
};
|
||||
|
||||
export type BackgroundPostData = {
|
||||
educations: Array<EducationPostData>;
|
||||
experiences: Array<ExperiencePostData>;
|
||||
specificYoes: Array<SpecificYoePostData>;
|
||||
totalYoe: number;
|
||||
};
|
||||
|
||||
type ExperiencePostData = {
|
||||
companyId?: string | null;
|
||||
durationInMonths?: number | null;
|
||||
jobType?: string | null;
|
||||
level?: string | null;
|
||||
location?: string | null;
|
||||
monthlySalary?: Money | null;
|
||||
specialization?: string | null;
|
||||
title?: string | null;
|
||||
totalCompensation?: Money | null;
|
||||
totalCompensationId?: string | null;
|
||||
};
|
||||
|
||||
type EducationPostData = {
|
||||
endDate?: Date | null;
|
||||
field?: string | null;
|
||||
school?: string | null;
|
||||
startDate?: Date | null;
|
||||
type?: string | null;
|
||||
};
|
||||
|
||||
type SpecificYoePostData = {
|
||||
domain: string;
|
||||
yoe: number;
|
||||
};
|
||||
|
||||
type SpecificYoe = SpecificYoePostData;
|
||||
|
||||
export type OfferPostData = {
|
||||
comments: string;
|
||||
companyId: string;
|
||||
jobType: string;
|
||||
location: string;
|
||||
monthYearReceived: Date;
|
||||
negotiationStrategy: string;
|
||||
offersFullTime?: OfferFullTimePostData | null;
|
||||
offersIntern?: OfferInternPostData | null;
|
||||
};
|
||||
|
||||
export type OfferFormData = Omit<OfferPostData, 'monthYearReceived'> & {
|
||||
monthYearReceived: MonthYear;
|
||||
};
|
||||
|
||||
export type OfferFullTimePostData = {
|
||||
baseSalary: Money;
|
||||
bonus: Money;
|
||||
level: string;
|
||||
specialization: string;
|
||||
@@ -39,7 +94,7 @@ type FullTimeJobData = {
|
||||
totalCompensation: Money;
|
||||
};
|
||||
|
||||
type InternshipJobData = {
|
||||
export type OfferInternPostData = {
|
||||
internshipCycle: string;
|
||||
monthlySalary: Money;
|
||||
specialization: string;
|
||||
@@ -47,83 +102,9 @@ type InternshipJobData = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
type OfferDetailsGeneralData = {
|
||||
comments: string;
|
||||
companyId: string;
|
||||
jobType: string;
|
||||
location: string;
|
||||
monthYearReceived: MonthYear;
|
||||
negotiationStrategy: string;
|
||||
};
|
||||
|
||||
export type FullTimeOfferDetailsFormData = OfferDetailsGeneralData & {
|
||||
job: FullTimeJobData;
|
||||
};
|
||||
|
||||
export type InternshipOfferDetailsFormData = OfferDetailsGeneralData & {
|
||||
job: InternshipJobData;
|
||||
};
|
||||
|
||||
export type OfferDetailsFormData =
|
||||
| FullTimeOfferDetailsFormData
|
||||
| InternshipOfferDetailsFormData;
|
||||
|
||||
export type OfferDetailsPostData = Omit<
|
||||
OfferDetailsFormData,
|
||||
'monthYearReceived'
|
||||
> & {
|
||||
monthYearReceived: Date;
|
||||
};
|
||||
|
||||
type SpecificYoe = {
|
||||
domain: string;
|
||||
yoe: number;
|
||||
};
|
||||
|
||||
type FullTimeExperience = {
|
||||
level?: string;
|
||||
totalCompensation?: Money;
|
||||
};
|
||||
|
||||
type InternshipExperience = {
|
||||
monthlySalary?: Money;
|
||||
};
|
||||
|
||||
type GeneralExperience = {
|
||||
companyId?: string;
|
||||
durationInMonths?: number;
|
||||
jobType?: string;
|
||||
specialization?: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export type Experience =
|
||||
| (FullTimeExperience & GeneralExperience)
|
||||
| (GeneralExperience & InternshipExperience);
|
||||
|
||||
type Education = {
|
||||
endDate?: Date;
|
||||
field?: string;
|
||||
school?: string;
|
||||
startDate?: Date;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
type BackgroundFormData = {
|
||||
educations: Array<Education>;
|
||||
experiences: Array<Experience>;
|
||||
specificYoes: Array<SpecificYoe>;
|
||||
totalYoe?: number;
|
||||
};
|
||||
|
||||
export type OfferProfileFormData = {
|
||||
background: BackgroundFormData;
|
||||
offers: Array<OfferDetailsFormData>;
|
||||
};
|
||||
|
||||
export type OfferProfilePostData = {
|
||||
background: BackgroundFormData;
|
||||
offers: Array<OfferDetailsPostData>;
|
||||
export type Money = {
|
||||
currency: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
type EducationDisplay = {
|
||||
@@ -158,3 +139,14 @@ export type BackgroundCard = {
|
||||
specificYoes: Array<SpecificYoe>;
|
||||
totalYoe: string;
|
||||
};
|
||||
|
||||
export type CommentEntity = {
|
||||
createdAt: Date;
|
||||
id: string;
|
||||
message: string;
|
||||
profileId: string;
|
||||
replies?: Array<CommentEntity>;
|
||||
replyingToId: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
@@ -84,9 +84,8 @@ export default function ContributeQuestionForm({
|
||||
name="company"
|
||||
render={({ field }) => (
|
||||
<CompaniesTypeahead
|
||||
onSelect={({ label }) => {
|
||||
// TODO: To change from using company name to company id (i.e., value)
|
||||
field.onChange(label);
|
||||
onSelect={({ id }) => {
|
||||
field.onChange(id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Document, Page, pdfjs } from 'react-pdf';
|
||||
import type { PDFDocumentProxy } from 'react-pdf/node_modules/pdfjs-dist';
|
||||
import {
|
||||
@@ -18,14 +19,30 @@ type Props = Readonly<{
|
||||
export default function ResumePdf({ url }: Props) {
|
||||
const [numPages, setNumPages] = useState(0);
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const [scale, setScale] = useState(1);
|
||||
const [pageWidth, setPageWidth] = useState(750);
|
||||
const [componentWidth, setComponentWidth] = useState(780);
|
||||
|
||||
const onPdfLoadSuccess = (pdf: PDFDocumentProxy) => {
|
||||
setNumPages(pdf.numPages);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onPageResize = () => {
|
||||
setComponentWidth(
|
||||
document.querySelector('#pdfView')?.getBoundingClientRect().width ??
|
||||
780,
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', onPageResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onPageResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id="pdfView">
|
||||
<div className="group relative">
|
||||
<Document
|
||||
className="flex h-[calc(100vh-17rem)] flex-row justify-center overflow-auto"
|
||||
@@ -33,25 +50,38 @@ export default function ResumePdf({ url }: Props) {
|
||||
loading={<Spinner display="block" size="lg" />}
|
||||
noData=""
|
||||
onLoadSuccess={onPdfLoadSuccess}>
|
||||
<Page pageNumber={pageNumber} scale={scale} width={750} />
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: clsx(
|
||||
pageWidth > componentWidth
|
||||
? `${pageWidth - componentWidth}px`
|
||||
: '',
|
||||
),
|
||||
}}>
|
||||
<Page
|
||||
pageNumber={pageNumber}
|
||||
renderTextLayer={false}
|
||||
width={pageWidth}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute top-2 right-5 hidden hover:block group-hover:block">
|
||||
<Button
|
||||
className="rounded-r-none focus:ring-0 focus:ring-offset-0"
|
||||
disabled={scale === 0.5}
|
||||
disabled={pageWidth === 450}
|
||||
icon={MagnifyingGlassMinusIcon}
|
||||
isLabelHidden={true}
|
||||
label="Zoom Out"
|
||||
variant="tertiary"
|
||||
onClick={() => setScale(scale - 0.25)}
|
||||
onClick={() => setPageWidth(pageWidth - 150)}
|
||||
/>
|
||||
<Button
|
||||
className="rounded-l-none focus:ring-0 focus:ring-offset-0"
|
||||
disabled={scale === 1.5}
|
||||
disabled={pageWidth === 1050}
|
||||
icon={MagnifyingGlassPlusIcon}
|
||||
isLabelHidden={true}
|
||||
label="Zoom In"
|
||||
variant="tertiary"
|
||||
onClick={() => setScale(scale + 0.25)}
|
||||
onClick={() => setPageWidth(pageWidth + 150)}
|
||||
/>
|
||||
</div>
|
||||
</Document>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { ResumeBadgeProps } from '../resume-badge';
|
||||
|
||||
export default function ResumeBadgeCoolIcon({ className }: ResumeBadgeProps) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
viewBox="0 0 511.999 511.999"
|
||||
x="0px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
y="0px">
|
||||
<circle cx="247.796" cy="255.997" fill="#FFDB6C" r="247.796" />
|
||||
<path
|
||||
d="M300.895,467.216c-136.853,0-247.794-110.941-247.794-247.794c0-73.116,31.673-138.825,82.04-184.181
|
||||
C54.919,76.258,0,159.716,0,256.003c0,136.853,110.941,247.794,247.794,247.794c63.738,0,121.848-24.073,165.754-63.612
|
||||
C379.75,457.466,341.462,467.216,300.895,467.216z"
|
||||
fill="#FCC56B"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
d="M141.308,259.555c-18.402,0-33.321,14.918-33.321,33.32h66.641
|
||||
C174.628,274.473,159.71,259.555,141.308,259.555z"
|
||||
fill="#F9A880"
|
||||
/>
|
||||
<path
|
||||
d="M431.948,259.555c-18.402,0-33.321,14.918-33.321,33.32h66.641
|
||||
C465.269,274.473,450.349,259.555,431.948,259.555z"
|
||||
fill="#F9A880"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M105.165,121.895c64.702-14.849,117.079-9.739,175.098,3.782c8.604,2.004,17.692,4.239,27.29,4.532
|
||||
c15.985,0.489,33.956-3.489,49.449-7.382c61.168-15.366,108.95-7.374,154.996,2.465l-3.402,27.211
|
||||
c-7.188,0.159-9.449,3.511-11.503,10.054c-10.747,34.242-1.594,93.16-81.048,86.233c-52.27-4.558-67.239-18.879-92.152-81.847
|
||||
c-2.12-5.356-3.497-14.207-15.602-13.88c-6.835,0.184-12.948,1.392-15.079,13.267c-3.973,22.126-34.188,82.245-95.535,82.179
|
||||
c-54.185-0.058-74.855-28.184-77.323-90.159c-0.306-7.695-7.012-9.156-11.035-9.246L105.165,121.895L105.165,121.895z"
|
||||
fill="#56586F"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
d="M199.128,113.331l-37.84,129.044c9.958,4.097,21.979,6.12,36.392,6.134
|
||||
c0.254,0,0.504-0.009,0.758-0.011l38.499-131.292C224.347,115.304,211.809,113.972,199.128,113.331z"
|
||||
fill="#737891"
|
||||
/>
|
||||
<path
|
||||
d="M434.438,114.376c-12.593-0.403-25.665,0-39.395,1.534l-33.781,115.202
|
||||
c9.238,7.758,20.144,12.263,34.543,15.016L434.438,114.376z"
|
||||
fill="#737891"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M319.673,395.914c-16.785,0-33.382-5.73-46.784-16.718c-4.305-3.53-4.933-9.882-1.403-14.187
|
||||
c3.53-4.306,9.882-4.933,14.188-1.403c15.016,12.314,35.551,15.539,53.597,8.423c17.582-6.937,30.535-23.491,33.802-43.202
|
||||
c0.913-5.492,6.101-9.207,11.594-8.296c5.493,0.911,9.207,6.102,8.297,11.594c-4.422,26.66-22.161,49.137-46.296,58.657
|
||||
C337.935,394.228,328.776,395.914,319.673,395.914z"
|
||||
fill="#7F184C"
|
||||
/>
|
||||
<ellipse
|
||||
cx="298.209"
|
||||
cy="78.261"
|
||||
fill="#FCEB88"
|
||||
rx="28.897"
|
||||
ry="51.747"
|
||||
transform="matrix(0.2723 -0.9622 0.9622 0.2723 141.702 343.89)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { ResumeBadgeProps } from '../resume-badge';
|
||||
|
||||
export default function ResumeBadgeRocketIcon({ className }: ResumeBadgeProps) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
viewBox="0 0 496.158 496.158"
|
||||
x="36px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
y="36px">
|
||||
<path
|
||||
d="M248.082,0.003C111.07,0.003,0,111.063,0,248.085c0,137.001,111.07,248.07,248.082,248.07 c137.006,0,248.076-111.069,248.076-248.07C496.158,111.062,385.088,0.003,248.082,0.003z"
|
||||
fill="#334D5C"
|
||||
/>
|
||||
<g>
|
||||
<polygon
|
||||
fill="#DBBB00"
|
||||
points="130.14,198.865 112.329,198.237 106.733,181.859 101.138,198.237 83.327,198.865 97.68,208.88 92.267,226.381 106.733,215.458 121.199,226.381 115.787,208.88 "
|
||||
/>
|
||||
<polygon
|
||||
fill="#DBBB00"
|
||||
points="112.416,202.889 115.484,191.248 105.788,198.382 95.265,191.881 99.455,203.294 89.618,211.306 102.168,210.835 106.348,222.679 110.18,210.584 122.334,210.282 "
|
||||
/>
|
||||
<polygon
|
||||
fill="#DBBB00"
|
||||
points="357.01,69.501 339.199,68.873 333.603,52.496 328.008,68.873 310.197,69.501 324.55,79.516 319.138,97.017 333.603,86.094 348.069,97.017 342.657,79.516 "
|
||||
/>
|
||||
<polygon
|
||||
fill="#DBBB00"
|
||||
points="339.286,73.525 342.354,61.884 332.658,69.018 322.135,62.517 326.325,73.93 316.488,81.942 329.038,81.472 333.218,93.315 337.05,81.221 349.204,80.918 "
|
||||
/>
|
||||
<polygon
|
||||
fill="#DBBB00"
|
||||
points="429.005,224.008 411.194,223.38 405.599,207.003 400.003,223.38 382.192,224.008 396.545,234.023 391.133,251.524 405.599,240.601 420.064,251.524 414.652,234.023 "
|
||||
/>
|
||||
<polygon
|
||||
fill="#DBBB00"
|
||||
points="411.281,228.032 414.35,216.392 404.653,223.526 394.13,217.024 398.32,228.437 388.483,236.449 401.033,235.979 405.213,247.822 409.045,235.728 421.199,235.426 "
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M383.34,314.795c-5.941-14.345-21.202-36.571-46.212-55.931 c-19.131-14.808-50.218-32.46-89.678-32.46c-39.018,0-69.746,16.634-88.654,30.588c-25.352,18.71-40.673,40.56-46.559,54.769 c-4.417,10.663-4.502,18.883-0.239,23.145c3.465,3.465,7.585,5.079,12.965,5.079c6.495,0,14.247-2.294,24.975-5.469 c20.098-5.947,50.469-14.936,97.513-14.936c48.545,0,80.322,8.617,101.35,14.318c10.673,2.894,18.384,4.985,24.472,4.986h0.003 c4.713,0,8.172-1.264,10.886-3.979C387.635,331.431,387.35,324.477,383.34,314.795z"
|
||||
fill="#EA6307"
|
||||
/>
|
||||
<path
|
||||
d="M286.255,121.222c-14.873-40.687-31.176-66.481-38.176-66.481c-6.988,0-23.253,25.596-38.118,66.13 c-15.702,42.815-29.844,102.297-29.844,165.89c0,40.446,6.193,56.536,6.193,56.536s25.869,13.801,62.818,13.801 s60.716-13.801,60.716-13.801s6.101-16.404,6.101-57.03C315.945,223.234,301.891,163.997,286.255,121.222z"
|
||||
fill="#DFEADC"
|
||||
/>
|
||||
<path
|
||||
d="M248.166,54.741c-8.74,0-24.42,24.539-38.204,66.13c10.715,2.375,24.12,4.325,39.314,4.325 c14.394,0,26.884-1.749,36.92-3.953C272.454,79.654,256.87,54.741,248.166,54.741z"
|
||||
fill="#CE5800"
|
||||
/>
|
||||
<path
|
||||
d="M248.165,54.741c-8.343,0-23.005,22.365-36.309,60.561c10.384,2.186,23.106,3.916,37.418,3.916 c13.501,0,25.329-1.54,35.026-3.549C271.044,77.446,256.471,54.741,248.165,54.741z"
|
||||
fill="#EA6307"
|
||||
/>
|
||||
<circle cx="248.079" cy="183.889" fill="#DBBB00" r="30.677" />
|
||||
<circle cx="248.079" cy="183.889" fill="#FFDB29" r="25.486" />
|
||||
<path
|
||||
d="M262.936,167.597c-8.602-8.601-22.547-8.602-31.148,0s-8.602,22.547,0,31.149 S271.538,176.199,262.936,167.597z"
|
||||
fill="#FFE36E"
|
||||
/>
|
||||
<path
|
||||
d="M249.007,368.151c-16.392,0.012-32.76,0.337-32.76,8.403c0,16.16,32.564,81.608,32.564,81.608 s33.101-65.882,33.101-81.608C281.912,368.464,265.447,368.139,249.007,368.151z"
|
||||
fill="#E17A2D"
|
||||
/>
|
||||
<path
|
||||
d="M249.079,371.948c-11.66,0-23.32-0.845-23.32,4.894c0,11.479,23.131,57.964,23.131,57.964 s23.51-46.794,23.51-57.964C272.399,371.103,260.739,371.948,249.079,371.948z"
|
||||
fill="#F4E028"
|
||||
/>
|
||||
<path
|
||||
d="M249.079,376.829c-7.005,0-14.011-1.99-14.011,1.458c0,6.896,13.897,34.824,13.897,34.824 s14.124-28.113,14.124-34.824C263.09,374.839,256.084,376.829,249.079,376.829z"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import type { ResumeBadgeProps } from '../resume-badge';
|
||||
|
||||
export default function ResumeBadgeTreasureIcon({
|
||||
className,
|
||||
}: ResumeBadgeProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 511.672 511.672"
|
||||
x="0px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
y="0px">
|
||||
<path
|
||||
d="M473.853,222.264l37.757-57.073c0,0,4.779-141.878-85.273-149.217h-170.47h-0.078H85.32
|
||||
C-4.731,23.313,0.047,165.19,0.047,165.19l37.929,56.526L0,325.057l42.629,170.579l426.398,0.062l42.645-170.595L473.853,222.264z"
|
||||
fill="#A85D5D"
|
||||
/>
|
||||
<g opacity={0.2}>
|
||||
<path
|
||||
d="M0.593,165.987C3.186,126.403,16.771,42.878,85.32,37.273h170.469h0.078h170.469
|
||||
c68.551,5.606,82.15,89.162,84.728,128.73l0.546-0.812c0,0,4.779-141.878-85.273-149.217h-170.47h-0.078H85.32
|
||||
C-4.731,23.313,0.047,165.19,0.047,165.19L0.593,165.987z"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
</g>
|
||||
<polygon
|
||||
fill="#723F3F"
|
||||
points="511.672,325.104 0,325.057 37.976,221.717 473.853,222.264 "
|
||||
/>
|
||||
<polygon
|
||||
fill="#8C4C4C"
|
||||
points="473.853,222.264 37.976,221.717 0.047,165.19 511.609,165.19 "
|
||||
/>
|
||||
<path
|
||||
d="M266.485,410.315c0,5.887-4.778,10.665-10.649,10.665c-5.887,0-10.665-4.778-10.665-10.665
|
||||
c0-5.888,4.778-10.649,10.665-10.649C261.707,399.666,266.485,404.428,266.485,410.315z"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
d="M170.251,295.045c-12.258,0-24.079-2.701-32.448-7.401c-6.387-3.591-10.181-8.042-10.181-11.914
|
||||
c0-7.87,16.615-19.315,42.629-19.315c12.257,0,24.094,2.701,32.463,7.417c6.371,3.575,10.181,8.026,10.181,11.898
|
||||
C212.895,283.615,196.28,295.045,170.251,295.045z"
|
||||
fill="#FFCE54"
|
||||
/>
|
||||
<path
|
||||
d="M255.524,295.045c-12.258,0-24.079-2.701-32.464-7.401c-6.371-3.591-10.165-8.042-10.165-11.914
|
||||
c0-7.87,16.599-19.315,42.629-19.315c12.257,0,24.078,2.701,32.463,7.417c6.371,3.575,10.165,8.026,10.165,11.898
|
||||
C298.152,283.615,281.555,295.045,255.524,295.045z"
|
||||
fill="#FFCE54"
|
||||
/>
|
||||
<path
|
||||
d="M340.797,295.045c-12.258,0-24.094-2.701-32.463-7.401c-6.371-3.591-10.182-8.042-10.182-11.914
|
||||
c0-7.87,16.615-19.315,42.645-19.315c12.258,0,24.078,2.701,32.448,7.417c6.371,3.575,10.181,8.026,10.181,11.898
|
||||
C383.426,283.615,366.813,295.045,340.797,295.045z"
|
||||
fill="#FFCE54"
|
||||
/>
|
||||
<path
|
||||
d="M212.895,265.065c-12.258,0-24.094-2.686-32.464-7.401c-6.371-3.592-10.181-8.026-10.181-11.899
|
||||
c0-7.885,16.614-19.332,42.645-19.332c12.242,0,24.078,2.717,32.448,7.417c6.371,3.576,10.181,8.042,10.181,11.915
|
||||
C255.524,253.634,238.909,265.065,212.895,265.065z"
|
||||
fill="#FFCE54"
|
||||
/>
|
||||
<path
|
||||
d="M298.152,265.065c-12.242,0-24.078-2.686-32.447-7.401c-6.371-3.592-10.181-8.026-10.181-11.899
|
||||
c0-7.885,16.615-19.332,42.628-19.332c12.258,0,24.094,2.717,32.464,7.417c6.371,3.576,10.181,8.042,10.181,11.915
|
||||
C340.797,253.634,324.184,265.065,298.152,265.065z"
|
||||
fill="#FFCE54"
|
||||
/>
|
||||
<path
|
||||
d="M255.524,235.099c-12.258,0-24.079-2.702-32.464-7.417c-6.371-3.575-10.165-8.026-10.165-11.898
|
||||
c0-7.87,16.599-19.315,42.629-19.315c12.257,0,24.078,2.701,32.463,7.417c6.371,3.576,10.165,8.026,10.165,11.898
|
||||
C298.152,223.653,281.555,235.099,255.524,235.099z"
|
||||
fill="#FFCE54"
|
||||
/>
|
||||
<path
|
||||
d="M91.629,325.104h72.516c4.153-3.123,6.417-6.511,6.417-9.415c0-3.873-3.81-8.308-10.181-11.899
|
||||
c-8.37-4.715-20.206-7.401-32.463-7.401c-26.015,0-42.629,11.431-42.629,19.301C85.289,318.718,87.6,322.074,91.629,325.104z"
|
||||
fill="#FFCE54"
|
||||
/>
|
||||
<path
|
||||
d="M176.902,325.104h72.516c4.153-3.123,6.402-6.511,6.402-9.415c0-3.873-3.794-8.308-10.165-11.899
|
||||
c-8.37-4.715-20.206-7.401-32.464-7.401c-26.03,0-42.629,11.431-42.629,19.301C170.563,318.718,172.874,322.074,176.902,325.104z"
|
||||
fill="#FFCE54"
|
||||
/>
|
||||
</g>
|
||||
<rect
|
||||
fill="#CCD1D9"
|
||||
height="98.75"
|
||||
width="96.27"
|
||||
x="206.586"
|
||||
y="325.106"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
d="M262.16,325.104h72.516c4.154-3.123,6.418-6.511,6.418-9.415c0-3.873-3.795-8.308-10.165-11.899
|
||||
c-8.386-4.715-20.206-7.401-32.464-7.401c-26.029,0-42.645,11.431-42.645,19.301C255.82,318.718,258.147,322.074,262.16,325.104z"
|
||||
fill="#FFCE54"
|
||||
/>
|
||||
<path
|
||||
d="M347.434,325.104h72.516c4.154-3.123,6.418-6.511,6.418-9.415c0-3.873-3.81-8.308-10.181-11.899
|
||||
c-8.37-4.715-20.206-7.401-32.448-7.401c-26.029,0-42.645,11.431-42.645,19.301C341.094,318.718,343.404,322.074,347.434,325.104z"
|
||||
fill="#FFCE54"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M434.331,325.104c1.733-2.951,2.702-6.121,2.702-9.415c0-15.179-20.098-27.732-46.158-29.7
|
||||
c2.076-3.186,3.217-6.652,3.217-10.259c0-14.507-18.332-26.593-42.66-29.372c0-0.203,0.016-0.406,0.016-0.593
|
||||
c0-14.522-18.316-26.608-42.66-29.387c0.016-0.188,0.031-0.391,0.031-0.594c0-16.552-23.86-29.98-53.294-29.98
|
||||
c-29.435,0-53.294,13.429-53.294,29.98c0,0.203,0.016,0.406,0.031,0.594c-24.344,2.779-42.661,14.865-42.661,29.387
|
||||
c0,0.188,0.016,0.39,0.016,0.593c-24.328,2.779-42.66,14.865-42.66,29.372c0,3.622,1.14,7.104,3.248,10.321
|
||||
c-25.78,2.093-45.58,14.568-45.58,29.638c0,3.294,0.968,6.464,2.701,9.415H434.331z M236.989,319.998
|
||||
c-6.605,2.811-15.053,4.356-23.797,4.356s-17.192-1.546-23.782-4.356c-3.654-1.562-5.934-3.154-7.198-4.31
|
||||
c1.265-1.124,3.544-2.733,7.198-4.278c6.59-2.812,15.038-4.373,23.782-4.373c8.745,0,17.192,1.562,23.797,4.373
|
||||
c3.638,1.545,5.918,3.154,7.199,4.278C242.907,316.844,240.627,318.437,236.989,319.998z M231.727,280.024
|
||||
c-3.638-1.546-5.918-3.154-7.199-4.294c0.297-0.266,0.656-0.547,1.062-0.858c9.322-1.281,17.676-3.936,24.344-7.59
|
||||
c1.843-0.125,3.716-0.203,5.59-0.203s3.748,0.078,5.574,0.203c6.684,3.654,15.038,6.309,24.359,7.59
|
||||
c0.406,0.312,0.766,0.593,1.062,0.858c-1.28,1.14-3.561,2.748-7.199,4.294c-6.604,2.811-15.053,4.372-23.796,4.372
|
||||
C246.779,284.396,238.332,282.834,231.727,280.024z M322.246,319.998c-6.589,2.811-15.037,4.356-23.781,4.356
|
||||
s-17.191-1.546-23.797-4.356c-3.639-1.562-5.918-3.154-7.199-4.31c1.281-1.124,3.561-2.733,7.199-4.278
|
||||
c6.605-2.812,15.053-4.373,23.797-4.373s17.192,1.562,23.781,4.373c3.654,1.545,5.934,3.154,7.199,4.278
|
||||
C328.18,316.844,325.9,318.437,322.246,319.998z M407.52,311.41c3.654,1.545,5.935,3.154,7.199,4.278
|
||||
c-1.265,1.155-3.545,2.748-7.199,4.31c-6.589,2.811-15.053,4.356-23.781,4.356c-8.744,0-17.191-1.546-23.797-4.356
|
||||
c-3.654-1.562-5.934-3.154-7.199-4.31c1.266-1.124,3.545-2.733,7.199-4.278c6.605-2.812,15.053-4.373,23.797-4.373
|
||||
C392.467,307.037,400.931,308.599,407.52,311.41z M340.797,267.078c8.744,0,17.192,1.547,23.782,4.357
|
||||
c3.653,1.562,5.934,3.154,7.198,4.294c-1.265,1.14-3.545,2.748-7.198,4.294c-6.59,2.811-15.038,4.372-23.782,4.372
|
||||
s-17.191-1.562-23.797-4.372c-3.654-1.546-5.934-3.154-7.198-4.294c0.296-0.266,0.655-0.547,1.062-0.858
|
||||
c9.322-1.281,17.676-3.936,24.344-7.59C337.05,267.156,338.908,267.078,340.797,267.078z M298.152,237.098
|
||||
c8.744,0,17.192,1.546,23.798,4.356c3.653,1.562,5.934,3.154,7.198,4.31c-0.297,0.25-0.656,0.546-1.062,0.859
|
||||
c-9.322,1.28-17.677,3.95-24.345,7.573c-1.842,0.141-3.7,0.219-5.59,0.219c-1.873,0-3.731-0.078-5.574-0.219
|
||||
c-6.684-3.623-15.037-6.293-24.359-7.573c-0.406-0.312-0.75-0.609-1.047-0.859c0.297-0.281,0.641-0.562,1.047-0.875
|
||||
c9.322-1.28,17.676-3.935,24.359-7.573C294.405,237.176,296.279,237.098,298.152,237.098z M231.727,211.489
|
||||
c6.605-2.811,15.053-4.356,23.797-4.356s17.192,1.546,23.796,4.356c3.639,1.546,5.919,3.154,7.199,4.294
|
||||
c-0.297,0.266-0.656,0.562-1.062,0.875c-9.321,1.281-17.676,3.935-24.359,7.558c-1.826,0.156-3.7,0.218-5.574,0.218
|
||||
s-3.748-0.062-5.59-0.218c-6.667-3.623-15.021-6.277-24.344-7.558c-0.406-0.312-0.765-0.609-1.062-0.875
|
||||
C225.809,214.644,228.088,213.035,231.727,211.489z M189.098,241.454c6.605-2.811,15.053-4.356,23.797-4.356
|
||||
c1.874,0,3.732,0.078,5.574,0.219c6.684,3.639,15.038,6.293,24.359,7.573c0.406,0.312,0.75,0.594,1.046,0.875
|
||||
c-0.297,0.25-0.64,0.546-1.046,0.859c-9.322,1.28-17.691,3.95-24.359,7.573c-1.842,0.141-3.701,0.219-5.574,0.219
|
||||
c-1.89,0-3.748-0.078-5.59-0.219c-6.667-3.623-15.021-6.293-24.344-7.573c-0.406-0.312-0.765-0.609-1.062-0.859
|
||||
C183.164,244.608,185.444,243.016,189.098,241.454z M146.469,271.436c6.589-2.811,15.037-4.357,23.782-4.357
|
||||
c1.889,0,3.748,0.078,5.59,0.203c6.667,3.654,15.021,6.309,24.344,7.59c0.406,0.312,0.765,0.593,1.062,0.858
|
||||
c-1.28,1.14-3.544,2.748-7.199,4.294c-6.605,2.811-15.053,4.372-23.797,4.372c-8.745,0-17.192-1.562-23.782-4.372
|
||||
c-3.654-1.546-5.934-3.154-7.199-4.294C140.535,274.59,142.815,272.997,146.469,271.436z M104.136,311.41
|
||||
c6.59-2.812,15.053-4.373,23.782-4.373c8.745,0,17.192,1.562,23.797,4.373c3.654,1.545,5.934,3.154,7.198,4.278
|
||||
c-1.265,1.155-3.544,2.748-7.198,4.31c-6.605,2.811-15.053,4.356-23.797,4.356s-17.192-1.546-23.782-4.356
|
||||
c-3.654-1.562-5.934-3.154-7.198-4.31C98.203,314.565,100.482,312.955,104.136,311.41z"
|
||||
fill="#F6BB42"
|
||||
/>
|
||||
<rect
|
||||
fill="#AAB2BC"
|
||||
height="26.702"
|
||||
width="21.331"
|
||||
x="244.856"
|
||||
y="383.616"
|
||||
/>
|
||||
<path
|
||||
d="M270.936,369.982c0,8.448-6.855,15.287-15.287,15.287c-8.448,0-15.303-6.839-15.303-15.287
|
||||
c0-8.447,6.855-15.287,15.303-15.287C264.08,354.694,270.936,361.534,270.936,369.982z"
|
||||
fill="#434A54"
|
||||
/>
|
||||
<path
|
||||
d="M319.467,165.19c0,0,0.016-0.016,0.016-0.031V122.53c0-5.871-4.777-10.649-10.664-10.649H202.23
|
||||
c-5.887,0-10.665,4.778-10.665,10.649v42.629c0,0.016,0,0.031,0,0.031H319.467z"
|
||||
fill="#CCD1D9"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
d="M212.895,165.19v-31.995h85.257v31.995h21.314c0-21.798,0.016-42.66,0.016-42.66
|
||||
c0-5.871-4.777-10.649-10.664-10.649H202.23c-5.887,0-10.665,4.778-10.665,10.649c0,0,0,20.862,0,42.66H212.895z"
|
||||
fill="#AAB2BC"
|
||||
/>
|
||||
<polygon
|
||||
fill="#AAB2BC"
|
||||
points="55.152,325.057 92.894,495.651 114.723,495.651 76.998,325.057 "
|
||||
/>
|
||||
<polygon
|
||||
fill="#AAB2BC"
|
||||
points="434.159,325.088 396.387,495.698 418.217,495.698 455.989,325.088 "
|
||||
/>
|
||||
<path
|
||||
d="M298.152,325.088v95.893h-85.257v-95.908h-21.33v106.573c0,5.871,4.778,10.649,10.665,10.649
|
||||
h106.588c5.887,0,10.664-4.778,10.664-10.649V325.088H298.152z"
|
||||
fill="#AAB2BC"
|
||||
/>
|
||||
<path
|
||||
d="M255.836,335.707c-17.661,0-31.979,14.318-31.979,31.979c0,17.66,14.319,31.979,31.979,31.979
|
||||
c17.645,0,31.964-14.319,31.964-31.979C287.8,350.025,273.481,335.707,255.836,335.707z M255.836,378.336
|
||||
c-5.887,0-10.665-4.778-10.665-10.649c0-5.872,4.778-10.665,10.665-10.665c5.871,0,10.649,4.793,10.649,10.665
|
||||
C266.485,373.558,261.707,378.336,255.836,378.336z"
|
||||
fill="#AAB2BC"
|
||||
/>
|
||||
</g>
|
||||
<polygon
|
||||
opacity={0.1}
|
||||
points="0,325.057 7.823,303.727 503.803,303.727 511.672,325.057"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
3
apps/portal/src/components/resumes/badgeIcons/resume-badge.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export type ResumeBadgeProps = Readonly<{
|
||||
className: string;
|
||||
}>;
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ResumeBadgeProps } from '../resume-badge';
|
||||
|
||||
export default function ResumeBadgeDetectiveIcon({
|
||||
className,
|
||||
}: ResumeBadgeProps) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
height="36px"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
role="img"
|
||||
viewBox="0 0 36 36"
|
||||
width="36px"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M33 36v-1a6 6 0 0 0-6-6H9a6 6 0 0 0-6 6v1h30zm-6.25-15.565c1.188.208 2.619.129 2.416.917c-.479 1.854-2.604 1.167-2.979 1.188c-.375.02.563-2.105.563-2.105z"
|
||||
fill="#66757F"></path>
|
||||
<path
|
||||
d="M27.062 20.645c1.875.25 2.541.416 1.166.958c-.772.305-2.243 4.803-3.331 4.118c-1.087-.685 2.165-5.076 2.165-5.076z"
|
||||
fill="#292F33"></path>
|
||||
<path
|
||||
d="M9.255 20.435c-1.188.208-2.619.129-2.416.917c.479 1.854 2.604 1.167 2.979 1.188c.375.02-.563-2.105-.563-2.105z"
|
||||
fill="#66757F"></path>
|
||||
<path
|
||||
d="M8.943 20.645c-1.875.25-2.541.416-1.166.958c.772.305 2.243 4.803 3.331 4.118c1.088-.685-2.165-5.076-2.165-5.076z"
|
||||
fill="#292F33"></path>
|
||||
<path
|
||||
d="M21.771 4.017c-1.958-.634-6.566-.461-7.718 1.037c-2.995.058-6.508 2.764-6.969 6.335c-.456 3.534.56 5.175.922 7.833c.409 3.011 2.102 3.974 3.456 4.377c1.947 2.572 4.017 2.462 7.492 2.462c6.787 0 10.019-4.541 10.305-12.253c.172-4.665-2.565-8.198-7.488-9.791z"
|
||||
fill="#FFAC33"></path>
|
||||
<path
|
||||
d="M25.652 14.137c-.657-.909-1.497-1.641-3.34-1.901c.691.317 1.353 1.411 1.44 2.016c.086.605.173 1.094-.374.49c-2.192-2.423-4.579-1.469-6.944-2.949c-1.652-1.034-2.155-2.177-2.155-2.177s-.202 1.526-2.707 3.081c-.726.451-1.593 1.455-2.073 2.937c-.346 1.066-.238 2.016-.238 3.64c0 4.74 3.906 8.726 8.726 8.726s8.726-4.02 8.726-8.726c-.004-2.948-.312-4.1-1.061-5.137z"
|
||||
fill="#FFDC5D"></path>
|
||||
<path
|
||||
d="M18.934 21.565h-1.922a.481.481 0 0 1-.481-.481v-.174c0-.265.215-.482.481-.482h1.922c.265 0 .482.216.482.482v.174a.481.481 0 0 1-.482.481"
|
||||
fill="#C1694F"></path>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M7.657 14.788c.148.147.888.591 1.036 1.034c.148.443.445 2.954 1.333 3.693c.916.762 4.37.478 5.032.149c1.48-.738 1.662-2.798 1.924-3.842c.148-.591 1.036-.591 1.036-.591s.888 0 1.036.591c.262 1.044.444 3.104 1.924 3.841c.662.33 4.116.614 5.034-.147c.887-.739 1.183-3.25 1.331-3.694c.146-.443.888-.886 1.035-1.034c.148-.148.148-.739 0-.887c-.296-.295-3.788-.559-7.548-.148c-.75.082-1.035.295-2.812.295c-1.776 0-2.062-.214-2.812-.295c-3.759-.411-7.252-.148-7.548.148c-.149.148-.149.74-.001.887z"
|
||||
fill="#292F33"
|
||||
fill-rule="evenodd"></path>
|
||||
<path
|
||||
d="M7.858 8.395S9.217-.506 13.79.023c3.512.406 4.89.825 7.833.097c1.947-.482 4.065 1.136 5.342 4.379a27.72 27.72 0 0 1 1.224 4.041s3.938-.385 4.165 1.732c.228 2.117-4.354 4.716-15.889 4.716C10 14.987 3.33 12.63 3.013 10.657c-.317-1.973 4.845-2.262 4.845-2.262z"
|
||||
fill="#66757F"></path>
|
||||
<path
|
||||
d="M8.125 7.15s-.27 1.104-.406 1.871c-.136.768.226 1.296 2.705 1.824c3.287.7 10.679.692 15.058-.383c1.759-.432 2.886-.72 2.751-1.583c-.167-1.068-.196-1.066-.541-2.208c0 0-1.477.502-3.427.96c-2.66.624-9.964.911-13.481.144c-1.874-.41-2.659-.625-2.659-.625zm-.136 13.953c-.354.145 2.921 1.378 7.48 1.458c4.771.084 6.234.39 5.146 1.459c-1.146 1.125-.852 2.894-.771 3.418c.081.524 2.047 1.916 2.208 2.56c.161.645-1.229 5.961-1.229 5.961l-8.729-.252c-2.565-8.844-2.883-8.501-4.105-13.604c-.241-1.008 0-1 0-1z"
|
||||
fill="#292F33"></path>
|
||||
<path
|
||||
d="M6.989 21.144c-.354.146 2.921 1.378 7.48 1.458c4.771.084 6.234.39 5.146 1.459c-1.146 1.125-.664 2.894-.583 3.418c.081.524 1.859 1.916 2.021 2.561c.16.644-1.231 5.96-1.231 5.96l-8.729-.252c-2.565-8.844-2.883-8.501-4.105-13.604c-.24-1.008.001-1 .001-1z"
|
||||
fill="#66757F"></path>
|
||||
<path
|
||||
d="M28.052 21.103c.354.145-2.921 1.378-7.479 1.458c-4.771.084-6.234.39-5.146 1.459c1.146 1.125 2.976 2.892 2.896 3.416c-.081.524-4.172 1.918-4.333 2.562c-.161.645 1.229 5.961 1.229 5.961l8.729-.252c2.565-8.844 2.883-8.501 4.104-13.604c.241-1.008 0-1 0-1z"
|
||||
fill="#292F33"></path>
|
||||
<path
|
||||
d="M28.958 21.103c.354.145-2.921 1.378-7.479 1.458c-4.771.084-6.234.39-5.146 1.459c1.146 1.125 2.977 2.892 2.896 3.416c-.081.524-4.172 1.918-4.333 2.562c-.161.645 1.229 5.961 1.229 5.961l8.657.01c2.565-8.844 2.955-8.763 4.177-13.866c.24-1.008-.001-1-.001-1z"
|
||||
fill="#66757F"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ResumeBadgeProps } from '../resume-badge';
|
||||
|
||||
export default function ResumeBadgeEagleIcon({ className }: ResumeBadgeProps) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
height="36px"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 36 36"
|
||||
width="36px"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.042 26c.33 0 .651.121.963.331c1.368-8.106 20.362-8.248 21.755-.29c1.666.412 3.08 4.378 3.748 9.959h-31c.793-5.899 2.522-10 4.534-10z"
|
||||
fill="#292F33"></path>
|
||||
<path
|
||||
d="M7.043 23.688C10.966 12.533 6.508 3 17.508 3s8.736 8.173 13.193 19.125c1.119 2.75-1.443 5.908-1.443 5.908s-2.612-4.756-4.75-5.846c-.591 3.277-1.75 6.938-1.75 6.938s-2.581-2.965-5.587-5.587c-.879 1.009-2.065 2.183-3.663 3.462c-.349-1.048-.943-2.339-1.568-3.576c-1.468 2.238-3.182 4.951-3.182 4.951s-2.507-2.435-1.715-4.687z"
|
||||
fill="#E1E8ED"></path>
|
||||
<path
|
||||
d="M11.507 5c-4.36 3.059-5.542 2.16-7.812 3.562c-2.125 1.312-2 4.938-.125 8.062c.579-2.661-.5-3.149 6.938-3.149c5 0 7.928.289 7-1c-.927-1.289-10.027.459-6.001-7.475z"
|
||||
fill="#FFCC4D"></path>
|
||||
<path
|
||||
d="M16.535 7.517a1.483 1.483 0 1 1-2.967 0c0-.157.031-.305.076-.446h2.816c.044.141.075.289.075.446z"
|
||||
fill="#292F33"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { ResumeBadgeProps } from '../resume-badge';
|
||||
|
||||
export default function ResumeBadgeSuperheroIcon({
|
||||
className,
|
||||
}: ResumeBadgeProps) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
height="36px"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
role="img"
|
||||
viewBox="0 0 36 36"
|
||||
width="36px"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M33.035 28.055c-3.843-2.612-14.989 2.92-15.037 2.944c-.047-.024-11.193-5.556-15.037-2.944C-.021 30.082 0 36 0 36h35.996s.021-5.918-2.961-7.945z"
|
||||
fill="#A0041E"></path>
|
||||
<path
|
||||
d="M32 29c-2.155-1.085-4 0-4 0l-10-1l-10 1s-1.845-1.085-4 0c-3.995 2.011-2 7-2 7h32s1.995-4.989-2-7z"
|
||||
fill="#55ACEE"></path>
|
||||
<path
|
||||
d="M24.056 36c-1.211-1.194-3.466-2-6.056-2s-4.845.806-6.056 2h12.112z"
|
||||
fill="#DD2E44"></path>
|
||||
<path
|
||||
d="M13.64 28.537C15.384 29.805 16.487 30.5 18 30.5c1.512 0 2.615-.696 4.359-1.963V24.29h-8.72v4.247z"
|
||||
fill="#D4AB88"></path>
|
||||
<path
|
||||
d="M30.453 27c-1.953-.266-3.594.547-3.594.547s-.845-.594-1.845-.614c-1.469-.03-2.442.935-3.014 1.755C21.281 29.719 19 30 18 30s-3.281-.281-4-1.312c-.572-.82-1.545-1.784-3.014-1.755c-1 .02-1.845.614-1.845.614S7.5 26.734 5.547 27c-1.305.177-2.357.764-2.846 1.248c2.83-1.685 4.757-.229 6.065.643C10.074 29.763 11 32 11 32c2-1 7-1 7-1s5 0 7 1c0 0 .926-2.237 2.234-3.109c1.308-.872 3.234-2.328 6.065-.643c-.489-.484-1.541-1.071-2.846-1.248z"
|
||||
fill="#DD2E44"></path>
|
||||
<path
|
||||
d="M13.632 25.5c.368 2.027 2.724 2.219 4.364 2.219c1.639 0 4.004-.191 4.363-2.219v-3.019h-8.728V25.5z"
|
||||
fill="#CC9B7A"></path>
|
||||
<path
|
||||
d="M11.444 15.936c0 1.448-.734 2.622-1.639 2.622s-1.639-1.174-1.639-2.622s.734-2.623 1.639-2.623c.905-.001 1.639 1.174 1.639 2.623m16.389 0c0 1.448-.733 2.622-1.639 2.622c-.905 0-1.639-1.174-1.639-2.622s.733-2.623 1.639-2.623c.906-.001 1.639 1.174 1.639 2.623"
|
||||
fill="#D4AB88"></path>
|
||||
<path
|
||||
d="M18 7c-5 0-8 2-8 5s0 9 2 12s4 3 6 3s4 0 6-3s2-9 2-12s-3-5-8-5z"
|
||||
fill="#D4AB88"></path>
|
||||
<path
|
||||
d="M18.821 3.118c6.004.49 8.356 4.246 8.356 7.851c0 3.604-.706 5.047-1.412 3.604c-.706-1.441-1.356-3.368-1.356-3.368s-4.292.485-5.704-.957c0 0 2.118 4.326-2.118 0c0 0 .706 2.884-3.53-.72c0 0-2.118 1.442-2.824 5.046c-.196 1.001-1.412 0-1.412-3.604c.001-2.677.179-6.652 4.908-6.17c1.028-1.639 3.018-1.851 5.092-1.682z"
|
||||
fill="#963B22"></path>
|
||||
<path
|
||||
d="M25 12c-3 0-5 1-7 1s-4-1-7-1s-1 5.72 0 6.72s5-1 7-1s6 2 7 1S28 12 25 12z"
|
||||
fill="#269"></path>
|
||||
<path
|
||||
d="M14 17c-.55 0-1-.45-1-1v-1c0-.55.45-1 1-1s1 .45 1 1v1c0 .55-.45 1-1 1m8 0c-.55 0-1-.45-1-1v-1c0-.55.45-1 1-1s1 .45 1 1v1c0 .55-.45 1-1 1"
|
||||
fill="#88C9F9"></path>
|
||||
<path
|
||||
d="M18.75 19.75h-1.5c-.413 0-.75-.337-.75-.75s.337-.75.75-.75h1.5c.413 0 .75.337.75.75s-.337.75-.75.75m-.75 3.5c-2.058 0-3.594-.504-3.658-.525a.5.5 0 0 1 .316-.949c.014.004 1.455.474 3.342.474s3.328-.47 3.343-.475a.5.5 0 0 1 .316.949c-.065.022-1.601.526-3.659.526z"
|
||||
fill="#C1694F"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { ResumeBadgeProps } from '../resume-badge';
|
||||
|
||||
export default function ResumeBadgeBookIcon({ className }: ResumeBadgeProps) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
viewBox="0 0 512 512"
|
||||
x="0px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
y="0px">
|
||||
<path
|
||||
d="M8.17,90.446v350.268H224.7c2.367,0,3.648,1.543,4.089,2.206l9.701,11.315l35.025-0.003l9.699-11.316
|
||||
c0.439-0.661,1.719-2.202,4.086-2.202H503.83V90.446H8.17z"
|
||||
fill="#FF7226"
|
||||
/>
|
||||
<path
|
||||
d="M224.699,57.766H40.851v350.268h183.848c13.061,0,24.571,6.669,31.301,16.786l21.787-175.126
|
||||
L256,74.567C249.271,64.442,237.767,57.766,224.699,57.766z"
|
||||
fill="#F7EBD4"
|
||||
/>
|
||||
<path
|
||||
d="M287.301,57.766c-13.068,0-24.573,6.677-31.301,16.801v350.252
|
||||
c6.729-10.119,18.238-16.786,31.301-16.786h183.848V57.766H287.301z"
|
||||
fill="#D2F0E7"
|
||||
/>
|
||||
<rect fill="#F99FB6" height="67.028" width="128" x="84.426" y="297.428" />
|
||||
<g>
|
||||
<path
|
||||
d="M256,148.099c4.513,0,8.17-3.658,8.17-8.17v-32.681c0-4.512-3.657-8.17-8.17-8.17
|
||||
s-8.17,3.658-8.17,8.17v32.681C247.83,144.441,251.487,148.099,256,148.099z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M256,390.861c4.513,0,8.17-3.658,8.17-8.17V172.609c0-4.512-3.657-8.17-8.17-8.17
|
||||
s-8.17,3.658-8.17,8.17v210.081C247.83,387.203,251.487,390.861,256,390.861z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M503.83,82.276c-4.513,0-8.17,3.658-8.17,8.17v342.098H287.3c-4.182,0-8.077,1.987-10.54,5.346
|
||||
l-7.004,8.172l-27.511,0.002l-7.007-8.172c-2.467-3.36-6.363-5.348-10.541-5.348H16.34V90.446c0-4.512-3.657-8.17-8.17-8.17
|
||||
S0,85.934,0,90.446v350.268c0,4.512,3.657,8.17,8.17,8.17h214.971l9.146,10.668c1.552,1.81,3.818,2.852,6.203,2.852l35.025-0.003
|
||||
c2.385,0,4.652-1.043,6.203-2.854l9.139-10.664H503.83c4.513,0,8.17-3.658,8.17-8.17V90.446
|
||||
C512,85.934,508.343,82.276,503.83,82.276z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M40.851,416.204H224.7c9.866,0,19.024,4.912,24.498,13.141c1.515,2.277,4.068,3.645,6.803,3.645
|
||||
c2.734,0,5.288-1.368,6.802-3.646c5.471-8.228,14.629-13.14,24.496-13.14h183.849c4.513,0,8.17-3.658,8.17-8.17V57.766
|
||||
c0-4.512-3.657-8.17-8.17-8.17H287.3c-11.783,0-22.915,4.503-31.3,12.389c-8.386-7.885-19.517-12.389-31.3-12.389H40.851
|
||||
c-4.513,0-8.17,3.658-8.17,8.17v350.268C32.681,412.546,36.338,416.204,40.851,416.204z M49.021,65.936H224.7
|
||||
c9.865,0,19.022,4.917,24.495,13.154c1.514,2.279,4.068,3.648,6.804,3.648c2.736,0,5.29-1.369,6.805-3.648
|
||||
c5.472-8.237,14.629-13.153,24.494-13.153h175.679v333.927H287.3c-11.784,0-22.915,4.5-31.301,12.378
|
||||
c-8.386-7.878-19.517-12.378-31.298-12.378H49.021V65.936z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M212.426,93.17h-128c-4.513,0-8.17,3.658-8.17,8.17c0,4.512,3.657,8.17,8.17,8.17h128
|
||||
c4.513,0,8.17-3.658,8.17-8.17C220.596,96.828,216.939,93.17,212.426,93.17z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M212.426,125.851h-128c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17h128
|
||||
c4.513,0,8.17-3.658,8.17-8.17C220.596,129.509,216.939,125.851,212.426,125.851z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M212.426,158.532h-128c-4.513,0-8.17,3.658-8.17,8.17c0,4.512,3.657,8.17,8.17,8.17h128
|
||||
c4.513,0,8.17-3.658,8.17-8.17C220.596,162.19,216.939,158.532,212.426,158.532z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M212.426,191.212h-128c-4.513,0-8.17,3.658-8.17,8.17c0,4.512,3.657,8.17,8.17,8.17h128
|
||||
c4.513,0,8.17-3.658,8.17-8.17C220.596,194.87,216.939,191.212,212.426,191.212z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M212.426,223.893h-128c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17h128
|
||||
c4.513,0,8.17-3.658,8.17-8.17S216.939,223.893,212.426,223.893z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M84.426,272.914h64c4.513,0,8.17-3.658,8.17-8.17c0-4.512-3.657-8.17-8.17-8.17h-64
|
||||
c-4.513,0-8.17,3.658-8.17,8.17C76.255,269.256,79.912,272.914,84.426,272.914z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M212.426,289.255h-128c-4.513,0-8.17,3.658-8.17,8.17v67.034c0,4.512,3.657,8.17,8.17,8.17h128
|
||||
c4.513,0,8.17-3.658,8.17-8.17v-67.034C220.596,292.913,216.939,289.255,212.426,289.255z M204.255,356.289H92.596v-50.693h111.66
|
||||
V356.289z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M299.574,241.906h128c4.513,0,8.17-3.658,8.17-8.17c0-4.512-3.657-8.17-8.17-8.17h-128
|
||||
c-4.513,0-8.17,3.658-8.17,8.17C291.404,238.248,295.061,241.906,299.574,241.906z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M299.574,274.587h128c4.513,0,8.17-3.658,8.17-8.17c0-4.512-3.657-8.17-8.17-8.17h-128
|
||||
c-4.513,0-8.17,3.658-8.17,8.17C291.404,270.929,295.061,274.587,299.574,274.587z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M299.574,307.268h128c4.513,0,8.17-3.658,8.17-8.17s-3.657-8.17-8.17-8.17h-128
|
||||
c-4.513,0-8.17,3.658-8.17,8.17S295.061,307.268,299.574,307.268z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M299.574,339.948h128c4.513,0,8.17-3.658,8.17-8.17s-3.657-8.17-8.17-8.17h-128
|
||||
c-4.513,0-8.17,3.658-8.17,8.17S295.061,339.948,299.574,339.948z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M299.574,372.629h64c4.513,0,8.17-3.658,8.17-8.17c0-4.512-3.657-8.17-8.17-8.17h-64
|
||||
c-4.513,0-8.17,3.658-8.17,8.17C291.404,368.971,295.061,372.629,299.574,372.629z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
<path
|
||||
d="M299.574,192.885c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17
|
||||
c11.714,0,21.543-2.95,30.036-7.774c0.43-0.204,0.84-0.44,1.226-0.712c13.658-8.171,23.803-21.223,32.739-34.574
|
||||
c8.933,13.346,19.073,26.393,32.723,34.564c0.398,0.282,0.821,0.528,1.268,0.736c8.486,4.814,18.308,7.758,30.01,7.758
|
||||
c4.513,0,8.17-3.658,8.17-8.17s-3.657-8.17-8.17-8.17c-6.915,0-12.958-1.35-18.383-3.759v-75.856
|
||||
c5.425-2.41,11.468-3.759,18.383-3.759c4.513,0,8.17-3.658,8.17-8.17c0-4.512-3.657-8.17-8.17-8.17
|
||||
c-11.706,0-21.531,2.947-30.021,7.764c-0.439,0.207-0.858,0.449-1.251,0.727c-13.654,8.171-23.795,21.22-32.729,34.568
|
||||
c-8.938-13.353-19.083-26.406-32.745-34.577c-0.38-0.268-0.784-0.501-1.208-0.702c-8.495-4.827-18.327-7.779-30.047-7.779
|
||||
c-4.513,0-8.17,3.658-8.17,8.17c0,4.512,3.657,8.17,8.17,8.17c6.915,0,12.958,1.35,18.383,3.759v75.856
|
||||
C312.532,191.535,306.49,192.885,299.574,192.885z M392.851,125.053v52.288c-7.034-7.18-13.2-16.294-19.562-26.144
|
||||
C379.651,141.347,385.817,132.232,392.851,125.053z M353.86,151.197c-6.363,9.85-12.528,18.965-19.562,26.144v-52.288
|
||||
C341.332,132.232,347.498,141.347,353.86,151.197z"
|
||||
fill="#3E0412"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import type { ResumeBadgeProps } from '../resume-badge';
|
||||
|
||||
export default function ResumeBadgeOwlIcon({ className }: ResumeBadgeProps) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
viewBox="0 0 511.988 511.988"
|
||||
x="0px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
y="0px">
|
||||
<g>
|
||||
<path
|
||||
d="M366.867,149.324c-5.891,0-10.672-4.766-10.672-10.656s4.781-10.672,10.672-10.672
|
||||
c16.781,0,23.312-4.469,25.719-7.141c1.562-1.718,2.062-3.515,2.062-3.53c0-5.891,4.781-10.656,10.688-10.656
|
||||
c5.875,0,10.656,4.766,10.656,10.656c0,1.578-0.375,9.843-7.562,17.812C399.961,144.557,385.961,149.324,366.867,149.324z"
|
||||
fill="#434A54"
|
||||
/>
|
||||
<path
|
||||
d="M145.122,127.995c-11.554,0-20.382-2.234-24.866-6.281c-2.438-2.202-2.859-4.296-2.93-4.765
|
||||
c0-0.031,0.008-0.062,0.008-0.078c0-5.891-4.781-10.672-10.672-10.672c-5.89,0-10.664,4.781-10.664,10.672
|
||||
c0,0.156,0.016,0.297,0.023,0.453h-0.023c0,1.578,0.367,9.843,7.555,17.812c8.484,9.422,22.476,14.188,41.569,14.188
|
||||
c5.891,0,10.672-4.766,10.672-10.656S151.013,127.995,145.122,127.995z M117.334,117.325h-0.023c0-0.109,0.016-0.234,0.016-0.344
|
||||
C117.342,117.2,117.334,117.325,117.334,117.325z"
|
||||
fill="#434A54"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M255.995,63.998c-76.459,0-138.661,62.201-138.661,138.669c0,17.906,5.039,42.093,14.57,69.936
|
||||
c8.781,25.641,20.773,52.921,34.687,78.936c14.227,26.578,29.188,49.39,43.273,65.969c17.437,20.515,32.522,30.483,46.131,30.483
|
||||
s28.687-9.969,46.124-30.483c14.094-16.579,29.062-39.391,43.28-65.969c13.905-26.015,25.905-53.295,34.687-78.936
|
||||
c9.531-27.843,14.562-52.029,14.562-69.936C394.648,126.199,332.462,63.998,255.995,63.998z"
|
||||
fill="#A85D5D"
|
||||
/>
|
||||
<path
|
||||
d="M501.334,362.663H10.664C4.774,362.663,0,357.882,0,351.991c0-5.89,4.773-10.671,10.664-10.671
|
||||
h490.669c5.874,0,10.655,4.781,10.655,10.671C511.989,357.882,507.208,362.663,501.334,362.663z"
|
||||
fill="#FFD2A6"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
d="M213.332,362.663c-5.891,0-10.672-4.781-10.672-10.672V341.32c0-5.891,4.781-10.656,10.672-10.656
|
||||
c5.89,0,10.664,4.766,10.664,10.656v10.671C223.996,357.882,219.223,362.663,213.332,362.663z"
|
||||
fill="#F6BB42"
|
||||
/>
|
||||
<path
|
||||
d="M234.66,362.663c-5.891,0-10.664-4.781-10.664-10.672V341.32c0-5.891,4.773-10.656,10.664-10.656
|
||||
c5.89,0,10.671,4.766,10.671,10.656v10.671C245.331,357.882,240.55,362.663,234.66,362.663z"
|
||||
fill="#F6BB42"
|
||||
/>
|
||||
<path
|
||||
d="M277.338,362.663c-5.897,0-10.679-4.781-10.679-10.672V341.32c0-5.891,4.781-10.656,10.679-10.656
|
||||
c5.875,0,10.656,4.766,10.656,10.656v10.671C287.994,357.882,283.213,362.663,277.338,362.663z"
|
||||
fill="#F6BB42"
|
||||
/>
|
||||
<path
|
||||
d="M298.65,362.663c-5.875,0-10.656-4.781-10.656-10.672V341.32c0-5.891,4.781-10.656,10.656-10.656
|
||||
c5.906,0,10.688,4.766,10.688,10.656v10.671C309.338,357.882,304.557,362.663,298.65,362.663z"
|
||||
fill="#F6BB42"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M255.995,234.665c-5.89,0-10.664-4.781-10.664-10.671v-21.328c0-5.891,4.773-10.672,10.664-10.672
|
||||
c5.891,0,10.664,4.781,10.664,10.672v21.328C266.659,229.885,261.886,234.665,255.995,234.665z"
|
||||
fill="#FFCE54"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
d="M387.914,159.947c-18.047-55.623-70.357-95.95-131.919-95.95
|
||||
c-61.295,0-113.419,39.983-131.685,95.231l0.148,0.766c35.375-71.326,131.537-42.67,131.537,21.328
|
||||
C255.995,117.34,352.523,88.684,387.914,159.947z"
|
||||
fill="#7F4545"
|
||||
/>
|
||||
<path
|
||||
d="M255.995,383.99c-5.89,0-10.664,4.781-10.664,10.672v51.391c3.656,1.297,7.211,1.938,10.664,1.938
|
||||
s7.008-0.641,10.664-1.938v-51.391C266.659,388.771,261.886,383.99,255.995,383.99z"
|
||||
fill="#7F4545"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M255.995,63.998c-1.828,0-3.656,0.031-5.468,0.109
|
||||
c74.061,2.75,133.465,63.842,133.465,138.56c0,17.906-5.031,42.093-14.578,69.936c-8.766,25.641-20.765,52.921-34.687,78.936
|
||||
c-14.218,26.578-29.187,49.39-43.265,65.969c-15.188,17.874-28.601,27.733-40.803,29.983c1.805,0.328,3.586,0.5,5.335,0.5
|
||||
c13.609,0,28.687-9.969,46.124-30.483c14.094-16.579,29.062-39.391,43.28-65.969c13.905-26.015,25.905-53.295,34.687-78.936
|
||||
c9.531-27.843,14.562-52.029,14.562-69.936C394.648,126.199,332.462,63.998,255.995,63.998z"
|
||||
fill="#FFFFFF"
|
||||
opacity={0.1}
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
d="M219.98,193.322c0,15.094-13.187,27.344-29.444,27.344c-16.266,0-29.445-12.25-29.445-27.344
|
||||
s13.179-27.328,29.445-27.328C206.793,165.995,219.98,178.229,219.98,193.322z"
|
||||
fill="#F6BB42"
|
||||
/>
|
||||
<path
|
||||
d="M344.649,191.214c0,14.672-12.297,26.562-27.452,26.562c-15.156,0-27.453-11.891-27.453-26.562
|
||||
c0-14.656,12.297-26.547,27.453-26.547C332.352,164.667,344.649,176.557,344.649,191.214z"
|
||||
fill="#F6BB42"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M191.997,181.322c-5.891,0-10.664,4.781-10.664,10.672s4.773,10.672,10.664,10.672
|
||||
s10.664-4.781,10.664-10.672S197.888,181.322,191.997,181.322z"
|
||||
fill="#434A54"
|
||||
/>
|
||||
<path
|
||||
d="M191.997,149.324c-23.523,0-42.664,19.14-42.664,42.671s19.14,42.671,42.664,42.671
|
||||
c23.523,0,42.663-19.14,42.663-42.671S215.52,149.324,191.997,149.324z M191.997,213.322c-11.766,0-21.336-9.562-21.336-21.328
|
||||
s9.57-21.328,21.336-21.328c11.765,0,21.335,9.562,21.335,21.328S203.762,213.322,191.997,213.322z"
|
||||
fill="#FFCE54"
|
||||
/>
|
||||
<path
|
||||
d="M319.994,181.322c-5.891,0-10.656,4.781-10.656,10.672s4.766,10.672,10.656,10.672
|
||||
s10.656-4.781,10.656-10.672S325.885,181.322,319.994,181.322z"
|
||||
fill="#434A54"
|
||||
/>
|
||||
<path
|
||||
d="M319.994,149.324c-23.531,0-42.656,19.14-42.656,42.671s19.125,42.671,42.656,42.671
|
||||
c23.53,0,42.654-19.14,42.654-42.671S343.524,149.324,319.994,149.324z M319.994,213.322c-11.766,0-21.344-9.562-21.344-21.328
|
||||
s9.578-21.328,21.344-21.328s21.343,9.562,21.343,21.328S331.76,213.322,319.994,213.322z"
|
||||
fill="#FFCE54"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { ResumeBadgeProps } from '../resume-badge';
|
||||
|
||||
export default function ResumeBadgeSageIcon({ className }: ResumeBadgeProps) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
viewBox="0 0 512 512"
|
||||
x="0px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
y="0px">
|
||||
<g>
|
||||
<path
|
||||
d="M252.356,4.59c-2.05-4.1-7.035-5.762-11.14-3.713c-4.1,2.051-5.763,7.037-3.713,11.14l16.605,33.211
|
||||
l14.852-7.427L252.356,4.59z"
|
||||
fill="#FFF3D4"
|
||||
/>
|
||||
<path
|
||||
d="M219.145,4.59c-2.051-4.1-7.035-5.762-11.14-3.713c-4.1,2.051-5.763,7.037-3.713,11.14
|
||||
l18.714,37.429l14.852-7.427L219.145,4.59z"
|
||||
fill="#FFF3D4"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M106.661,141.146h-0.111c-18.342,0-33.211,14.868-33.211,33.211s14.868,33.211,33.211,33.211h149.448
|
||||
v-66.421C255.999,141.146,106.661,141.146,106.661,141.146z"
|
||||
fill="#FFCDC1"
|
||||
/>
|
||||
<path
|
||||
d="M405.448,141.146h-0.111H256v66.421h149.448c18.342,0,33.211-14.868,33.211-33.211
|
||||
S423.789,141.146,405.448,141.146z"
|
||||
fill="#FFAB97"
|
||||
/>
|
||||
<path
|
||||
d="M482.94,334.876c0-24.455-19.825-44.281-44.281-44.281l-33.211,94.097l33.211,94.097h44.281V334.876z
|
||||
"
|
||||
fill="#7F7774"
|
||||
/>
|
||||
<path
|
||||
d="M73.341,290.595c-24.456,0-44.281,19.826-44.281,44.281v143.913h409.599V290.595H73.341z"
|
||||
fill="#A99E9B"
|
||||
/>
|
||||
<path
|
||||
d="M386.948,74.725h-44.281L372.237,512c18.342,0,33.211-14.868,33.211-33.211V146.682
|
||||
C405.448,120.611,398.731,96.084,386.948,74.725z"
|
||||
fill="#FFEAB2"
|
||||
/>
|
||||
<path
|
||||
d="M342.667,74.725H125.051c-11.783,21.359-18.501,45.886-18.501,71.957v332.107
|
||||
c0,18.342,14.868,33.211,33.211,33.211c12.507,0,23.396-6.918,29.059-17.132C174.484,505.082,185.373,512,197.88,512
|
||||
s23.396-6.918,29.059-17.132C232.603,505.082,243.492,512,255.999,512c12.507,0,23.396-6.918,29.059-17.132
|
||||
C290.722,505.082,301.61,512,314.118,512s23.396-6.918,29.059-17.132C348.84,505.082,359.729,512,372.236,512V146.682
|
||||
L342.667,74.725z"
|
||||
fill="#FFF3D4"
|
||||
/>
|
||||
<path
|
||||
d="M256,30.444l83.027,149.448h33.211v-33.211C372.237,82.485,320.195,30.444,256,30.444z"
|
||||
fill="#FFAB97"
|
||||
/>
|
||||
<path
|
||||
d="M256,30.444c-64.196,0-116.238,52.041-116.238,116.238v33.211h199.264v-33.211
|
||||
C339.027,82.485,301.854,30.444,256,30.444z"
|
||||
fill="#FFCDC1"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
d="M172.973,298.897c-4.586,0-8.303-3.716-8.303-8.303v-11.07c0-4.586,3.716-8.303,8.303-8.303
|
||||
s8.303,3.716,8.303,8.303v11.07C181.276,295.181,177.558,298.897,172.973,298.897z"
|
||||
fill="#FFD159"
|
||||
/>
|
||||
<path
|
||||
d="M339.027,387.459c-4.586,0-8.303-3.716-8.303-8.303v-11.07c0-4.586,3.716-8.303,8.303-8.303
|
||||
c4.586,0,8.303,3.716,8.303,8.303v11.07C347.329,383.743,343.612,387.459,339.027,387.459z"
|
||||
fill="#FFD159"
|
||||
/>
|
||||
<path
|
||||
d="M305.816,420.67c-4.586,0-8.303-3.716-8.303-8.303v-11.07c0-4.586,3.716-8.303,8.303-8.303
|
||||
c4.586,0,8.303,3.716,8.303,8.303v11.07C314.119,416.954,310.401,420.67,305.816,420.67z"
|
||||
fill="#FFD159"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M256,234.297c-11.69,0-22.174-7.399-26.088-18.412c-1.537-4.321,0.721-9.069,5.042-10.604
|
||||
c4.315-1.54,9.068,0.72,10.603,5.041c1.568,4.408,5.764,7.369,10.444,7.369c4.679,0,8.876-2.961,10.444-7.369
|
||||
c1.535-4.32,6.285-6.58,10.603-5.041c4.321,1.535,6.578,6.283,5.042,10.604C278.174,226.898,267.689,234.297,256,234.297z"
|
||||
fill="#E26142"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
d="M225.003,154.984c-3.616,0-6.691-2.31-7.83-5.535h-10.989c-4.586,0-8.303-3.716-8.303-8.303
|
||||
c0-4.586,3.716-8.303,8.303-8.303h18.819c4.586,0,8.303,3.716,8.303,8.303v5.535C233.306,151.268,229.588,154.984,225.003,154.984z
|
||||
"
|
||||
fill="#554F4E"
|
||||
/>
|
||||
<path
|
||||
d="M286.997,154.984c-4.586,0-8.303-3.716-8.303-8.303v-5.535c0-4.586,3.716-8.303,8.303-8.303h18.819
|
||||
c4.586,0,8.303,3.716,8.303,8.303c0,4.586-3.716,8.303-8.303,8.303h-10.989C293.686,152.674,290.611,154.984,286.997,154.984z"
|
||||
fill="#554F4E"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { BadgeIcon } from './resumeBadgeConstants';
|
||||
|
||||
type Props = Readonly<{
|
||||
description: string;
|
||||
icon: BadgeIcon;
|
||||
title: string;
|
||||
}>;
|
||||
|
||||
export default function ResumeUserBadge({
|
||||
description,
|
||||
icon: Icon,
|
||||
title,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="group relative flex items-center justify-center">
|
||||
<div
|
||||
className="absolute -top-3 hidden w-48 -translate-y-full flex-col
|
||||
justify-center gap-1 rounded-lg bg-white px-2 py-2 text-center drop-shadow-xl
|
||||
after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2
|
||||
after:border-8 after:border-x-transparent after:border-b-transparent
|
||||
after:border-t-white after:drop-shadow-lg after:content-['']
|
||||
group-hover:flex">
|
||||
<Icon className="h-12 w-12 self-center" />
|
||||
<p className="font-medium">{title}</p>
|
||||
<p className="text-sm">{description}.</p>
|
||||
</div>
|
||||
<Icon className="h-4 w-4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import type { BadgePayload } from './resumeBadgeConstants';
|
||||
import { RESUME_USER_BADGES } from './resumeBadgeConstants';
|
||||
import ResumeUserBadge from './ResumeUserBadge';
|
||||
|
||||
type Props = Readonly<{
|
||||
userId: string;
|
||||
}>;
|
||||
|
||||
export default function ResumeUserBadges({ userId }: Props) {
|
||||
const userReviewedResumeCountQuery = trpc.useQuery([
|
||||
'resumes.resume.findUserReviewedResumeCount',
|
||||
{ userId },
|
||||
]);
|
||||
const userMaxResumeUpvoteCountQuery = trpc.useQuery([
|
||||
'resumes.resume.findUserMaxResumeUpvoteCount',
|
||||
{ userId },
|
||||
]);
|
||||
const userTopUpvotedCommentCountQuery = trpc.useQuery([
|
||||
'resumes.resume.findUserTopUpvotedCommentCount',
|
||||
{ userId },
|
||||
]);
|
||||
|
||||
const payload: BadgePayload = {
|
||||
maxResumeUpvoteCount: userMaxResumeUpvoteCountQuery.data ?? 0,
|
||||
reviewedResumesCount: userReviewedResumeCountQuery.data ?? 0,
|
||||
topUpvotedCommentCount: userTopUpvotedCommentCountQuery.data ?? 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
{RESUME_USER_BADGES.filter((badge) => badge.isValid(payload)).map(
|
||||
(badge) => (
|
||||
<ResumeUserBadge
|
||||
key={badge.id}
|
||||
description={badge.description}
|
||||
icon={badge.icon}
|
||||
title={badge.title}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import ResumeBadgeCoolIcon from '../badgeIcons/popularResumes/ResumeBadgeCoolIcon';
|
||||
import ResumeBadgeRocketIcon from '../badgeIcons/popularResumes/ResumeBadgeRocketIcon';
|
||||
import ResumeBadgeTreasureIcon from '../badgeIcons/popularResumes/ResumeBadgeTreasureIcon';
|
||||
import ResumeBadgeDetectiveIcon from '../badgeIcons/reviewer/ResumeBadgeDetectiveIcon';
|
||||
import ResumeBadgeEagleIcon from '../badgeIcons/reviewer/ResumeBadgeEagleIcon';
|
||||
import ResumeBadgeSuperheroIcon from '../badgeIcons/reviewer/ResumeBadgeSuperheroIcon';
|
||||
import ResumeBadgeBookIcon from '../badgeIcons/topComment/ResumeBadgeBookIcon';
|
||||
import ResumeBadgeOwlIcon from '../badgeIcons/topComment/ResumeBadgeOwlIcon';
|
||||
import ResumeBadgeSageIcon from '../badgeIcons/topComment/ResumeBadgeSageIcon';
|
||||
|
||||
export type BadgeIcon = (
|
||||
props: React.ComponentProps<typeof ResumeBadgeDetectiveIcon>,
|
||||
) => JSX.Element;
|
||||
|
||||
export type BadgeInfo = {
|
||||
description: string;
|
||||
icon: BadgeIcon;
|
||||
id: string;
|
||||
isValid: (payload: BadgePayload) => boolean;
|
||||
title: string;
|
||||
};
|
||||
|
||||
// TODO: Add other badges in
|
||||
export type BadgePayload = {
|
||||
maxResumeUpvoteCount: number;
|
||||
reviewedResumesCount: number;
|
||||
topUpvotedCommentCount: number;
|
||||
};
|
||||
|
||||
const TIER_THREE = 20;
|
||||
const TIER_TWO = 10;
|
||||
const TIER_ONE = 5;
|
||||
|
||||
export const RESUME_USER_BADGES: Array<BadgeInfo> = [
|
||||
{
|
||||
description: `Reviewed over ${TIER_THREE} resumes`,
|
||||
icon: ResumeBadgeSuperheroIcon,
|
||||
id: 'Superhero',
|
||||
isValid: (payload: BadgePayload) =>
|
||||
payload.reviewedResumesCount >= TIER_THREE,
|
||||
title: 'True saviour of the people',
|
||||
},
|
||||
{
|
||||
description: `Reviewed over ${TIER_TWO} resumes`,
|
||||
icon: ResumeBadgeDetectiveIcon,
|
||||
id: 'Detective',
|
||||
isValid: (payload: BadgePayload) =>
|
||||
payload.reviewedResumesCount >= TIER_TWO &&
|
||||
payload.reviewedResumesCount < TIER_THREE,
|
||||
title: 'Keen eye for details like a private eye',
|
||||
},
|
||||
{
|
||||
description: `Reviewed over ${TIER_ONE} resumes`,
|
||||
icon: ResumeBadgeEagleIcon,
|
||||
id: 'Eagle',
|
||||
isValid: (payload: BadgePayload) =>
|
||||
payload.reviewedResumesCount >= TIER_ONE &&
|
||||
payload.reviewedResumesCount < TIER_TWO,
|
||||
title: 'As sharp as an eagle',
|
||||
},
|
||||
{
|
||||
description: `${TIER_THREE} upvotes on a resume`,
|
||||
icon: ResumeBadgeRocketIcon,
|
||||
id: 'Rocket',
|
||||
isValid: (payload: BadgePayload) =>
|
||||
payload.maxResumeUpvoteCount >= TIER_THREE,
|
||||
title: 'To the moon!',
|
||||
},
|
||||
{
|
||||
description: `${TIER_TWO} upvotes on a resume`,
|
||||
icon: ResumeBadgeTreasureIcon,
|
||||
id: 'Treasure',
|
||||
isValid: (payload: BadgePayload) =>
|
||||
payload.maxResumeUpvoteCount >= TIER_TWO &&
|
||||
payload.maxResumeUpvoteCount < TIER_THREE,
|
||||
title: "Can't get enough of this!",
|
||||
},
|
||||
{
|
||||
description: `${TIER_ONE} upvotes on a resume`,
|
||||
icon: ResumeBadgeCoolIcon,
|
||||
id: 'Cool',
|
||||
isValid: (payload: BadgePayload) =>
|
||||
payload.maxResumeUpvoteCount >= TIER_ONE &&
|
||||
payload.maxResumeUpvoteCount < TIER_TWO,
|
||||
title: 'Like the cool kids',
|
||||
},
|
||||
{
|
||||
description: `${TIER_THREE} top upvoted comment`,
|
||||
icon: ResumeBadgeSageIcon,
|
||||
id: 'Sage',
|
||||
isValid: (payload: BadgePayload) =>
|
||||
payload.topUpvotedCommentCount >= TIER_THREE,
|
||||
title: 'I am wisdom',
|
||||
},
|
||||
{
|
||||
description: `${TIER_TWO} top upvoted comment`,
|
||||
icon: ResumeBadgeBookIcon,
|
||||
id: 'Book',
|
||||
isValid: (payload: BadgePayload) =>
|
||||
payload.topUpvotedCommentCount >= TIER_TWO &&
|
||||
payload.topUpvotedCommentCount < TIER_THREE,
|
||||
title: 'The walking encyclopaedia',
|
||||
},
|
||||
{
|
||||
description: `${TIER_ONE} top upvoted comment`,
|
||||
icon: ResumeBadgeOwlIcon,
|
||||
id: 'Owl',
|
||||
isValid: (payload: BadgePayload) =>
|
||||
payload.topUpvotedCommentCount >= TIER_ONE &&
|
||||
payload.topUpvotedCommentCount < TIER_TWO,
|
||||
title: 'Wise as an owl',
|
||||
},
|
||||
];
|
||||
@@ -1,12 +1,22 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
type Props = Readonly<{
|
||||
isSelected: boolean;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
title: string;
|
||||
}>;
|
||||
|
||||
export default function ResumeFilterPill({ title, onClick }: Props) {
|
||||
export default function ResumeFilterPill({
|
||||
title,
|
||||
onClick,
|
||||
isSelected,
|
||||
}: Props) {
|
||||
return (
|
||||
<button
|
||||
className="rounded-xl border border-indigo-500 border-transparent bg-white px-2 py-1 text-xs font-medium text-indigo-500 focus:bg-indigo-500 focus:text-white"
|
||||
className={clsx(
|
||||
'rounded-xl border border-indigo-500 border-transparent px-2 py-1 text-xs font-medium focus:bg-indigo-500 focus:text-white',
|
||||
isSelected ? 'bg-indigo-500 text-white' : 'bg-white text-indigo-500',
|
||||
)}
|
||||
type="button"
|
||||
onClick={onClick}>
|
||||
{title}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
||||
import Link from 'next/link';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import type { UrlObject } from 'url';
|
||||
import { ChevronRightIcon } from '@heroicons/react/20/solid';
|
||||
import {
|
||||
AcademicCapIcon,
|
||||
BriefcaseIcon,
|
||||
ChevronRightIcon,
|
||||
StarIcon as ColouredStarIcon,
|
||||
} from '@heroicons/react/20/solid';
|
||||
import { ChatBubbleLeftIcon, StarIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import type { Resume } from '~/types/resume';
|
||||
|
||||
type Props = Readonly<{
|
||||
@@ -19,16 +16,7 @@ type Props = Readonly<{
|
||||
resumeInfo: Resume;
|
||||
}>;
|
||||
|
||||
export default function BrowseListItem({ href, resumeInfo }: Props) {
|
||||
const { data: sessionData } = useSession();
|
||||
|
||||
// Find out if user has starred this particular resume
|
||||
const resumeId = resumeInfo.id;
|
||||
const isStarredQuery = trpc.useQuery([
|
||||
'resumes.resume.user.isResumeStarred',
|
||||
{ resumeId },
|
||||
]);
|
||||
|
||||
export default function ResumeListItem({ href, resumeInfo }: Props) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<div className="grid grid-cols-8 gap-4 border-b border-slate-200 p-4 hover:bg-slate-100">
|
||||
@@ -53,22 +41,27 @@ export default function BrowseListItem({ href, resumeInfo }: Props) {
|
||||
<div className="mt-4 flex justify-start text-xs text-slate-500">
|
||||
<div className="flex gap-2 pr-4">
|
||||
<ChatBubbleLeftIcon className="w-4" />
|
||||
{resumeInfo.numComments} comments
|
||||
{`${resumeInfo.numComments} comment${
|
||||
resumeInfo.numComments === 1 ? '' : 's'
|
||||
}`}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{isStarredQuery.data && sessionData?.user ? (
|
||||
{resumeInfo.isStarredByUser ? (
|
||||
<ColouredStarIcon className="w-4 text-yellow-400" />
|
||||
) : (
|
||||
<StarIcon className="w-4" />
|
||||
)}
|
||||
{resumeInfo.numStars} stars
|
||||
{`${resumeInfo.numStars} star${
|
||||
resumeInfo.numStars === 1 ? '' : 's'
|
||||
}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-3 self-center text-sm text-slate-500">
|
||||
<div>
|
||||
Uploaded {formatDistanceToNow(resumeInfo.createdAt)} ago by{' '}
|
||||
{resumeInfo.user}
|
||||
{`Uploaded ${formatDistanceToNow(resumeInfo.createdAt, {
|
||||
addSuffix: true,
|
||||
})} by ${resumeInfo.user}`}
|
||||
</div>
|
||||
<div className="mt-2 text-slate-400">{resumeInfo.location}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Spinner } from '@tih/ui';
|
||||
|
||||
import ResumseListItem from './ResumeListItem';
|
||||
import ResumeListItem from './ResumeListItem';
|
||||
|
||||
import type { Resume } from '~/types/resume';
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function ResumeListItems({ isLoading, resumes }: Props) {
|
||||
<ul role="list">
|
||||
{resumes.map((resumeObj: Resume) => (
|
||||
<li key={resumeObj.id}>
|
||||
<ResumseListItem
|
||||
<ResumeListItem
|
||||
href={`/resumes/${resumeObj.id}`}
|
||||
resumeInfo={resumeObj}
|
||||
/>
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
export const BROWSE_TABS_VALUES = {
|
||||
ALL: 'all',
|
||||
MY: 'my',
|
||||
STARRED: 'starred',
|
||||
};
|
||||
|
||||
export type SortOrder = 'latest' | 'popular' | 'topComments';
|
||||
type SortOption = {
|
||||
name: string;
|
||||
value: SortOrder;
|
||||
};
|
||||
|
||||
export const SORT_OPTIONS: Array<SortOption> = [
|
||||
{ name: 'Latest', value: 'latest' },
|
||||
{ name: 'Popular', value: 'popular' },
|
||||
{ name: 'Top Comments', value: 'topComments' },
|
||||
];
|
||||
|
||||
export const TOP_HITS = [
|
||||
{ href: '#', name: 'Unreviewed' },
|
||||
{ href: '#', name: 'Fresh Grad' },
|
||||
{ href: '#', name: 'GOATs' },
|
||||
{ href: '#', name: 'US Only' },
|
||||
];
|
||||
|
||||
export type FilterOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const ROLE: Array<FilterOption> = [
|
||||
{
|
||||
label: 'Full-Stack Engineer',
|
||||
value: 'Full-Stack Engineer',
|
||||
},
|
||||
{ label: 'Frontend Engineer', value: 'Frontend Engineer' },
|
||||
{ label: 'Backend Engineer', value: 'Backend Engineer' },
|
||||
{ label: 'DevOps Engineer', value: 'DevOps Engineer' },
|
||||
{ label: 'iOS Engineer', value: 'iOS Engineer' },
|
||||
{ label: 'Android Engineer', value: 'Android Engineer' },
|
||||
];
|
||||
|
||||
export const EXPERIENCE: Array<FilterOption> = [
|
||||
{ label: 'Freshman', value: 'Freshman' },
|
||||
{ label: 'Sophomore', value: 'Sophomore' },
|
||||
{ label: 'Junior', value: 'Junior' },
|
||||
{ label: 'Senior', value: 'Senior' },
|
||||
{
|
||||
label: 'Entry Level (0 - 2 years)',
|
||||
value: 'Entry Level (0 - 2 years)',
|
||||
},
|
||||
{
|
||||
label: 'Mid Level (3 - 5 years)',
|
||||
value: 'Mid Level (3 - 5 years)',
|
||||
},
|
||||
{
|
||||
label: 'Senior Level (5+ years)',
|
||||
value: 'Senior Level (5+ years)',
|
||||
},
|
||||
];
|
||||
|
||||
export const LOCATION: Array<FilterOption> = [
|
||||
{ label: 'Singapore', value: 'Singapore' },
|
||||
{ label: 'United States', value: 'United States' },
|
||||
{ label: 'India', value: 'India' },
|
||||
];
|
||||
|
||||
export const TEST_RESUMES = [
|
||||
{
|
||||
createdAt: new Date(),
|
||||
experience: 'Fresh Grad (0-1 years)',
|
||||
numComments: 9,
|
||||
numStars: 1,
|
||||
role: 'Backend Engineer',
|
||||
title: 'Rejected from multiple companies, please help...:(',
|
||||
user: 'Git Ji Ra',
|
||||
},
|
||||
{
|
||||
createdAt: new Date(),
|
||||
experience: 'Fresh Grad (0-1 years)',
|
||||
numComments: 9,
|
||||
numStars: 1,
|
||||
role: 'Backend Engineer',
|
||||
title: 'Rejected from multiple companies, please help...:(',
|
||||
user: 'Git Ji Ra',
|
||||
},
|
||||
{
|
||||
createdAt: new Date(),
|
||||
experience: 'Fresh Grad (0-1 years)',
|
||||
numComments: 9,
|
||||
numStars: 1,
|
||||
role: 'Backend Engineer',
|
||||
title: 'Rejected from multiple companies, please help...:(',
|
||||
user: 'Git Ji Ra',
|
||||
},
|
||||
];
|
||||
151
apps/portal/src/components/resumes/browse/resumeFilters.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
export type FilterId = 'experience' | 'location' | 'role';
|
||||
|
||||
export type CustomFilter = {
|
||||
numComments: number;
|
||||
};
|
||||
|
||||
type RoleFilter =
|
||||
| 'Android Engineer'
|
||||
| 'Backend Engineer'
|
||||
| 'DevOps Engineer'
|
||||
| 'Frontend Engineer'
|
||||
| 'Full-Stack Engineer'
|
||||
| 'iOS Engineer';
|
||||
|
||||
type ExperienceFilter =
|
||||
| 'Entry Level (0 - 2 years)'
|
||||
| 'Freshman'
|
||||
| 'Junior'
|
||||
| 'Mid Level (3 - 5 years)'
|
||||
| 'Senior Level (5+ years)'
|
||||
| 'Senior'
|
||||
| 'Sophomore';
|
||||
|
||||
type LocationFilter = 'India' | 'Singapore' | 'United States';
|
||||
|
||||
export type FilterValue = ExperienceFilter | LocationFilter | RoleFilter;
|
||||
|
||||
export type FilterOption<T> = {
|
||||
label: string;
|
||||
value: T;
|
||||
};
|
||||
|
||||
export type Filter = {
|
||||
id: FilterId;
|
||||
label: string;
|
||||
options: Array<FilterOption<FilterValue>>;
|
||||
};
|
||||
|
||||
export type FilterState = Partial<CustomFilter> &
|
||||
Record<FilterId, Array<FilterValue>>;
|
||||
|
||||
export type SortOrder = 'latest' | 'popular' | 'topComments';
|
||||
|
||||
export type Shortcut = {
|
||||
customFilters?: CustomFilter;
|
||||
filters: FilterState;
|
||||
name: string;
|
||||
sortOrder: SortOrder;
|
||||
};
|
||||
|
||||
export const BROWSE_TABS_VALUES = {
|
||||
ALL: 'all',
|
||||
MY: 'my',
|
||||
STARRED: 'starred',
|
||||
};
|
||||
|
||||
export const SORT_OPTIONS: Record<string, string> = {
|
||||
latest: 'Latest',
|
||||
popular: 'Popular',
|
||||
topComments: 'Top Comments',
|
||||
};
|
||||
|
||||
export const ROLE: Array<FilterOption<RoleFilter>> = [
|
||||
{
|
||||
label: 'Full-Stack Engineer',
|
||||
value: 'Full-Stack Engineer',
|
||||
},
|
||||
{ label: 'Frontend Engineer', value: 'Frontend Engineer' },
|
||||
{ label: 'Backend Engineer', value: 'Backend Engineer' },
|
||||
{ label: 'DevOps Engineer', value: 'DevOps Engineer' },
|
||||
{ label: 'iOS Engineer', value: 'iOS Engineer' },
|
||||
{ label: 'Android Engineer', value: 'Android Engineer' },
|
||||
];
|
||||
|
||||
export const EXPERIENCE: Array<FilterOption<ExperienceFilter>> = [
|
||||
{ label: 'Freshman', value: 'Freshman' },
|
||||
{ label: 'Sophomore', value: 'Sophomore' },
|
||||
{ label: 'Junior', value: 'Junior' },
|
||||
{ label: 'Senior', value: 'Senior' },
|
||||
{
|
||||
label: 'Entry Level (0 - 2 years)',
|
||||
value: 'Entry Level (0 - 2 years)',
|
||||
},
|
||||
{
|
||||
label: 'Mid Level (3 - 5 years)',
|
||||
value: 'Mid Level (3 - 5 years)',
|
||||
},
|
||||
{
|
||||
label: 'Senior Level (5+ years)',
|
||||
value: 'Senior Level (5+ years)',
|
||||
},
|
||||
];
|
||||
|
||||
export const LOCATION: Array<FilterOption<LocationFilter>> = [
|
||||
{ label: 'Singapore', value: 'Singapore' },
|
||||
{ label: 'United States', value: 'United States' },
|
||||
{ label: 'India', value: 'India' },
|
||||
];
|
||||
|
||||
export const INITIAL_FILTER_STATE: FilterState = {
|
||||
experience: Object.values(EXPERIENCE).map(({ value }) => value),
|
||||
location: Object.values(LOCATION).map(({ value }) => value),
|
||||
role: Object.values(ROLE).map(({ value }) => value),
|
||||
};
|
||||
|
||||
export const SHORTCUTS: Array<Shortcut> = [
|
||||
{
|
||||
filters: INITIAL_FILTER_STATE,
|
||||
name: 'All',
|
||||
sortOrder: 'latest',
|
||||
},
|
||||
{
|
||||
filters: {
|
||||
...INITIAL_FILTER_STATE,
|
||||
numComments: 0,
|
||||
},
|
||||
name: 'Unreviewed',
|
||||
sortOrder: 'latest',
|
||||
},
|
||||
{
|
||||
filters: {
|
||||
...INITIAL_FILTER_STATE,
|
||||
experience: ['Entry Level (0 - 2 years)'],
|
||||
},
|
||||
name: 'Fresh Grad',
|
||||
sortOrder: 'latest',
|
||||
},
|
||||
{
|
||||
filters: INITIAL_FILTER_STATE,
|
||||
name: 'GOATs',
|
||||
sortOrder: 'popular',
|
||||
},
|
||||
{
|
||||
filters: {
|
||||
...INITIAL_FILTER_STATE,
|
||||
location: ['United States'],
|
||||
},
|
||||
name: 'US Only',
|
||||
sortOrder: 'latest',
|
||||
},
|
||||
];
|
||||
|
||||
export const isInitialFilterState = (filters: FilterState) =>
|
||||
Object.keys(filters).every((filter) => {
|
||||
if (!['experience', 'location', 'role'].includes(filter)) {
|
||||
return true;
|
||||
}
|
||||
return INITIAL_FILTER_STATE[filter as FilterId].every((value) =>
|
||||
filters[filter as FilterId].includes(value),
|
||||
);
|
||||
});
|
||||
@@ -1,16 +1,19 @@
|
||||
import {
|
||||
ArrowDownCircleIcon,
|
||||
ArrowUpCircleIcon,
|
||||
} from '@heroicons/react/20/solid';
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { ChevronUpIcon } from '@heroicons/react/20/solid';
|
||||
import { FaceSmileIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
import ResumeCommentEditForm from './comment/ResumeCommentEditForm';
|
||||
import ResumeCommentReplyForm from './comment/ResumeCommentReplyForm';
|
||||
import ResumeCommentVoteButtons from './comment/ResumeCommentVoteButtons';
|
||||
import ResumeUserBadges from '../badges/ResumeUserBadges';
|
||||
import ResumeExpandableText from '../shared/ResumeExpandableText';
|
||||
|
||||
import type { ResumeComment } from '~/types/resume-comments';
|
||||
|
||||
type ResumeCommentListItemProps = {
|
||||
comment: ResumeComment;
|
||||
userId?: string;
|
||||
userId: string | undefined;
|
||||
};
|
||||
|
||||
export default function ResumeCommentListItem({
|
||||
@@ -18,34 +21,57 @@ export default function ResumeCommentListItem({
|
||||
userId,
|
||||
}: ResumeCommentListItemProps) {
|
||||
const isCommentOwner = userId === comment.user.userId;
|
||||
const [isEditingComment, setIsEditingComment] = useState(false);
|
||||
const [isReplyingComment, setIsReplyingComment] = useState(false);
|
||||
const [showReplies, setShowReplies] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="border-primary-300 w-3/4 rounded-md border-2 bg-white p-2 drop-shadow-md">
|
||||
<div className="flex w-full flex-row space-x-2 p-1 align-top">
|
||||
<div
|
||||
className={clsx(
|
||||
'min-w-fit rounded-md bg-white ',
|
||||
!comment.parentId &&
|
||||
'w-11/12 border-2 border-indigo-300 p-2 drop-shadow-md',
|
||||
)}>
|
||||
<div className="flex flex-row space-x-2 p-1 align-top">
|
||||
{/* Image Icon */}
|
||||
{comment.user.image ? (
|
||||
<img
|
||||
alt={comment.user.name ?? 'Reviewer'}
|
||||
className="mt-1 h-8 w-8 rounded-full"
|
||||
className={clsx(
|
||||
'mt-1 rounded-full',
|
||||
comment.parentId ? 'h-6 w-6' : 'h-8 w-8 ',
|
||||
)}
|
||||
src={comment.user.image!}
|
||||
/>
|
||||
) : (
|
||||
<FaceSmileIcon className="h-8 w-8 rounded-full" />
|
||||
<FaceSmileIcon
|
||||
className={clsx(
|
||||
'mt-1 rounded-full',
|
||||
comment.parentId ? 'h-6 w-6' : 'h-8 w-8 ',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex w-full flex-col space-y-1">
|
||||
{/* Name and creation time */}
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
<div className="font-medium">
|
||||
<p
|
||||
className={clsx(
|
||||
'font-medium text-black',
|
||||
!!comment.parentId && 'text-sm',
|
||||
)}>
|
||||
{comment.user.name ?? 'Reviewer ABC'}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<div className="text-primary-800 text-xs font-medium">
|
||||
<p className="text-xs font-medium text-indigo-800">
|
||||
{isCommentOwner ? '(Me)' : ''}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<ResumeUserBadges userId={comment.user.userId} />
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-600">
|
||||
<div className="px-2 text-xs text-gray-600">
|
||||
{comment.createdAt.toLocaleString('en-US', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
@@ -54,22 +80,93 @@ export default function ResumeCommentListItem({
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<ResumeExpandableText>{comment.description}</ResumeExpandableText>
|
||||
{isEditingComment ? (
|
||||
<ResumeCommentEditForm
|
||||
comment={comment}
|
||||
setIsEditingComment={setIsEditingComment}
|
||||
/>
|
||||
) : (
|
||||
<ResumeExpandableText
|
||||
key={comment.description}
|
||||
text={comment.description}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Upvote and edit */}
|
||||
<div className="flex flex-row space-x-1 pt-1 align-middle">
|
||||
{/* TODO: Implement upvote */}
|
||||
<ArrowUpCircleIcon className="h-4 w-4 fill-gray-400" />
|
||||
<div className="text-xs">{comment.numVotes}</div>
|
||||
<ArrowDownCircleIcon className="h-4 w-4 fill-gray-400" />
|
||||
<ResumeCommentVoteButtons commentId={comment.id} userId={userId} />
|
||||
|
||||
{/* TODO: Implement edit */}
|
||||
{isCommentOwner ? (
|
||||
<div className="text-primary-800 hover:text-primary-400 px-1 text-xs">
|
||||
Edit
|
||||
</div>
|
||||
) : null}
|
||||
{/* Action buttons; only present for authenticated user when not editing/replying */}
|
||||
{userId && !isEditingComment && !isReplyingComment && (
|
||||
<>
|
||||
{isCommentOwner && (
|
||||
<button
|
||||
className="px-1 text-xs text-indigo-800 hover:text-indigo-400"
|
||||
type="button"
|
||||
onClick={() => setIsEditingComment(true)}>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!comment.parentId && (
|
||||
<button
|
||||
className="px-1 text-xs text-indigo-800 hover:text-indigo-400"
|
||||
type="button"
|
||||
onClick={() => setIsReplyingComment(true)}>
|
||||
Reply
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reply Form */}
|
||||
{isReplyingComment && (
|
||||
<ResumeCommentReplyForm
|
||||
parentId={comment.id}
|
||||
resumeId={comment.resumeId}
|
||||
section={comment.section}
|
||||
setIsReplyingComment={setIsReplyingComment}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Replies */}
|
||||
{comment.children.length > 0 && (
|
||||
<div className="min-w-fit space-y-1 pt-2">
|
||||
<button
|
||||
className="flex items-center space-x-1 rounded-md text-xs font-medium text-indigo-800 hover:text-indigo-300"
|
||||
type="button"
|
||||
onClick={() => setShowReplies(!showReplies)}>
|
||||
<ChevronUpIcon
|
||||
className={clsx(
|
||||
'h-5 w-5 ',
|
||||
!showReplies && 'rotate-180 transform',
|
||||
)}
|
||||
/>
|
||||
<span>{showReplies ? 'Hide replies' : 'Show replies'}</span>
|
||||
</button>
|
||||
|
||||
{showReplies && (
|
||||
<div className="flex flex-row">
|
||||
<div className="relative flex flex-col px-2 py-2">
|
||||
<div className="flex-grow border-r border-gray-300" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
{comment.children.map((child) => {
|
||||
return (
|
||||
<ResumeCommentListItem
|
||||
key={child.id}
|
||||
comment={child}
|
||||
userId={userId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,6 +47,9 @@ export default function ResumeCommentsForm({
|
||||
onSuccess: () => {
|
||||
// New Comment added, invalidate query to trigger refetch
|
||||
trpcContext.invalidateQueries(['resumes.comments.list']);
|
||||
trpcContext.invalidateQueries(['resumes.resume.findAll']);
|
||||
trpcContext.invalidateQueries(['resumes.resume.user.findUserStarred']);
|
||||
trpcContext.invalidateQueries(['resumes.resume.user.findUserCreated']);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useState } from 'react';
|
||||
import { Spinner, Tabs } from '@tih/ui';
|
||||
import {
|
||||
BookOpenIcon,
|
||||
BriefcaseIcon,
|
||||
CodeBracketSquareIcon,
|
||||
FaceSmileIcon,
|
||||
IdentificationIcon,
|
||||
SparklesIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { ResumesSection } from '@prisma/client';
|
||||
import { Spinner } from '@tih/ui';
|
||||
import { Button } from '@tih/ui';
|
||||
|
||||
import { trpc } from '~/utils/trpc';
|
||||
@@ -21,23 +29,26 @@ export default function ResumeCommentsList({
|
||||
setShowCommentsForm,
|
||||
}: ResumeCommentsListProps) {
|
||||
const { data: sessionData } = useSession();
|
||||
const [tab, setTab] = useState(RESUME_COMMENTS_SECTIONS[0].value);
|
||||
const [tabs, setTabs] = useState(RESUME_COMMENTS_SECTIONS);
|
||||
|
||||
const commentsQuery = trpc.useQuery(['resumes.comments.list', { resumeId }], {
|
||||
onSuccess: (data: Array<ResumeComment>) => {
|
||||
const updatedTabs = RESUME_COMMENTS_SECTIONS.map(({ label, value }) => {
|
||||
const count = data.filter(({ section }) => section === value).length;
|
||||
const updatedLabel = count > 0 ? `${label} (${count})` : label;
|
||||
return {
|
||||
label: updatedLabel,
|
||||
value,
|
||||
};
|
||||
});
|
||||
const commentsQuery = trpc.useQuery(['resumes.comments.list', { resumeId }]);
|
||||
|
||||
setTabs(updatedTabs);
|
||||
},
|
||||
});
|
||||
const renderIcon = (section: ResumesSection) => {
|
||||
const className = 'h-7 w-7';
|
||||
switch (section) {
|
||||
case ResumesSection.GENERAL:
|
||||
return <IdentificationIcon className={className} />;
|
||||
case ResumesSection.EDUCATION:
|
||||
return <BookOpenIcon className={className} />;
|
||||
case ResumesSection.EXPERIENCE:
|
||||
return <BriefcaseIcon className={className} />;
|
||||
case ResumesSection.PROJECTS:
|
||||
return <CodeBracketSquareIcon className={className} />;
|
||||
case ResumesSection.SKILLS:
|
||||
return <SparklesIcon className={className} />;
|
||||
default:
|
||||
return <FaceSmileIcon className={className} />;
|
||||
}
|
||||
};
|
||||
|
||||
const renderButton = () => {
|
||||
if (sessionData === null) {
|
||||
@@ -45,6 +56,7 @@ export default function ResumeCommentsList({
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
className="-mb-2"
|
||||
display="block"
|
||||
label="Add your review"
|
||||
variant="tertiary"
|
||||
@@ -57,28 +69,44 @@ export default function ResumeCommentsList({
|
||||
<div className="space-y-3">
|
||||
{renderButton()}
|
||||
|
||||
<Tabs
|
||||
label="comments"
|
||||
tabs={tabs}
|
||||
value={tab}
|
||||
onChange={(value) => setTab(value)}
|
||||
/>
|
||||
|
||||
{commentsQuery.isFetching ? (
|
||||
{commentsQuery.isLoading ? (
|
||||
<div className="col-span-10 pt-4">
|
||||
<Spinner display="block" size="lg" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="m-2 flow-root h-[calc(100vh-20rem)] w-full flex-col space-y-3 overflow-y-auto">
|
||||
{(commentsQuery.data?.filter((c) => c.section === tab) ?? []).map(
|
||||
(comment) => (
|
||||
<ResumeCommentListItem
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
userId={sessionData?.user?.id}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<div className="scrollbar-hide m-2 flow-root h-[calc(100vh-20rem)] w-full flex-col space-y-4 overflow-y-auto overflow-x-hidden pt-14 pb-6">
|
||||
{RESUME_COMMENTS_SECTIONS.map(({ label, value }) => {
|
||||
const comments = commentsQuery.data
|
||||
? commentsQuery.data.filter((comment: ResumeComment) => {
|
||||
return (comment.section as string) === value;
|
||||
})
|
||||
: [];
|
||||
const commentCount = comments.length;
|
||||
|
||||
return (
|
||||
<div key={value} className="mb-4 space-y-4">
|
||||
<div className="flex flex-row items-center space-x-2 text-indigo-800">
|
||||
{renderIcon(value)}
|
||||
|
||||
<div className="w-fit text-lg font-medium">{label}</div>
|
||||
</div>
|
||||
|
||||
{commentCount > 0 ? (
|
||||
comments.map((comment) => {
|
||||
return (
|
||||
<ResumeCommentListItem
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
userId={sessionData?.user?.id}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div>There are no comments for this section yet!</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Button, TextArea } from '@tih/ui';
|
||||
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import type { ResumeComment } from '~/types/resume-comments';
|
||||
|
||||
type ResumeCommentEditFormProps = {
|
||||
comment: ResumeComment;
|
||||
setIsEditingComment: (value: boolean) => void;
|
||||
};
|
||||
|
||||
type ICommentInput = {
|
||||
description: string;
|
||||
};
|
||||
|
||||
export default function ResumeCommentEditForm({
|
||||
comment,
|
||||
setIsEditingComment,
|
||||
}: ResumeCommentEditFormProps) {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors, isDirty },
|
||||
reset,
|
||||
} = useForm<ICommentInput>({
|
||||
defaultValues: {
|
||||
description: comment.description,
|
||||
},
|
||||
});
|
||||
|
||||
const trpcContext = trpc.useContext();
|
||||
const commentUpdateMutation = trpc.useMutation(
|
||||
'resumes.comments.user.update',
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Comment updated, invalidate query to trigger refetch
|
||||
trpcContext.invalidateQueries(['resumes.comments.list']);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const onCancel = () => {
|
||||
reset({ description: comment.description });
|
||||
setIsEditingComment(false);
|
||||
};
|
||||
|
||||
const onSubmit: SubmitHandler<ICommentInput> = async (data) => {
|
||||
const { id } = comment;
|
||||
return commentUpdateMutation.mutate(
|
||||
{
|
||||
id,
|
||||
...data,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setIsEditingComment(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const setFormValue = (value: string) => {
|
||||
setValue('description', value.trim(), { shouldDirty: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex-column mt-1 space-y-2">
|
||||
<TextArea
|
||||
{...(register('description', {
|
||||
required: 'Comments cannot be empty!',
|
||||
}),
|
||||
{})}
|
||||
defaultValue={comment.description}
|
||||
disabled={commentUpdateMutation.isLoading}
|
||||
errorMessage={errors.description?.message}
|
||||
label=""
|
||||
placeholder="Leave your comment here"
|
||||
onChange={setFormValue}
|
||||
/>
|
||||
|
||||
<div className="flex-row space-x-2">
|
||||
<Button
|
||||
disabled={commentUpdateMutation.isLoading}
|
||||
label="Cancel"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={!isDirty || commentUpdateMutation.isLoading}
|
||||
isLoading={commentUpdateMutation.isLoading}
|
||||
label="Confirm"
|
||||
size="sm"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { ResumesSection } from '@prisma/client';
|
||||
import { Button, TextArea } from '@tih/ui';
|
||||
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
type ResumeCommentEditFormProps = {
|
||||
parentId: string;
|
||||
resumeId: string;
|
||||
section: ResumesSection;
|
||||
setIsReplyingComment: (value: boolean) => void;
|
||||
};
|
||||
|
||||
type IReplyInput = {
|
||||
description: string;
|
||||
};
|
||||
|
||||
export default function ResumeCommentReplyForm({
|
||||
parentId,
|
||||
setIsReplyingComment,
|
||||
resumeId,
|
||||
section,
|
||||
}: ResumeCommentEditFormProps) {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors, isDirty },
|
||||
reset,
|
||||
} = useForm<IReplyInput>({
|
||||
defaultValues: {
|
||||
description: '',
|
||||
},
|
||||
});
|
||||
|
||||
const trpcContext = trpc.useContext();
|
||||
const commentReplyMutation = trpc.useMutation('resumes.comments.user.reply', {
|
||||
onSuccess: () => {
|
||||
// Comment updated, invalidate query to trigger refetch
|
||||
trpcContext.invalidateQueries(['resumes.comments.list']);
|
||||
},
|
||||
});
|
||||
|
||||
const onCancel = () => {
|
||||
reset({ description: '' });
|
||||
setIsReplyingComment(false);
|
||||
};
|
||||
|
||||
const onSubmit: SubmitHandler<IReplyInput> = async (data) => {
|
||||
return commentReplyMutation.mutate(
|
||||
{
|
||||
parentId,
|
||||
resumeId,
|
||||
section,
|
||||
...data,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setIsReplyingComment(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const setFormValue = (value: string) => {
|
||||
setValue('description', value.trim(), { shouldDirty: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex-column space-y-2 pt-2">
|
||||
<TextArea
|
||||
{...(register('description', {
|
||||
required: 'Reply cannot be empty!',
|
||||
}),
|
||||
{})}
|
||||
defaultValue=""
|
||||
disabled={commentReplyMutation.isLoading}
|
||||
errorMessage={errors.description?.message}
|
||||
label=""
|
||||
placeholder="Leave your reply here"
|
||||
onChange={setFormValue}
|
||||
/>
|
||||
|
||||
<div className="flex-row space-x-2">
|
||||
<Button
|
||||
disabled={commentReplyMutation.isLoading}
|
||||
label="Cancel"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={!isDirty || commentReplyMutation.isLoading}
|
||||
isLoading={commentReplyMutation.isLoading}
|
||||
label="Confirm"
|
||||
size="sm"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
ArrowDownCircleIcon,
|
||||
ArrowUpCircleIcon,
|
||||
} from '@heroicons/react/20/solid';
|
||||
import { Vote } from '@prisma/client';
|
||||
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
type ResumeCommentVoteButtonsProps = {
|
||||
commentId: string;
|
||||
userId: string | undefined;
|
||||
};
|
||||
|
||||
export default function ResumeCommentVoteButtons({
|
||||
commentId,
|
||||
userId,
|
||||
}: ResumeCommentVoteButtonsProps) {
|
||||
const [upvoteAnimation, setUpvoteAnimation] = useState(false);
|
||||
const [downvoteAnimation, setDownvoteAnimation] = useState(false);
|
||||
|
||||
const trpcContext = trpc.useContext();
|
||||
|
||||
// COMMENT VOTES
|
||||
const commentVotesQuery = trpc.useQuery([
|
||||
'resumes.comments.votes.list',
|
||||
{ commentId },
|
||||
]);
|
||||
const commentVotesUpsertMutation = trpc.useMutation(
|
||||
'resumes.comments.votes.user.upsert',
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Comment updated, invalidate query to trigger refetch
|
||||
trpcContext.invalidateQueries(['resumes.comments.votes.list']);
|
||||
},
|
||||
},
|
||||
);
|
||||
const commentVotesDeleteMutation = trpc.useMutation(
|
||||
'resumes.comments.votes.user.delete',
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Comment updated, invalidate query to trigger refetch
|
||||
trpcContext.invalidateQueries(['resumes.comments.votes.list']);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const onVote = async (value: Vote, setAnimation: (_: boolean) => void) => {
|
||||
setAnimation(true);
|
||||
|
||||
if (commentVotesQuery.data?.userVote?.value === value) {
|
||||
return commentVotesDeleteMutation.mutate(
|
||||
{
|
||||
commentId,
|
||||
},
|
||||
{
|
||||
onSettled: async () => setAnimation(false),
|
||||
},
|
||||
);
|
||||
}
|
||||
return commentVotesUpsertMutation.mutate(
|
||||
{
|
||||
commentId,
|
||||
value,
|
||||
},
|
||||
{
|
||||
onSettled: async () => setAnimation(false),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
disabled={
|
||||
!userId ||
|
||||
commentVotesQuery.isLoading ||
|
||||
commentVotesUpsertMutation.isLoading ||
|
||||
commentVotesDeleteMutation.isLoading
|
||||
}
|
||||
type="button"
|
||||
onClick={() => onVote(Vote.UPVOTE, setUpvoteAnimation)}>
|
||||
<ArrowUpCircleIcon
|
||||
className={clsx(
|
||||
'h-4 w-4',
|
||||
commentVotesQuery.data?.userVote?.value === Vote.UPVOTE ||
|
||||
upvoteAnimation
|
||||
? 'fill-indigo-500'
|
||||
: 'fill-gray-400',
|
||||
userId &&
|
||||
!downvoteAnimation &&
|
||||
!upvoteAnimation &&
|
||||
'hover:fill-indigo-500',
|
||||
upvoteAnimation && 'animate-[bounce_0.5s_infinite] cursor-default',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div className="flex min-w-[1rem] justify-center text-xs">
|
||||
{commentVotesQuery.data?.numVotes ?? 0}
|
||||
</div>
|
||||
|
||||
<button
|
||||
disabled={
|
||||
!userId ||
|
||||
commentVotesQuery.isLoading ||
|
||||
commentVotesUpsertMutation.isLoading ||
|
||||
commentVotesDeleteMutation.isLoading
|
||||
}
|
||||
type="button"
|
||||
onClick={() => onVote(Vote.DOWNVOTE, setDownvoteAnimation)}>
|
||||
<ArrowDownCircleIcon
|
||||
className={clsx(
|
||||
'h-4 w-4',
|
||||
commentVotesQuery.data?.userVote?.value === Vote.DOWNVOTE ||
|
||||
downvoteAnimation
|
||||
? 'fill-red-500'
|
||||
: 'fill-gray-400',
|
||||
userId &&
|
||||
!downvoteAnimation &&
|
||||
!upvoteAnimation &&
|
||||
'hover:fill-red-500',
|
||||
downvoteAnimation &&
|
||||
'animate-[bounce_0.5s_infinite] cursor-default',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
import Link from 'next/link';
|
||||
|
||||
const baseStyles = {
|
||||
outline:
|
||||
'group inline-flex ring-1 items-center justify-center rounded-full py-2 px-4 text-sm focus:outline-none',
|
||||
solid:
|
||||
'group inline-flex items-center justify-center rounded-full py-2 px-4 text-sm font-semibold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
};
|
||||
|
||||
const variantStyles = {
|
||||
outline: {
|
||||
slate:
|
||||
'ring-slate-200 text-slate-700 hover:text-slate-900 hover:ring-slate-300 active:bg-slate-100 active:text-slate-600 focus-visible:outline-blue-600 focus-visible:ring-slate-300',
|
||||
white:
|
||||
'ring-slate-700 text-white hover:ring-slate-500 active:ring-slate-700 active:text-slate-400 focus-visible:outline-white',
|
||||
},
|
||||
solid: {
|
||||
blue: 'bg-blue-600 text-white hover:text-slate-100 hover:bg-blue-500 active:bg-blue-800 active:text-blue-100 focus-visible:outline-blue-600',
|
||||
slate:
|
||||
'bg-slate-900 text-white hover:bg-slate-700 hover:text-slate-100 active:bg-slate-800 active:text-slate-300 focus-visible:outline-slate-900',
|
||||
white:
|
||||
'bg-white text-slate-900 hover:bg-blue-50 active:bg-blue-200 active:text-slate-600 focus-visible:outline-white',
|
||||
},
|
||||
};
|
||||
|
||||
export function Button({
|
||||
variant = 'solid',
|
||||
color = 'slate',
|
||||
className,
|
||||
href,
|
||||
...props
|
||||
}) {
|
||||
className = clsx(
|
||||
baseStyles[variant],
|
||||
variantStyles[variant][color],
|
||||
className,
|
||||
);
|
||||
|
||||
return href ? (
|
||||
<Link className={className} href={href} {...props} />
|
||||
) : (
|
||||
<button className={className} type="button" {...props} />
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import Image from 'next/future/image';
|
||||
|
||||
import { Button } from './Button';
|
||||
import { Container } from './Container';
|
||||
import backgroundImage from './images/background-call-to-action.jpg';
|
||||
|
||||
export function CallToAction() {
|
||||
return (
|
||||
<section
|
||||
className="relative overflow-hidden bg-blue-600 py-32"
|
||||
id="get-started-today">
|
||||
<Image
|
||||
alt=""
|
||||
className="absolute top-1/2 left-1/2 max-w-none -translate-x-1/2 -translate-y-1/2"
|
||||
height={1244}
|
||||
src={backgroundImage}
|
||||
unoptimized={true}
|
||||
width={2347}
|
||||
/>
|
||||
<Container className="relative">
|
||||
<div className="mx-auto max-w-lg text-center">
|
||||
<h2 className="font-display text-3xl tracking-tight text-white sm:text-4xl">
|
||||
Resume review can start right now.
|
||||
</h2>
|
||||
<p className="mt-4 text-lg tracking-tight text-white">
|
||||
It's free! Take charge of your resume game by learning from the top
|
||||
engineers in the field.
|
||||
</p>
|
||||
<Button className="mt-10" color="white" href="/resumes/browse">
|
||||
Start browsing now
|
||||
</Button>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
28
apps/portal/src/components/resumes/landing/CallToAction.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Container } from './Container';
|
||||
|
||||
export function CallToAction() {
|
||||
return (
|
||||
<section className="relative overflow-hidden py-32" id="get-started-today">
|
||||
<Container className="relative">
|
||||
<div className="mx-auto max-w-lg text-center">
|
||||
<h2 className="font-display text-3xl tracking-tight text-gray-900 sm:text-4xl">
|
||||
Resume review can start right now.
|
||||
</h2>
|
||||
<p className="mt-4 text-lg tracking-tight text-gray-600">
|
||||
It's free! Take charge of your resume game by learning from the top
|
||||
engineers in the field.
|
||||
</p>
|
||||
<Link href="/resumes/browse">
|
||||
<button
|
||||
className="mt-4 rounded-md bg-indigo-500 py-2 px-3 text-sm font-medium text-white"
|
||||
type="button">
|
||||
Start browsing now
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
export function Container({ className, ...props }) {
|
||||
return (
|
||||
<div
|
||||
className={clsx('mx-auto max-w-7xl px-4 sm:px-6 lg:px-8', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
16
apps/portal/src/components/resumes/landing/Container.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import clsx from 'clsx';
|
||||
import type { FC } from 'react';
|
||||
|
||||
type ContainerProps = {
|
||||
children: Array<JSX.Element> | JSX.Element;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Container: FC<ContainerProps> = ({ className, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx('mx-auto max-w-7xl px-4 lg:px-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Container } from './Container';
|
||||
import { Logo } from './Logo';
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="bg-slate-50">
|
||||
<Container>
|
||||
<div className="py-16">
|
||||
<Logo className="mx-auto h-10 w-auto" />
|
||||
<nav aria-label="quick links" className="mt-10 text-sm">
|
||||
<div className="-my-1 flex justify-center gap-x-6">
|
||||
<Link href="#features">Features</Link>
|
||||
<Link href="#testimonials">Testimonials</Link>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-col items-center border-t border-slate-400/10 py-10 sm:flex-row-reverse sm:justify-between">
|
||||
<div className="flex gap-x-6">
|
||||
<Link
|
||||
aria-label="TaxPal on Twitter"
|
||||
className="group"
|
||||
href="https://twitter.com">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-6 w-6 fill-slate-500 group-hover:fill-slate-700">
|
||||
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0 0 22 5.92a8.19 8.19 0 0 1-2.357.646 4.118 4.118 0 0 0 1.804-2.27 8.224 8.224 0 0 1-2.605.996 4.107 4.107 0 0 0-6.993 3.743 11.65 11.65 0 0 1-8.457-4.287 4.106 4.106 0 0 0 1.27 5.477A4.073 4.073 0 0 1 2.8 9.713v.052a4.105 4.105 0 0 0 3.292 4.022 4.093 4.093 0 0 1-1.853.07 4.108 4.108 0 0 0 3.834 2.85A8.233 8.233 0 0 1 2 18.407a11.615 11.615 0 0 0 6.29 1.84" />
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
aria-label="TaxPal on GitHub"
|
||||
className="group"
|
||||
href="https://github.com">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-6 w-6 fill-slate-500 group-hover:fill-slate-700">
|
||||
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2Z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-6 text-sm text-slate-500 sm:mt-0">
|
||||
Copyright © {new Date().getFullYear()} Resume Review. All
|
||||
rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</Container>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,13 @@
|
||||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Button } from './Button';
|
||||
import { Container } from './Container';
|
||||
import logoLaravel from './images/logos/laravel.svg';
|
||||
import logoMirage from './images/logos/mirage.svg';
|
||||
import logoStatamic from './images/logos/statamic.svg';
|
||||
import logoStaticKit from './images/logos/statickit.svg';
|
||||
import logoTransistor from './images/logos/transistor.svg';
|
||||
import logoTuple from './images/logos/tuple.svg';
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
<Container className="pt-20 pb-16 text-center lg:pt-32">
|
||||
<Container className="pb-36 pt-20 text-center lg:pt-32">
|
||||
<h1 className="font-display mx-auto max-w-4xl text-5xl font-medium tracking-tight text-slate-900 sm:text-7xl">
|
||||
Resume review{' '}
|
||||
<span className="relative whitespace-nowrap text-blue-600">
|
||||
<span className="relative whitespace-nowrap text-indigo-500">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="absolute top-2/3 left-0 h-[0.58em] w-full fill-blue-300/70"
|
||||
@@ -33,41 +25,25 @@ export function Hero() {
|
||||
</p>
|
||||
<div className="mt-10 flex justify-center gap-x-4">
|
||||
<Link href="/resumes/browse">
|
||||
<Button>Start browsing now</Button>
|
||||
<button
|
||||
className="rounded-md bg-indigo-500 py-2 px-3 text-sm font-medium text-white"
|
||||
type="button">
|
||||
Start browsing now
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">
|
||||
<Button variant="outline">
|
||||
<button
|
||||
className="group inline-flex items-center justify-center rounded-md py-2 px-4 text-sm ring-1 ring-slate-200 hover:text-slate-900 hover:ring-slate-300 focus:outline-none focus-visible:outline-indigo-600 focus-visible:ring-slate-300 active:bg-slate-100 active:text-slate-600"
|
||||
type="button">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-3 w-3 flex-none fill-blue-600 group-active:fill-current">
|
||||
className="h-3 w-3 flex-none fill-indigo-600 group-active:fill-current">
|
||||
<path d="m9.997 6.91-7.583 3.447A1 1 0 0 1 1 9.447V2.553a1 1 0 0 1 1.414-.91L9.997 5.09c.782.355.782 1.465 0 1.82Z" />
|
||||
</svg>
|
||||
<span className="ml-3">Watch video</span>
|
||||
</Button>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-36 lg:mt-44">
|
||||
<p className="font-display text-base text-slate-900">
|
||||
Resumes reviewed from engineers from these companies so far
|
||||
</p>
|
||||
<ul
|
||||
className="mt-8 flex items-center justify-center gap-x-8 sm:flex-col sm:gap-x-0 sm:gap-y-10 xl:flex-row xl:gap-x-12 xl:gap-y-0"
|
||||
role="list">
|
||||
{[
|
||||
{ logo: logoTransistor, name: 'Apple' },
|
||||
{ logo: logoTuple, name: 'Meta' },
|
||||
{ logo: logoStaticKit, name: 'Google' },
|
||||
|
||||
{ logo: logoMirage, name: 'Mirage' },
|
||||
{ logo: logoLaravel, name: 'Laravel' },
|
||||
{ logo: logoStatamic, name: 'Statamic' },
|
||||
].map((company) => (
|
||||
<li key={company.name} className="flex">
|
||||
<Image alt={company.name} src={company.logo} unoptimized={true} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export function Logo(props) {
|
||||
import type { FC } from 'react';
|
||||
|
||||
export const Logo: FC = (props) => {
|
||||
return (
|
||||
<svg aria-hidden="true" viewBox="0 0 109 40" {...props}>
|
||||
<path
|
||||
@@ -29,4 +31,4 @@ export function Logo(props) {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -4,28 +4,27 @@ import { useEffect, useState } from 'react';
|
||||
import { Tab } from '@headlessui/react';
|
||||
|
||||
import { Container } from './Container';
|
||||
import backgroundImage from './images/background-features.jpg';
|
||||
import screenshotExpenses from './images/screenshots/expenses.png';
|
||||
import screenshotPayroll from './images/screenshots/payroll.png';
|
||||
import screenshotVatReturns from './images/screenshots/vat-returns.png';
|
||||
import resumeBrowse from './images/screenshots/resumes-browse.png';
|
||||
import resumeReview from './images/screenshots/resumes-review.png';
|
||||
import resumeSubmit from './images/screenshots/resumes-submit.png';
|
||||
|
||||
const features = [
|
||||
{
|
||||
description:
|
||||
'Browse the most popular reviewed resumes out there and see what you can learn',
|
||||
image: screenshotPayroll,
|
||||
image: resumeBrowse,
|
||||
title: 'Browse',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Upload your own resume easily to get feedback from people in industry.',
|
||||
image: screenshotExpenses,
|
||||
image: resumeSubmit,
|
||||
title: 'Submit',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Pass the baton forward by reviewing resumes and bounce off ideas with other engineers out there.',
|
||||
image: screenshotVatReturns,
|
||||
image: resumeReview,
|
||||
title: 'Review',
|
||||
},
|
||||
];
|
||||
@@ -36,7 +35,7 @@ export function PrimaryFeatures() {
|
||||
useEffect(() => {
|
||||
const lgMediaQuery = window.matchMedia('(min-width: 1024px)');
|
||||
|
||||
function onMediaQueryChange({ matches }) {
|
||||
function onMediaQueryChange({ matches }: { matches: boolean }) {
|
||||
setTabOrientation(matches ? 'vertical' : 'horizontal');
|
||||
}
|
||||
|
||||
@@ -50,17 +49,8 @@ export function PrimaryFeatures() {
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label="Features for running your books"
|
||||
className="relative overflow-hidden bg-blue-600 pt-20 pb-28 sm:py-32"
|
||||
className="relative overflow-hidden bg-gradient-to-r from-indigo-400 to-indigo-700 pt-20 pb-28 sm:py-32"
|
||||
id="features">
|
||||
<Image
|
||||
alt=""
|
||||
className="absolute top-1/2 left-1/2 max-w-none translate-x-[-44%] translate-y-[-42%]"
|
||||
height={1636}
|
||||
src={backgroundImage}
|
||||
unoptimized={true}
|
||||
width={2245}
|
||||
/>
|
||||
<Container className="relative">
|
||||
<div className="max-w-2xl md:mx-auto md:text-center xl:max-w-none">
|
||||
<h2 className="font-display text-3xl tracking-tight text-white sm:text-4xl md:text-5xl">
|
||||
@@ -73,7 +63,7 @@ export function PrimaryFeatures() {
|
||||
vertical={tabOrientation === 'vertical'}>
|
||||
{({ selectedIndex }) => (
|
||||
<>
|
||||
<div className="-mx-4 flex overflow-x-auto pb-4 sm:mx-0 sm:overflow-visible sm:pb-0 lg:col-span-5">
|
||||
<div className="-mx-4 flex overflow-x-auto pb-4 sm:mx-0 sm:overflow-visible sm:pb-0 lg:col-span-4">
|
||||
<Tab.List className="relative z-10 flex gap-x-4 whitespace-nowrap px-4 sm:mx-auto sm:px-0 lg:mx-0 lg:block lg:gap-x-0 lg:gap-y-1 lg:whitespace-normal">
|
||||
{features.map((feature, featureIndex) => (
|
||||
<div
|
||||
@@ -109,7 +99,7 @@ export function PrimaryFeatures() {
|
||||
))}
|
||||
</Tab.List>
|
||||
</div>
|
||||
<Tab.Panels className="lg:col-span-7">
|
||||
<Tab.Panels className="lg:col-span-8">
|
||||
{features.map((feature) => (
|
||||
<Tab.Panel key={feature.title} unmount={false}>
|
||||
<div className="relative sm:px-6 lg:hidden">
|
||||
@@ -7,6 +7,10 @@ import avatarImage3 from './images/avatars/avatar-3.png';
|
||||
import avatarImage4 from './images/avatars/avatar-4.png';
|
||||
import avatarImage5 from './images/avatars/avatar-5.png';
|
||||
|
||||
type QuoteProps = {
|
||||
className: string;
|
||||
};
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
columns: [
|
||||
@@ -79,7 +83,7 @@ const testimonials = [
|
||||
},
|
||||
];
|
||||
|
||||
function QuoteIcon(props) {
|
||||
function QuoteIcon(props: QuoteProps) {
|
||||
return (
|
||||
<svg aria-hidden="true" height={78} width={105} {...props}>
|
||||
<path d="M25.086 77.292c-4.821 0-9.115-1.205-12.882-3.616-3.767-2.561-6.78-6.102-9.04-10.622C1.054 58.534 0 53.411 0 47.686c0-5.273.904-10.396 2.712-15.368 1.959-4.972 4.746-9.567 8.362-13.786a59.042 59.042 0 0 1 12.43-11.3C28.325 3.917 33.599 1.507 39.324 0l11.074 13.786c-6.479 2.561-11.677 5.951-15.594 10.17-3.767 4.219-5.65 7.835-5.65 10.848 0 1.356.377 2.863 1.13 4.52.904 1.507 2.637 3.089 5.198 4.746 3.767 2.41 6.328 4.972 7.684 7.684 1.507 2.561 2.26 5.5 2.26 8.814 0 5.123-1.959 9.19-5.876 12.204-3.767 3.013-8.588 4.52-14.464 4.52Zm54.24 0c-4.821 0-9.115-1.205-12.882-3.616-3.767-2.561-6.78-6.102-9.04-10.622-2.11-4.52-3.164-9.643-3.164-15.368 0-5.273.904-10.396 2.712-15.368 1.959-4.972 4.746-9.567 8.362-13.786a59.042 59.042 0 0 1 12.43-11.3C82.565 3.917 87.839 1.507 93.564 0l11.074 13.786c-6.479 2.561-11.677 5.951-15.594 10.17-3.767 4.219-5.65 7.835-5.65 10.848 0 1.356.377 2.863 1.13 4.52.904 1.507 2.637 3.089 5.198 4.746 3.767 2.41 6.328 4.972 7.684 7.684 1.507 2.561 2.26 5.5 2.26 8.814 0 5.123-1.959 9.19-5.876 12.204-3.767 3.013-8.588 4.52-14.464 4.52Z" />
|
||||
@@ -90,15 +94,14 @@ function QuoteIcon(props) {
|
||||
export function Testimonials() {
|
||||
return (
|
||||
<section
|
||||
aria-label="What our customers are saying"
|
||||
className="bg-slate-50 py-20 sm:py-32"
|
||||
className="bg-gradient-to-r from-indigo-700 to-indigo-400 py-20 sm:py-32"
|
||||
id="testimonials">
|
||||
<Container>
|
||||
<div className="mx-auto max-w-2xl md:text-center">
|
||||
<h2 className="font-display text-3xl tracking-tight text-slate-900 sm:text-4xl">
|
||||
<h2 className="font-display text-3xl tracking-tight text-white sm:text-4xl">
|
||||
Loved by software engineers worldwide.
|
||||
</h2>
|
||||
<p className="mt-4 text-lg tracking-tight text-slate-700">
|
||||
<p className="mt-4 text-lg tracking-tight text-white">
|
||||
We crowdsource ideas and feedback from across the world,
|
||||
guaranteeing you for success in your job application.
|
||||
</p>
|
||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 181 KiB |
@@ -1,18 +0,0 @@
|
||||
<svg width="136" height="48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#a)" fill="#334155">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M38.455 13.018c.004.01.01.02.012.03a.658.658 0 0 1 .022.164v8.586a.627.627 0 0 1-.311.543l-7.138 4.15v8.223a.627.627 0 0 1-.312.543l-14.899 8.66a.533.533 0 0 1-.108.044c-.014.006-.027.014-.042.018a.612.612 0 0 1-.318 0 .164.164 0 0 1-.028-.01l-.02-.01c-.035-.013-.07-.024-.103-.043L.311 35.257A.628.628 0 0 1 0 34.714V8.956a.68.68 0 0 1 .021-.163c.006-.019.016-.035.022-.053l.02-.053a.328.328 0 0 1 .02-.044c.012-.02.029-.037.043-.056.018-.025.033-.05.055-.073.018-.018.04-.03.06-.047.023-.018.044-.038.069-.053l7.45-4.33a.616.616 0 0 1 .62 0l7.45 4.33c.025.015.046.035.069.053l.021.016c.014.01.028.02.04.031a.345.345 0 0 1 .04.051l.015.022c.013.02.03.035.042.056.017.03.028.064.04.097l.01.022.012.03a.644.644 0 0 1 .021.164v16.088l6.208-3.608v-8.224a.64.64 0 0 1 .022-.163c.005-.019.015-.035.021-.053l.007-.02a.279.279 0 0 1 .076-.133c.018-.025.034-.05.055-.073.01-.01.02-.017.03-.025.01-.007.021-.014.03-.022l.036-.03a.26.26 0 0 1 .033-.023l7.45-4.33a.616.616 0 0 1 .62 0l7.45 4.33c.026.015.046.036.069.053l.022.016c.013.01.027.02.038.031a.327.327 0 0 1 .04.052l.016.021.016.02c.01.012.019.023.026.036a.522.522 0 0 1 .034.08l.006.017.01.022ZM9.322 30.453l6.196 3.54 13.652-7.867-6.201-3.605-13.647 7.932Zm20.476-5.409v-7.14l-6.208-3.607v7.14l6.207 3.607h.001Zm6.826-11.83-6.206-3.608-6.205 3.607 6.205 3.606 6.206-3.606Zm-27.933.434v15.726l6.208-3.609V10.04L8.69 13.648h.001Zm5.584-4.692L8.07 5.35 1.864 8.956l6.206 3.607 6.205-3.607ZM7.449 13.65l-6.208-3.61v24.31L14.9 42.29v-7.21l-7.135-4.076h-.002L7.759 31c-.025-.015-.045-.035-.067-.053a.277.277 0 0 1-.059-.045l-.002-.002c-.013-.013-.024-.029-.035-.044a.567.567 0 0 0-.016-.022l-.03-.038a.201.201 0 0 1-.016-.023l-.001-.002a.259.259 0 0 1-.023-.054l-.01-.024-.015-.033a.237.237 0 0 1-.014-.038.374.374 0 0 1-.01-.068l-.003-.025a.48.48 0 0 0-.004-.026c-.002-.014-.005-.029-.005-.044V13.65v.001Zm8.691 21.43v7.21l13.657-7.937V27.21L16.14 35.08v.001Zm14.9-10.037 6.208-3.608v-7.14l-6.208 3.61v7.14-.002Z" />
|
||||
<path d="M132.739 13.214H136V34.36h-3.261V13.214Zm-84.346 0h3.441V31.25h6.463v3.11h-9.904V13.216Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M68.576 20.525c.808.403 1.43.956 1.87 1.66v-1.902h3.261V34.36h-3.261v-1.903c-.439.705-1.062 1.259-1.87 1.66-.808.404-1.62.605-2.44.605-1.057 0-2.024-.196-2.902-.59a6.79 6.79 0 0 1-2.26-1.615 7.525 7.525 0 0 1-1.465-2.356 7.669 7.669 0 0 1-.524-2.84c0-.986.174-1.928.524-2.824a7.496 7.496 0 0 1 1.466-2.371 6.8 6.8 0 0 1 2.26-1.616c.877-.393 1.844-.59 2.902-.59.818 0 1.63.202 2.439.605Zm.733 9.938c.367-.416.651-.898.838-1.42.2-.552.302-1.134.3-1.721 0-.605-.1-1.18-.3-1.722a4.373 4.373 0 0 0-.838-1.42 4.056 4.056 0 0 0-1.302-.967 3.893 3.893 0 0 0-1.69-.362c-.62 0-1.178.12-1.677.362a4.105 4.105 0 0 0-1.286.967c-.36.403-.634.876-.823 1.42a5.182 5.182 0 0 0-.284 1.722c0 .604.094 1.178.284 1.72a4.17 4.17 0 0 0 .823 1.42c.36.404.788.726 1.286.968.524.247 1.097.37 1.676.362.618 0 1.182-.12 1.691-.362.495-.231.938-.56 1.302-.967Zm27.649-8.277c-.44-.705-1.063-1.258-1.87-1.661-.808-.403-1.62-.604-2.44-.604-1.057 0-2.024.196-2.902.589a6.8 6.8 0 0 0-2.26 1.616 7.492 7.492 0 0 0-1.465 2.37c-.35.901-.528 1.86-.524 2.826 0 1.007.174 1.953.524 2.84.338.869.836 1.668 1.466 2.355a6.79 6.79 0 0 0 2.26 1.616c.877.393 1.844.59 2.902.59.818 0 1.63-.202 2.439-.605.808-.402 1.43-.956 1.87-1.66v1.902h3.261V20.283h-3.261v1.903Zm-.3 6.857a4.368 4.368 0 0 1-.838 1.42 4.043 4.043 0 0 1-1.301.967 3.89 3.89 0 0 1-1.69.362c-.619 0-1.178-.12-1.677-.362a4.094 4.094 0 0 1-2.109-2.387 5.182 5.182 0 0 1-.285-1.721c0-.605.095-1.18.285-1.722a4.148 4.148 0 0 1 .823-1.42c.36-.404.798-.733 1.286-.967a3.794 3.794 0 0 1 1.676-.362c.618 0 1.182.12 1.69.362.51.242.943.565 1.302.967.36.403.639.876.839 1.42.198.543.299 1.117.299 1.722 0 .604-.1 1.178-.3 1.72Z" />
|
||||
<path
|
||||
d="M76.281 34.36h3.262V23.523h5.596v-3.24H76.28V34.36h.001Zm32.916-3.297 4.099-10.78h3.304l-5.354 14.077h-4.099l-5.353-14.077h3.303l4.1 10.78Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M123.714 19.92c-3.994 0-7.156 3.315-7.156 7.4 0 4.52 3.06 7.402 7.574 7.402 2.526 0 4.139-.975 6.109-3.098l-2.203-1.721c-.002 0-1.664 2.204-4.145 2.204-2.884 0-4.099-2.348-4.099-3.562h10.821c.568-4.65-2.46-8.624-6.901-8.624Zm-3.911 6.178c.025-.27.401-3.562 3.885-3.562s3.907 3.29 3.931 3.562h-7.816Z" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h136v48H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.5 KiB |
@@ -1,17 +0,0 @@
|
||||
<svg width="138" height="48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#a)" fill="#334155">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M30.316 5c3.984 0 7.213 3.256 7.213 7.273a7.272 7.272 0 0 1-4.771 6.845l5.814 10.462h3.98c.613 0 1.11.5 1.11 1.118 0 .618-.497 1.118-1.11 1.118H1.11c-.612 0-1.109-.5-1.109-1.118 0-.618.497-1.118 1.11-1.118h3.98l10.353-18.562a1.106 1.106 0 0 1 1.896-.063l5.948 9.189 1.85-2.809a7.28 7.28 0 0 1-2.035-5.062c0-4.017 3.23-7.273 7.214-7.273h-.001Zm-5.709 17.183 4.788 7.397h6.634l-7.457-13.418-3.965 6.021Zm2.14 7.397L16.48 13.72 7.635 29.58H26.747Zm8.702-17.307a5.172 5.172 0 0 1-3.728 4.98l-2.101-3.781a1.106 1.106 0 0 0-1.892-.072l-1.402 2.13a5.18 5.18 0 0 1-1.144-3.257c0-2.859 2.299-5.176 5.134-5.176 2.835 0 5.133 2.317 5.133 5.176Z" />
|
||||
<path
|
||||
d="M9.62 35.173c-.611 0-1.107.5-1.107 1.117s.496 1.116 1.107 1.116h24.42c.612 0 1.108-.5 1.108-1.116 0-.617-.496-1.117-1.107-1.117H9.62Zm8.513 5.59c-.613 0-1.11.5-1.11 1.119 0 .617.497 1.118 1.11 1.118h7.396c.612 0 1.109-.5 1.109-1.118 0-.618-.497-1.12-1.11-1.12h-7.395Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M120.027 21.871c-.982-1.25-2.453-1.923-4.347-1.923-2.984 0-6.049 2.528-6.049 6.786 0 4.258 3.065 6.786 6.049 6.786 1.894 0 3.365-.66 4.347-1.923v1.058c0 2.445-1.472 3.93-4.142 3.93-1.594 0-3.107-.524-4.428-1.334l-1.036 2.432c1.376.99 3.515 1.525 5.464 1.525 4.36 0 7.003-2.54 7.003-6.677v-12.24h-2.861v1.58Zm-7.467 4.863c0-2.225 1.444-4.08 3.855-4.08 2.303 0 3.857 1.773 3.857 4.08 0 2.308-1.554 4.08-3.857 4.08-2.411 0-3.855-1.867-3.855-4.08Zm-8.219-4.849c-.899-1.168-2.248-1.937-4.101-1.937-3.65 0-6.526 2.898-6.526 6.923s2.875 6.924 6.526 6.924c1.854 0 3.202-.755 4.101-1.923v1.58h2.848v-13.16h-2.848v1.593Zm-7.698 4.986c0-2.307 1.486-4.217 3.938-4.217 2.357 0 3.938 1.813 3.938 4.217s-1.581 4.218-3.938 4.218c-2.452 0-3.938-1.91-3.938-4.218ZM138 26.858c-.013-4.107-2.52-6.91-6.172-6.91-3.842 0-6.499 2.803-6.499 6.924 0 4.176 2.766 6.924 6.676 6.924 1.976 0 3.774-.48 5.368-1.854l-1.417-2.048c-1.076.865-2.466 1.388-3.774 1.388-1.853 0-3.501-.99-3.883-3.353h9.647c.027-.329.054-.7.054-1.07Zm-9.687-1.113c.3-1.923 1.43-3.242 3.46-3.242 1.813 0 2.998 1.195 3.311 3.242h-6.771Z" />
|
||||
<path
|
||||
d="m64.333 27.957-5.546-13.738H54.06v19.233h3.08V17.777L62.71 31.57h3.243l5.573-13.944v15.826h3.08V14.219h-4.729l-5.545 13.738h.001Zm16.871 5.495v-13.16h-2.86v13.16h2.86Zm12.182-13.133c-.654-.261-1.322-.37-2.194-.37-1.594 0-2.93.576-3.788 1.826V20.29h-2.82v13.16h2.848v-7.24c0-2.238 1.294-3.53 3.106-3.53.695 0 1.567.165 2.166.48l.682-2.842Zm-11.61-4.575c0-1.14-.886-2.033-2.017-2.033-1.13 0-1.99.893-1.99 2.033s.86 2.006 1.99 2.006c1.131 0 2.017-.866 2.017-2.006Z" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h138v48H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="127" height="48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="m3.31 28.903 14.75-15.816L14.749 10 0 25.816l3.31 3.087ZM5.792 39 20.54 23.184l-3.31-3.087L2.482 35.913 5.792 39Zm31.261-18.216a3.87 3.87 0 0 0-1.441-1.115c-.594-.276-1.166-.414-1.718-.414-.284 0-.572.025-.864.075a2.23 2.23 0 0 0-.79.289c-.234.142-.43.326-.59.551-.158.226-.237.514-.237.865 0 .3.062.552.188.752.125.2.309.376.551.527.242.15.53.288.865.413.334.126.71.255 1.128.389.602.2 1.229.422 1.88.664a7.03 7.03 0 0 1 1.78.965 5.07 5.07 0 0 1 1.329 1.492c.35.593.526 1.333.526 2.219 0 1.019-.188 1.9-.564 2.644a5.274 5.274 0 0 1-1.516 1.843 6.499 6.499 0 0 1-2.181 1.078 9.17 9.17 0 0 1-2.532.35c-1.27 0-2.499-.22-3.685-.663-1.187-.443-2.173-1.074-2.959-1.893l2.808-2.858c.435.535 1.007.982 1.717 1.341.71.36 1.417.54 2.119.54.317 0 .626-.034.927-.101.301-.067.564-.176.79-.326.225-.15.405-.351.539-.602.134-.25.2-.551.2-.902 0-.334-.083-.619-.25-.853a2.45 2.45 0 0 0-.715-.639 5.76 5.76 0 0 0-1.153-.526c-.46-.159-.982-.33-1.567-.514a14.963 14.963 0 0 1-1.667-.652 5.58 5.58 0 0 1-1.454-.965 4.471 4.471 0 0 1-1.028-1.43c-.259-.559-.388-1.24-.388-2.042 0-.986.2-1.83.601-2.532.39-.689.93-1.28 1.58-1.73a6.786 6.786 0 0 1 2.206-.99c.81-.208 1.645-.314 2.482-.314 1.002 0 2.026.184 3.07.552 1.045.368 1.96.91 2.746 1.63l-2.733 2.882Zm9.677 3.736v4.763c0 .585.113 1.023.338 1.316.226.292.631.439 1.216.439.2 0 .414-.017.64-.05.194-.025.383-.075.563-.15l.05 3.007c-.283.1-.643.188-1.077.264a7.63 7.63 0 0 1-1.304.112c-.836 0-1.538-.104-2.106-.313-.568-.209-1.023-.51-1.366-.902a3.54 3.54 0 0 1-.74-1.404 6.804 6.804 0 0 1-.225-1.818V24.52h-2.006v-3.084h1.98v-3.284h4.037v3.284h2.933v3.084H46.73Zm12.234 3.96h-.527c-.451 0-.906.021-1.366.063-.46.042-.87.122-1.228.238a2.27 2.27 0 0 0-.89.514c-.234.226-.351.523-.351.89 0 .234.054.435.163.602.108.167.246.3.413.401.167.1.36.171.577.213a3.3 3.3 0 0 0 .627.063c.835 0 1.474-.23 1.917-.69.443-.46.665-1.082.665-1.867v-.427Zm-7.546-5.34a7.2 7.2 0 0 1 2.57-1.579 8.805 8.805 0 0 1 2.995-.526c1.053 0 1.943.13 2.67.389.727.259 1.316.66 1.767 1.203.452.543.782 1.228.99 2.056.21.827.314 1.809.314 2.945v6.293h-3.76v-1.329h-.076c-.317.518-.798.92-1.441 1.203a5.125 5.125 0 0 1-2.093.426 6.403 6.403 0 0 1-1.555-.2 4.554 4.554 0 0 1-1.466-.652 3.53 3.53 0 0 1-1.09-1.203c-.285-.502-.427-1.12-.427-1.855 0-.903.247-1.63.74-2.181.493-.552 1.128-.978 1.905-1.279.777-.3 1.642-.501 2.595-.601.952-.1 1.88-.151 2.782-.151v-.2c0-.619-.217-1.074-.651-1.367-.435-.292-.97-.439-1.605-.439a3.99 3.99 0 0 0-1.692.377 5.4 5.4 0 0 0-1.392.902l-2.08-2.231v-.001Zm18.688 1.38v4.763c0 .585.112 1.023.338 1.316.225.292.63.439 1.216.439.2 0 .413-.017.639-.05.226-.034.414-.084.564-.15l.05 3.007a6.88 6.88 0 0 1-1.078.264c-.43.075-.866.112-1.303.112-.836 0-1.538-.104-2.106-.313-.568-.209-1.024-.51-1.366-.902a3.537 3.537 0 0 1-.74-1.404 6.808 6.808 0 0 1-.226-1.818V24.52H64.09v-3.084h1.98v-3.284h4.037v3.284h2.933v3.084H70.106Zm9.325-7.07c0 .318-.063.614-.188.89-.12.268-.29.51-.501.715a2.44 2.44 0 0 1-1.667.652c-.669 0-1.229-.222-1.68-.665a2.15 2.15 0 0 1-.677-1.592c0-.3.059-.589.176-.865.117-.275.284-.514.501-.714.217-.2.468-.364.752-.489s.593-.188.928-.188a2.445 2.445 0 0 1 1.667.652c.209.2.376.439.501.714.126.276.188.573.188.89ZM75.02 33.92V21.437h4.111v12.485H75.02v-.002Zm15.273-8.448a2.496 2.496 0 0 0-.953-.727 2.92 2.92 0 0 0-1.228-.275c-.435 0-.828.087-1.179.263a2.86 2.86 0 0 0-.902.702c-.25.292-.447.63-.59 1.015-.143.393-.215.81-.212 1.228 0 .435.067.844.2 1.229a3 3 0 0 0 .59 1.015c.258.293.568.522.927.69.36.167.765.25 1.216.25.418 0 .831-.08 1.24-.238.41-.159.74-.389.99-.69l2.282 2.783c-.518.502-1.186.894-2.005 1.178-.84.288-1.72.432-2.608.427a8.229 8.229 0 0 1-2.757-.452 6.361 6.361 0 0 1-2.219-1.316 6.18 6.18 0 0 1-1.479-2.093c-.36-.819-.539-1.746-.539-2.783 0-1.02.18-1.938.54-2.757a6.181 6.181 0 0 1 1.478-2.093 6.519 6.519 0 0 1 2.219-1.33 7.951 7.951 0 0 1 5.352.001c.41.142.786.317 1.128.526.343.21.64.439.89.69l-2.381 2.757Zm15.091 8.449-6.593-8.173h-.05v8.173h-4.212V16.17h4.212v7.22h.075l6.343-7.22h5.364l-7.646 8.173 8.098 9.577h-5.591v.001Zm11.206-16.47c0 .317-.062.613-.188.89-.12.268-.29.51-.501.714a2.445 2.445 0 0 1-1.667.652c-.669 0-1.229-.222-1.68-.665a2.152 2.152 0 0 1-.677-1.592c0-.3.059-.589.176-.865.117-.275.284-.514.501-.714.217-.2.468-.364.752-.489s.593-.188.928-.188a2.445 2.445 0 0 1 1.667.652c.209.2.376.439.501.714.126.276.188.573.188.89v.001Zm-4.412 16.47V21.436h4.111v12.485h-4.111Zm11.833-9.401v4.763c0 .585.112 1.023.338 1.316.226.292.631.439 1.216.439.2 0 .414-.017.639-.05.194-.024.384-.075.564-.15l.05 3.007a6.88 6.88 0 0 1-1.078.264c-.43.075-.866.112-1.303.112-.836 0-1.538-.104-2.106-.313-.568-.209-1.024-.51-1.366-.902a3.535 3.535 0 0 1-.74-1.404 6.84 6.84 0 0 1-.225-1.818V24.52h-2.006v-3.084h1.981v-3.284h4.036v3.284h2.933v3.084h-2.933Z"
|
||||
fill="#334155" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.7 KiB |
@@ -1,13 +0,0 @@
|
||||
<svg width="158" height="48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#a)" fill="#334155">
|
||||
<path
|
||||
d="M55.423 18.178v-2.491H66.38v2.491h-4.076v13.875H59.47V18.178h-4.047Zm18.098 4.577a7.526 7.526 0 0 0-1.33-.116c-1.82 0-2.89 1.188-2.89 3.592v5.793h-2.69v-11.47h2.631v1.622h.058c.52-.985 1.677-2 3.21-2 .433 0 .722.03 1.011.059v2.52Zm12.866 9.269h-2.602v-1.448h-.058c-.78.985-2.024 1.883-3.932 1.883-2.573 0-5.493-2.057-5.493-6.17 0-3.65 2.573-6.083 5.695-6.083 1.908 0 3.065 1.013 3.76 1.911h.057v-1.564h2.602v11.471h-.03.001Zm-5.898-1.94c1.763 0 3.411-1.536 3.411-3.738 0-2.317-1.503-3.852-3.382-3.852-2.37 0-3.499 1.912-3.499 3.795 0 1.911 1.1 3.794 3.47 3.794v.001Zm9.021-9.531h2.66v1.449h.057c.896-1.304 2.226-1.825 3.498-1.825 2.371 0 4.453 1.564 4.453 5.243v6.604h-2.69v-6.46c0-1.97-.924-3.012-2.457-3.012-1.677 0-2.833 1.188-2.833 3.418v6.083H89.51v-11.5h.001Zm18.792 2.955c-.116-.84-.752-1.39-1.533-1.39-.925 0-1.445.579-1.445 1.216 0 .695.347 1.188 2.341 1.854 2.458.782 3.325 2.057 3.325 3.679 0 2.114-1.59 3.592-4.221 3.592-2.746 0-4.105-1.507-4.308-3.65h2.487c.115.956.694 1.68 1.879 1.68 1.012 0 1.59-.637 1.59-1.42 0-.868-.491-1.419-2.399-2.056-2.14-.695-3.239-1.767-3.239-3.563 0-1.883 1.475-3.273 3.903-3.273 2.458 0 3.759 1.448 4.048 3.33h-2.428v.001Zm5.03-8.227h2.978v2.723h-2.978v-2.723Zm.145 5.272h2.688v11.5h-2.688v-11.5Zm10.986 2.955c-.116-.84-.752-1.39-1.533-1.39-.925 0-1.445.579-1.445 1.216 0 .695.347 1.188 2.342 1.854 2.457.782 3.324 2.057 3.324 3.679 0 2.114-1.59 3.592-4.221 3.592-2.746 0-4.105-1.507-4.307-3.65h2.486c.116.956.694 1.68 1.879 1.68 1.012 0 1.59-.637 1.59-1.42 0-.868-.491-1.419-2.399-2.056-2.14-.695-3.238-1.767-3.238-3.563 0-1.883 1.474-3.273 3.903-3.273 2.457 0 3.758 1.448 4.047 3.33h-2.428v.001Zm3.845-2.955h1.445v-3.678h2.689v3.678h2.862v2.26h-2.891v5.127c0 1.564.492 1.999 1.59 1.999.463 0 .983-.087 1.388-.203v2.172c-.607.174-1.359.261-2.024.261-2.862 0-3.614-1.738-3.614-4.084v-5.272h-1.445v-2.26Zm14.311-.376c3.585 0 6.129 2.636 6.129 6.112 0 3.389-2.573 6.17-6.129 6.17-3.498 0-6.129-2.694-6.129-6.17 0-3.563 2.66-6.112 6.129-6.112Zm0 9.877c2.024 0 3.411-1.622 3.411-3.765 0-2.028-1.301-3.737-3.411-3.737-2.053 0-3.412 1.593-3.412 3.737 0 2.201 1.562 3.765 3.412 3.765Zm14.052-7.415c-1.822 0-2.891 1.188-2.891 3.592v5.793h-2.689v-11.47h2.631v1.622h.058c.52-.985 1.676-2 3.209-2 .433 0 .722.03 1.012.059v2.52a7.525 7.525 0 0 0-1.33-.116ZM20.816 37.731a1.39 1.39 0 0 1-1.388-1.39V11.37a1.389 1.389 0 0 1 2.369-.982c.26.26.406.614.406.982v24.97c0 .753-.636 1.39-1.387 1.39v.001Zm-5.783-12.484h-6.65a1.39 1.39 0 0 1-1.387-1.39c0-.783.607-1.391 1.388-1.391h6.65a1.39 1.39 0 1 1 0 2.78v.001Zm18.243 0h-6.678a1.39 1.39 0 0 1-1.388-1.39c0-.783.607-1.391 1.388-1.391h6.65a1.39 1.39 0 0 1 1.387 1.39c0 .782-.607 1.39-1.359 1.39v.001Z" />
|
||||
<path
|
||||
d="M20.816 44.712C9.338 44.712 0 35.356 0 23.856 0 12.356 9.338 3 20.816 3s20.816 9.356 20.816 20.856c0 11.5-9.338 20.856-20.816 20.856Zm0-38.931c-9.945 0-18.04 8.11-18.04 18.075s8.095 18.075 18.04 18.075c9.946 0 18.04-8.11 18.04-18.075S30.763 5.781 20.817 5.781h-.001Z" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h158v48H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1,13 +0,0 @@
|
||||
<svg width="105" height="48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M18 4 0 10v19.5l6 2V37l18 6V11.5l-6 2V4ZM8 32.167 18 35.5V15.608l4-1.333v25.95L8 35.56v-3.393Z" fill="#334155" />
|
||||
<path
|
||||
d="M42.9 20.45V31h4.446V20.45h3.53v-3.392H39.39v3.393h3.51Zm10.205 4.798c0 3.978 2.3 6.006 6.376 6.006 3.9 0 6.396-1.853 6.396-6.045v-8.15H61.43v7.994c0 1.833-.39 2.73-1.95 2.73-1.58 0-1.97-.897-1.97-2.71v-8.015h-4.406v8.19Z"
|
||||
fill="#334155" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M68.965 31V17.058h5.558c4.017 0 5.733 1.794 5.733 4.777v.078c0 2.906-1.93 4.544-5.538 4.544h-1.346V31h-4.407Zm5.323-7.507h-.916v-3.14h.936c1.15 0 1.755.43 1.755 1.502v.078c0 1.033-.605 1.56-1.775 1.56Z"
|
||||
fill="#334155" />
|
||||
<path
|
||||
d="M82.563 31V17.058h4.427v10.53h5.07V31h-9.497Zm11.999-13.942V31h10.218v-3.393h-5.811v-2.086h4.368v-3.1h-4.368v-1.97h5.499v-3.393h-9.906Z"
|
||||
fill="#334155" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 967 B |
|
Before Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 652 KiB |
|
After Width: | Height: | Size: 1006 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
Before Width: | Height: | Size: 149 KiB |
@@ -1,48 +1,46 @@
|
||||
import clsx from 'clsx';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
type ResumeExpandableTextProps = Readonly<{
|
||||
children: ReactNode;
|
||||
text: string;
|
||||
}>;
|
||||
|
||||
export default function ResumeExpandableText({
|
||||
children,
|
||||
text,
|
||||
}: ResumeExpandableTextProps) {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [descriptionOverflow, setDescriptionOverflow] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (ref.current && ref.current.clientHeight < ref.current.scrollHeight) {
|
||||
setDescriptionOverflow(true);
|
||||
} else {
|
||||
setDescriptionOverflow(false);
|
||||
}
|
||||
}, [ref]);
|
||||
|
||||
const onSeeActionClicked = () => {
|
||||
setDescriptionExpanded(!descriptionExpanded);
|
||||
setIsExpanded((prevExpanded) => !prevExpanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<span
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
'whitespace-pre-wrap text-sm',
|
||||
'line-clamp-3',
|
||||
descriptionExpanded ? 'line-clamp-none' : '',
|
||||
'line-clamp-3 whitespace-pre-wrap text-sm',
|
||||
isExpanded ? 'line-clamp-none' : '',
|
||||
)}>
|
||||
{children}
|
||||
{text}
|
||||
</span>
|
||||
{descriptionOverflow && (
|
||||
<div className="flex flex-row">
|
||||
<div
|
||||
className="text-xs text-indigo-500 hover:text-indigo-300"
|
||||
onClick={onSeeActionClicked}>
|
||||
{descriptionExpanded ? 'See Less' : 'See More'}
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
className="mt-1 cursor-pointer text-xs text-indigo-500 hover:text-indigo-300"
|
||||
onClick={onSeeActionClicked}>
|
||||
{isExpanded ? 'See Less' : 'See More'}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import clsx from 'clsx';
|
||||
import { signIn } from 'next-auth/react';
|
||||
|
||||
type Props = Readonly<{
|
||||
className?: string;
|
||||
text: string;
|
||||
}>;
|
||||
|
||||
export default function ResumeSignInButton({ text }: Props) {
|
||||
export default function ResumeSignInButton({ text, className }: Props) {
|
||||
return (
|
||||
<div className="flex justify-center pt-4">
|
||||
<div className={clsx('flex justify-center pt-4', className)}>
|
||||
<p>
|
||||
<a
|
||||
className="text-primary-800 hover:text-primary-500"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
export default function SubmissionGuidelines() {
|
||||
return (
|
||||
<div className="mb-4 text-left text-sm text-slate-700">
|
||||
<h2 className="mb-2 text-xl font-medium">Submission Guidelines</h2>
|
||||
<p>
|
||||
Before you submit, please review and acknolwedge our
|
||||
<span className="font-bold"> submission guidelines </span>
|
||||
stated below.
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-lg font-bold">• </span>
|
||||
Ensure that you do not divulge any of your
|
||||
<span className="font-bold"> personal particulars</span>.
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-lg font-bold">• </span>
|
||||
Ensure that you do not divulge any
|
||||
<span className="font-bold">
|
||||
{' '}
|
||||
company's proprietary and confidential information
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-lg font-bold">• </span>
|
||||
Proof-read your resumes to look for grammatical/spelling errors.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
628
apps/portal/src/mappers/offers-mappers.ts
Normal file
@@ -0,0 +1,628 @@
|
||||
import type {
|
||||
Company,
|
||||
OffersAnalysis,
|
||||
OffersBackground,
|
||||
OffersCurrency,
|
||||
OffersEducation,
|
||||
OffersExperience,
|
||||
OffersFullTime,
|
||||
OffersIntern,
|
||||
OffersOffer,
|
||||
OffersProfile,
|
||||
OffersReply,
|
||||
OffersSpecificYoe,
|
||||
User,
|
||||
} from '@prisma/client';
|
||||
import { JobType } from '@prisma/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import type {
|
||||
AddToProfileResponse,
|
||||
Analysis,
|
||||
AnalysisHighestOffer,
|
||||
AnalysisOffer,
|
||||
Background,
|
||||
CreateOfferProfileResponse,
|
||||
DashboardOffer,
|
||||
Education,
|
||||
Experience,
|
||||
GetOffersResponse,
|
||||
OffersCompany,
|
||||
Paging,
|
||||
Profile,
|
||||
ProfileAnalysis,
|
||||
ProfileOffer,
|
||||
SpecificYoe,
|
||||
Valuation,
|
||||
} from '~/types/offers';
|
||||
|
||||
const analysisOfferDtoMapper = (
|
||||
offer: OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||
profile: OffersProfile & {
|
||||
background:
|
||||
| (OffersBackground & {
|
||||
experiences: Array<OffersExperience & { company: Company | null }>;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
},
|
||||
) => {
|
||||
const { background, profileName } = offer.profile;
|
||||
const analysisOfferDto: AnalysisOffer = {
|
||||
company: offersCompanyDtoMapper(offer.company),
|
||||
id: offer.id,
|
||||
income: { baseCurrency: '', baseValue: -1, currency: '', value: -1 },
|
||||
jobType: offer.jobType,
|
||||
level: offer.offersFullTime?.level ?? '',
|
||||
location: offer.location,
|
||||
monthYearReceived: offer.monthYearReceived,
|
||||
negotiationStrategy: offer.negotiationStrategy,
|
||||
previousCompanies:
|
||||
background?.experiences
|
||||
?.filter((exp) => exp.company != null)
|
||||
.map((exp) => exp.company?.name ?? '') ?? [],
|
||||
profileName,
|
||||
specialization:
|
||||
offer.jobType === JobType.FULLTIME
|
||||
? offer.offersFullTime?.specialization ?? ''
|
||||
: offer.offersIntern?.specialization ?? '',
|
||||
title:
|
||||
offer.jobType === JobType.FULLTIME
|
||||
? offer.offersFullTime?.title ?? ''
|
||||
: offer.offersIntern?.title ?? '',
|
||||
totalYoe: background?.totalYoe ?? -1,
|
||||
};
|
||||
|
||||
if (offer.offersFullTime?.totalCompensation) {
|
||||
analysisOfferDto.income.value =
|
||||
offer.offersFullTime.totalCompensation.value;
|
||||
analysisOfferDto.income.currency =
|
||||
offer.offersFullTime.totalCompensation.currency;
|
||||
analysisOfferDto.income.baseValue =
|
||||
offer.offersFullTime.totalCompensation.baseValue;
|
||||
analysisOfferDto.income.baseCurrency =
|
||||
offer.offersFullTime.totalCompensation.baseCurrency;
|
||||
} else if (offer.offersIntern?.monthlySalary) {
|
||||
analysisOfferDto.income.value = offer.offersIntern.monthlySalary.value;
|
||||
analysisOfferDto.income.currency =
|
||||
offer.offersIntern.monthlySalary.currency;
|
||||
analysisOfferDto.income.baseValue =
|
||||
offer.offersIntern.monthlySalary.baseValue;
|
||||
analysisOfferDto.income.baseCurrency =
|
||||
offer.offersIntern.monthlySalary.baseCurrency;
|
||||
} else {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Total Compensation or Salary not found',
|
||||
});
|
||||
}
|
||||
|
||||
return analysisOfferDto;
|
||||
};
|
||||
|
||||
const analysisDtoMapper = (
|
||||
noOfOffers: number,
|
||||
percentile: number,
|
||||
topPercentileOffers: Array<
|
||||
OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & {
|
||||
totalCompensation: OffersCurrency;
|
||||
})
|
||||
| null;
|
||||
offersIntern:
|
||||
| (OffersIntern & {
|
||||
monthlySalary: OffersCurrency;
|
||||
})
|
||||
| null;
|
||||
profile: OffersProfile & {
|
||||
background:
|
||||
| (OffersBackground & {
|
||||
experiences: Array<
|
||||
OffersExperience & {
|
||||
company: Company | null;
|
||||
}
|
||||
>;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
}
|
||||
>,
|
||||
) => {
|
||||
const analysisDto: Analysis = {
|
||||
noOfOffers,
|
||||
percentile,
|
||||
topPercentileOffers: topPercentileOffers.map((offer) =>
|
||||
analysisOfferDtoMapper(offer),
|
||||
),
|
||||
};
|
||||
return analysisDto;
|
||||
};
|
||||
|
||||
const analysisHighestOfferDtoMapper = (
|
||||
offer: OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||
profile: OffersProfile & { background: OffersBackground | null };
|
||||
},
|
||||
) => {
|
||||
const analysisHighestOfferDto: AnalysisHighestOffer = {
|
||||
company: offersCompanyDtoMapper(offer.company),
|
||||
id: offer.id,
|
||||
level: offer.offersFullTime?.level ?? '',
|
||||
location: offer.location,
|
||||
specialization:
|
||||
offer.jobType === JobType.FULLTIME
|
||||
? offer.offersFullTime?.specialization ?? ''
|
||||
: offer.offersIntern?.specialization ?? '',
|
||||
totalYoe: offer.profile.background?.totalYoe ?? -1,
|
||||
};
|
||||
return analysisHighestOfferDto;
|
||||
};
|
||||
|
||||
export const profileAnalysisDtoMapper = (
|
||||
analysis:
|
||||
| (OffersAnalysis & {
|
||||
overallHighestOffer: OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern:
|
||||
| (OffersIntern & { monthlySalary: OffersCurrency })
|
||||
| null;
|
||||
profile: OffersProfile & { background: OffersBackground | null };
|
||||
};
|
||||
topCompanyOffers: Array<
|
||||
OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern:
|
||||
| (OffersIntern & { monthlySalary: OffersCurrency })
|
||||
| null;
|
||||
profile: OffersProfile & {
|
||||
background:
|
||||
| (OffersBackground & {
|
||||
experiences: Array<
|
||||
OffersExperience & { company: Company | null }
|
||||
>;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
}
|
||||
>;
|
||||
topOverallOffers: Array<
|
||||
OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern:
|
||||
| (OffersIntern & { monthlySalary: OffersCurrency })
|
||||
| null;
|
||||
profile: OffersProfile & {
|
||||
background:
|
||||
| (OffersBackground & {
|
||||
experiences: Array<
|
||||
OffersExperience & { company: Company | null }
|
||||
>;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
}
|
||||
>;
|
||||
})
|
||||
| null,
|
||||
) => {
|
||||
if (!analysis) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profileAnalysisDto: ProfileAnalysis = {
|
||||
companyAnalysis: [
|
||||
analysisDtoMapper(
|
||||
analysis.noOfSimilarCompanyOffers,
|
||||
analysis.companyPercentile,
|
||||
analysis.topCompanyOffers,
|
||||
),
|
||||
],
|
||||
id: analysis.id,
|
||||
overallAnalysis: analysisDtoMapper(
|
||||
analysis.noOfSimilarOffers,
|
||||
analysis.overallPercentile,
|
||||
analysis.topOverallOffers,
|
||||
),
|
||||
overallHighestOffer: analysisHighestOfferDtoMapper(
|
||||
analysis.overallHighestOffer,
|
||||
),
|
||||
profileId: analysis.profileId,
|
||||
};
|
||||
return profileAnalysisDto;
|
||||
};
|
||||
|
||||
export const valuationDtoMapper = (currency: {
|
||||
baseCurrency: string;
|
||||
baseValue: number;
|
||||
currency: string;
|
||||
id?: string;
|
||||
value: number;
|
||||
}) => {
|
||||
const valuationDto: Valuation = {
|
||||
baseCurrency: currency.baseCurrency,
|
||||
baseValue: currency.baseValue,
|
||||
currency: currency.currency,
|
||||
value: currency.value,
|
||||
};
|
||||
return valuationDto;
|
||||
};
|
||||
|
||||
export const offersCompanyDtoMapper = (company: Company) => {
|
||||
const companyDto: OffersCompany = {
|
||||
createdAt: company.createdAt,
|
||||
description: company?.description ?? '',
|
||||
id: company.id,
|
||||
logoUrl: company.logoUrl ?? '',
|
||||
name: company.name,
|
||||
slug: company.slug,
|
||||
updatedAt: company.updatedAt,
|
||||
};
|
||||
return companyDto;
|
||||
};
|
||||
|
||||
export const educationDtoMapper = (education: {
|
||||
backgroundId?: string;
|
||||
endDate: Date | null;
|
||||
field: string | null;
|
||||
id: string;
|
||||
school: string | null;
|
||||
startDate: Date | null;
|
||||
type: string | null;
|
||||
}) => {
|
||||
const educationDto: Education = {
|
||||
endDate: education.endDate,
|
||||
field: education.field,
|
||||
id: education.id,
|
||||
school: education.school,
|
||||
startDate: education.startDate,
|
||||
type: education.type,
|
||||
};
|
||||
return educationDto;
|
||||
};
|
||||
|
||||
export const experienceDtoMapper = (
|
||||
experience: OffersExperience & {
|
||||
company: Company | null;
|
||||
monthlySalary: OffersCurrency | null;
|
||||
totalCompensation: OffersCurrency | null;
|
||||
},
|
||||
) => {
|
||||
const experienceDto: Experience = {
|
||||
company: experience.company
|
||||
? offersCompanyDtoMapper(experience.company)
|
||||
: null,
|
||||
durationInMonths: experience.durationInMonths,
|
||||
id: experience.id,
|
||||
jobType: experience.jobType,
|
||||
level: experience.level,
|
||||
location: experience.location,
|
||||
monthlySalary: experience.monthlySalary
|
||||
? valuationDtoMapper(experience.monthlySalary)
|
||||
: experience.monthlySalary,
|
||||
specialization: experience.specialization,
|
||||
title: experience.title,
|
||||
totalCompensation: experience.totalCompensation
|
||||
? valuationDtoMapper(experience.totalCompensation)
|
||||
: experience.totalCompensation,
|
||||
};
|
||||
return experienceDto;
|
||||
};
|
||||
|
||||
export const specificYoeDtoMapper = (specificYoe: {
|
||||
backgroundId?: string;
|
||||
domain: string;
|
||||
id: string;
|
||||
yoe: number;
|
||||
}) => {
|
||||
const specificYoeDto: SpecificYoe = {
|
||||
domain: specificYoe.domain,
|
||||
id: specificYoe.id,
|
||||
yoe: specificYoe.yoe,
|
||||
};
|
||||
return specificYoeDto;
|
||||
};
|
||||
|
||||
export const backgroundDtoMapper = (
|
||||
background:
|
||||
| (OffersBackground & {
|
||||
educations: Array<OffersEducation>;
|
||||
experiences: Array<
|
||||
OffersExperience & {
|
||||
company: Company | null;
|
||||
monthlySalary: OffersCurrency | null;
|
||||
totalCompensation: OffersCurrency | null;
|
||||
}
|
||||
>;
|
||||
specificYoes: Array<OffersSpecificYoe>;
|
||||
})
|
||||
| null,
|
||||
) => {
|
||||
if (!background) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const educations = background.educations.map((education) =>
|
||||
educationDtoMapper(education),
|
||||
);
|
||||
|
||||
const experiences = background.experiences.map((experience) =>
|
||||
experienceDtoMapper(experience),
|
||||
);
|
||||
|
||||
const specificYoes = background.specificYoes.map((specificYoe) =>
|
||||
specificYoeDtoMapper(specificYoe),
|
||||
);
|
||||
|
||||
const backgroundDto: Background = {
|
||||
educations,
|
||||
experiences,
|
||||
id: background.id,
|
||||
specificYoes,
|
||||
totalYoe: background.totalYoe,
|
||||
};
|
||||
|
||||
return backgroundDto;
|
||||
};
|
||||
|
||||
export const profileOfferDtoMapper = (
|
||||
offer: OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & {
|
||||
baseSalary: OffersCurrency;
|
||||
bonus: OffersCurrency;
|
||||
stocks: OffersCurrency;
|
||||
totalCompensation: OffersCurrency;
|
||||
})
|
||||
| null;
|
||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||
},
|
||||
) => {
|
||||
const profileOfferDto: ProfileOffer = {
|
||||
comments: offer.comments,
|
||||
company: offersCompanyDtoMapper(offer.company),
|
||||
id: offer.id,
|
||||
jobType: offer.jobType,
|
||||
location: offer.location,
|
||||
monthYearReceived: offer.monthYearReceived,
|
||||
negotiationStrategy: offer.negotiationStrategy,
|
||||
offersFullTime: offer.offersFullTime,
|
||||
offersIntern: offer.offersIntern,
|
||||
};
|
||||
|
||||
if (offer.offersFullTime) {
|
||||
profileOfferDto.offersFullTime = {
|
||||
baseSalary: valuationDtoMapper(offer.offersFullTime.baseSalary),
|
||||
bonus: valuationDtoMapper(offer.offersFullTime.bonus),
|
||||
id: offer.offersFullTime.id,
|
||||
level: offer.offersFullTime.level,
|
||||
specialization: offer.offersFullTime.specialization,
|
||||
stocks: valuationDtoMapper(offer.offersFullTime.stocks),
|
||||
title: offer.offersFullTime.title,
|
||||
totalCompensation: valuationDtoMapper(
|
||||
offer.offersFullTime.totalCompensation,
|
||||
),
|
||||
};
|
||||
} else if (offer.offersIntern) {
|
||||
profileOfferDto.offersIntern = {
|
||||
id: offer.offersIntern.id,
|
||||
internshipCycle: offer.offersIntern.internshipCycle,
|
||||
monthlySalary: valuationDtoMapper(offer.offersIntern.monthlySalary),
|
||||
specialization: offer.offersIntern.specialization,
|
||||
startYear: offer.offersIntern.startYear,
|
||||
title: offer.offersIntern.title,
|
||||
};
|
||||
}
|
||||
|
||||
return profileOfferDto;
|
||||
};
|
||||
|
||||
export const profileDtoMapper = (
|
||||
profile: OffersProfile & {
|
||||
analysis:
|
||||
| (OffersAnalysis & {
|
||||
overallHighestOffer: OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern:
|
||||
| (OffersIntern & { monthlySalary: OffersCurrency })
|
||||
| null;
|
||||
profile: OffersProfile & { background: OffersBackground | null };
|
||||
};
|
||||
topCompanyOffers: Array<
|
||||
OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern:
|
||||
| (OffersIntern & { monthlySalary: OffersCurrency })
|
||||
| null;
|
||||
profile: OffersProfile & {
|
||||
background:
|
||||
| (OffersBackground & {
|
||||
experiences: Array<
|
||||
OffersExperience & { company: Company | null }
|
||||
>;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
}
|
||||
>;
|
||||
topOverallOffers: Array<
|
||||
OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||
| null;
|
||||
offersIntern:
|
||||
| (OffersIntern & { monthlySalary: OffersCurrency })
|
||||
| null;
|
||||
profile: OffersProfile & {
|
||||
background:
|
||||
| (OffersBackground & {
|
||||
experiences: Array<
|
||||
OffersExperience & { company: Company | null }
|
||||
>;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
}
|
||||
>;
|
||||
})
|
||||
| null;
|
||||
background:
|
||||
| (OffersBackground & {
|
||||
educations: Array<OffersEducation>;
|
||||
experiences: Array<
|
||||
OffersExperience & {
|
||||
company: Company | null;
|
||||
monthlySalary: OffersCurrency | null;
|
||||
totalCompensation: OffersCurrency | null;
|
||||
}
|
||||
>;
|
||||
specificYoes: Array<OffersSpecificYoe>;
|
||||
})
|
||||
| null;
|
||||
discussion: Array<
|
||||
OffersReply & {
|
||||
replies: Array<OffersReply>;
|
||||
replyingTo: OffersReply | null;
|
||||
user: User | null;
|
||||
}
|
||||
>;
|
||||
offers: Array<
|
||||
OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & {
|
||||
baseSalary: OffersCurrency;
|
||||
bonus: OffersCurrency;
|
||||
stocks: OffersCurrency;
|
||||
totalCompensation: OffersCurrency;
|
||||
})
|
||||
| null;
|
||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||
}
|
||||
>;
|
||||
},
|
||||
inputToken: string | undefined,
|
||||
) => {
|
||||
const profileDto: Profile = {
|
||||
analysis: profileAnalysisDtoMapper(profile.analysis),
|
||||
background: backgroundDtoMapper(profile.background),
|
||||
editToken: null,
|
||||
id: profile.id,
|
||||
isEditable: false,
|
||||
offers: profile.offers.map((offer) => profileOfferDtoMapper(offer)),
|
||||
profileName: profile.profileName,
|
||||
};
|
||||
|
||||
if (inputToken === profile.editToken) {
|
||||
profileDto.editToken = profile.editToken;
|
||||
profileDto.isEditable = true;
|
||||
}
|
||||
|
||||
return profileDto;
|
||||
};
|
||||
|
||||
export const createOfferProfileResponseMapper = (
|
||||
profile: { id: string },
|
||||
token: string,
|
||||
) => {
|
||||
const res: CreateOfferProfileResponse = {
|
||||
id: profile.id,
|
||||
token,
|
||||
};
|
||||
return res;
|
||||
};
|
||||
|
||||
export const addToProfileResponseMapper = (updatedProfile: {
|
||||
id: string;
|
||||
profileName: string;
|
||||
userId?: string | null;
|
||||
}) => {
|
||||
const addToProfileResponse: AddToProfileResponse = {
|
||||
id: updatedProfile.id,
|
||||
profileName: updatedProfile.profileName,
|
||||
userId: updatedProfile.userId ?? '',
|
||||
};
|
||||
|
||||
return addToProfileResponse;
|
||||
};
|
||||
|
||||
export const dashboardOfferDtoMapper = (
|
||||
offer: OffersOffer & {
|
||||
company: Company;
|
||||
offersFullTime:
|
||||
| (OffersFullTime & {
|
||||
baseSalary: OffersCurrency;
|
||||
bonus: OffersCurrency;
|
||||
stocks: OffersCurrency;
|
||||
totalCompensation: OffersCurrency;
|
||||
})
|
||||
| null;
|
||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||
profile: OffersProfile & { background: OffersBackground | null };
|
||||
},
|
||||
) => {
|
||||
const dashboardOfferDto: DashboardOffer = {
|
||||
company: offersCompanyDtoMapper(offer.company),
|
||||
id: offer.id,
|
||||
income: valuationDtoMapper({
|
||||
baseCurrency: '',
|
||||
baseValue: -1,
|
||||
currency: '',
|
||||
value: -1,
|
||||
}),
|
||||
monthYearReceived: offer.monthYearReceived,
|
||||
profileId: offer.profileId,
|
||||
title: offer.offersFullTime?.title ?? '',
|
||||
totalYoe: offer.profile.background?.totalYoe ?? -1,
|
||||
};
|
||||
|
||||
if (offer.offersFullTime) {
|
||||
dashboardOfferDto.income = valuationDtoMapper(
|
||||
offer.offersFullTime.totalCompensation,
|
||||
);
|
||||
} else if (offer.offersIntern) {
|
||||
dashboardOfferDto.income = valuationDtoMapper(
|
||||
offer.offersIntern.monthlySalary,
|
||||
);
|
||||
}
|
||||
|
||||
return dashboardOfferDto;
|
||||
};
|
||||
|
||||
export const getOffersResponseMapper = (
|
||||
data: Array<DashboardOffer>,
|
||||
paging: Paging,
|
||||
) => {
|
||||
const getOffersResponse: GetOffersResponse = {
|
||||
data,
|
||||
paging,
|
||||
};
|
||||
return getOffersResponse;
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import type { Session } from 'next-auth';
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
import React from 'react';
|
||||
import superjson from 'superjson';
|
||||
import { ToastsProvider } from '@tih/ui';
|
||||
import { httpBatchLink } from '@trpc/client/links/httpBatchLink';
|
||||
import { loggerLink } from '@trpc/client/links/loggerLink';
|
||||
import { withTRPC } from '@trpc/next';
|
||||
@@ -19,9 +20,11 @@ const MyApp: AppType<{ session: Session | null }> = ({
|
||||
}) => {
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<AppShell>
|
||||
<Component {...pageProps} />
|
||||
</AppShell>
|
||||
<ToastsProvider>
|
||||
<AppShell>
|
||||
<Component {...pageProps} />
|
||||
</AppShell>
|
||||
</ToastsProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
};
|
||||
|
||||