From 7052e8c175d69eb5db4d3b293e3056e76ef8016d Mon Sep 17 00:00:00 2001 From: BryannYeap Date: Mon, 10 Oct 2022 22:09:38 +0800 Subject: [PATCH 01/90] [offers][fix] Fix pagination, include yoe in DTO and fix story bug --- apps/portal/src/pages/offers/test.tsx | 24 +++++----- apps/portal/src/server/router/offers.ts | 58 +++++++++++++++++++++---- packages/ui/src/SlideOut/SlideOut.tsx | 2 +- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/apps/portal/src/pages/offers/test.tsx b/apps/portal/src/pages/offers/test.tsx index 5206935f..8537f04e 100644 --- a/apps/portal/src/pages/offers/test.tsx +++ b/apps/portal/src/pages/offers/test.tsx @@ -2,25 +2,29 @@ import React from 'react'; import { trpc } from '~/utils/trpc'; -function test() { +function Test() { const data = trpc.useQuery([ 'offers.list', { - limit: 3, + limit: 5, location: 'Singapore, Singapore', offset: 0, - sortBy: '-monthYearReceived', - yoeCategory: 0, + sortBy: '-totalYoe', + yoeCategory: 1, }, ]); return ( - + <> + +
+

{JSON.stringify(data.data?.paging)}

+ ); } -export default test; +export default Test; diff --git a/apps/portal/src/server/router/offers.ts b/apps/portal/src/server/router/offers.ts index 28e6c0eb..d2f640af 100644 --- a/apps/portal/src/server/router/offers.ts +++ b/apps/portal/src/server/router/offers.ts @@ -22,7 +22,7 @@ const getYoeRange = (yoeCategory: number) => { const ascOrder = '+'; const descOrder = '-'; -const sortingKeys = ['monthYearReceived', 'totalCompensation', 'yoe']; +const sortingKeys = ['monthYearReceived', 'totalCompensation', 'totalYoe']; const createSortByValidationRegex = () => { const startsWithPlusOrMinusOnly = '^[+-]{1}'; @@ -65,9 +65,12 @@ export const offersRouter = createRouter().query('list', { }, }, company: true, + profile: { + include: { + background: true, + }, + }, }, - skip: input.limit * input.offset, - take: input.limit, where: { AND: [ { @@ -103,10 +106,13 @@ export const offersRouter = createRouter().query('list', { }, }, company: true, + profile: { + include: { + background: true, + }, + }, }, // Junior, Mid - skip: input.limit * input.offset, - take: input.limit, where: { AND: [ { @@ -160,9 +166,12 @@ export const offersRouter = createRouter().query('list', { }, }, company: true, + profile: { + include: { + background: true, + }, + }, }, - skip: input.limit * input.offset, - take: input.limit, where: { AND: [ { @@ -259,6 +268,16 @@ export const offersRouter = createRouter().query('list', { return salary1 - salary2; } } + + if (sortingKey === 'totalYoe') { + const yoe1 = offer1.profile.background?.totalYoe; + const yoe2 = offer2.profile.background?.totalYoe; + + if (yoe1 && yoe2) { + return yoe1 - yoe2; + } + } + return defaultReturn; })(); } @@ -286,12 +305,35 @@ export const offersRouter = createRouter().query('list', { } } + if (sortingKey === 'totalYoe') { + const yoe1 = offer1.profile.background?.totalYoe; + const yoe2 = offer2.profile.background?.totalYoe; + + if (yoe1 && yoe2) { + return yoe2 - yoe1; + } + } + return defaultReturn; })(); } return defaultReturn; }); - return data; + const startRecordIndex: number = input.limit * input.offset; + const endRecordIndex: number = + startRecordIndex + input.limit <= data.length + ? startRecordIndex + input.limit + : data.length; + const paginatedData = data.slice(startRecordIndex, endRecordIndex); + + return { + data: paginatedData, + paging: { + currPage: input.offset, + numOfItemsInPage: paginatedData.length, + numOfPages: Math.ceil(data.length / input.limit), + }, + }; }, }); diff --git a/packages/ui/src/SlideOut/SlideOut.tsx b/packages/ui/src/SlideOut/SlideOut.tsx index 065c60dc..33dd456a 100644 --- a/packages/ui/src/SlideOut/SlideOut.tsx +++ b/packages/ui/src/SlideOut/SlideOut.tsx @@ -8,7 +8,7 @@ export type SlideOutEnterFrom = 'end' | 'start'; type Props = Readonly<{ children: React.ReactNode; - className: string; + className?: string; enterFrom?: SlideOutEnterFrom; isShown?: boolean; onClose?: () => void; From 50d33865923a7c031331d6beff4bc7f540635c56 Mon Sep 17 00:00:00 2001 From: Jeff Sieu Date: Mon, 10 Oct 2022 22:23:58 +0800 Subject: [PATCH 02/90] [question][ui] integrate backend voting (#355) Co-authored-by: wlren --- ...ListItem.tsx => AnswerCommentListItem.tsx} | 23 +- .../questions/ContributeQuestionCard.tsx | 2 +- .../questions/ContributeQuestionDialog.tsx | 4 +- .../questions/ContributeQuestionForm.tsx | 79 ++++-- .../questions/QuestionSearchBar.tsx | 45 ++-- .../questions/QuestionTypeBadge.tsx | 17 ++ .../components/questions/VotingButtons.tsx | 36 ++- .../components/questions/card/AnswerCard.tsx | 49 ++-- .../questions/card/FullAnswerCard.tsx | 43 +--- .../questions/card/FullQuestionCard.tsx | 74 ++---- .../questions/card/QuestionAnswerCard.tsx | 15 ++ .../questions/card/QuestionCard.tsx | 34 ++- .../questions/filter/FilterSection.tsx | 33 ++- .../questions/ui-patch/Checkbox.tsx | 25 -- .../questions/ui-patch/RadioGroup.tsx | 36 --- .../answer/[answerId]/[answerSlug]/index.tsx | 18 +- .../[questionId]/[questionSlug]/index.tsx | 33 +-- apps/portal/src/pages/questions/index.tsx | 233 +++++++++++------- .../router/questions-answer-comment-router.ts | 6 +- .../server/router/questions-answer-router.ts | 8 +- .../questions-question-comment-router.ts | 6 +- .../router/questions-question-router.ts | 4 +- apps/portal/src/types/questions.d.ts | 3 + .../src/utils/questions/useSearchFilter.ts | 20 +- apps/portal/src/utils/questions/useVote.ts | 175 +++++++++++++ 25 files changed, 639 insertions(+), 382 deletions(-) rename apps/portal/src/components/questions/{CommentListItem.tsx => AnswerCommentListItem.tsx} (58%) create mode 100644 apps/portal/src/components/questions/QuestionTypeBadge.tsx create mode 100644 apps/portal/src/components/questions/card/QuestionAnswerCard.tsx delete mode 100644 apps/portal/src/components/questions/ui-patch/Checkbox.tsx delete mode 100644 apps/portal/src/components/questions/ui-patch/RadioGroup.tsx create mode 100644 apps/portal/src/utils/questions/useVote.ts diff --git a/apps/portal/src/components/questions/CommentListItem.tsx b/apps/portal/src/components/questions/AnswerCommentListItem.tsx similarity index 58% rename from apps/portal/src/components/questions/CommentListItem.tsx rename to apps/portal/src/components/questions/AnswerCommentListItem.tsx index 1cf87d50..c65a379f 100644 --- a/apps/portal/src/components/questions/CommentListItem.tsx +++ b/apps/portal/src/components/questions/AnswerCommentListItem.tsx @@ -1,8 +1,11 @@ import { format } from 'date-fns'; +import { useAnswerCommentVote } from '~/utils/questions/useVote'; + import VotingButtons from './VotingButtons'; -export type CommentListItemProps = { +export type AnswerCommentListItemProps = { + answerCommentId: string; authorImageUrl: string; authorName: string; content: string; @@ -10,16 +13,26 @@ export type CommentListItemProps = { upvoteCount: number; }; -export default function CommentListItem({ +export default function AnswerCommentListItem({ authorImageUrl, authorName, content, createdAt, upvoteCount, -}: CommentListItemProps) { + answerCommentId, +}: AnswerCommentListItemProps) { + const { handleDownvote, handleUpvote, vote } = + useAnswerCommentVote(answerCommentId); + return (
- +

{authorName}

- Posted on: {format(createdAt, 'Pp')} + Posted on: {format(createdAt, 'h:mm a, MMMM dd, yyyy')}

{content}

diff --git a/apps/portal/src/components/questions/ContributeQuestionCard.tsx b/apps/portal/src/components/questions/ContributeQuestionCard.tsx index fa11f098..73faba27 100644 --- a/apps/portal/src/components/questions/ContributeQuestionCard.tsx +++ b/apps/portal/src/components/questions/ContributeQuestionCard.tsx @@ -40,7 +40,7 @@ export default function ContributeQuestionCard({ placeholder="Contribute a question" onChange={handleOpenContribute} /> -
+
-
+
-
+
diff --git a/apps/portal/src/components/questions/ContributeQuestionForm.tsx b/apps/portal/src/components/questions/ContributeQuestionForm.tsx index 367f0d88..5e230d73 100644 --- a/apps/portal/src/components/questions/ContributeQuestionForm.tsx +++ b/apps/portal/src/components/questions/ContributeQuestionForm.tsx @@ -1,12 +1,16 @@ +import { startOfMonth } from 'date-fns'; import { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { - BuildingOffice2Icon, - CalendarDaysIcon, - UserIcon, -} from '@heroicons/react/24/outline'; +import { Controller, useForm } from 'react-hook-form'; +import { CalendarDaysIcon, UserIcon } from '@heroicons/react/24/outline'; import type { QuestionsQuestionType } from '@prisma/client'; -import { Button, Collapsible, Select, TextArea, TextInput } from '@tih/ui'; +import { + Button, + CheckboxInput, + Collapsible, + Select, + TextArea, + TextInput, +} from '@tih/ui'; import { QUESTION_TYPES } from '~/utils/questions/constants'; import { @@ -14,7 +18,9 @@ import { useSelectRegister, } from '~/utils/questions/useFormRegister'; -import Checkbox from './ui-patch/Checkbox'; +import CompaniesTypeahead from '../shared/CompaniesTypeahead'; +import type { Month } from '../shared/MonthYearPicker'; +import MonthYearPicker from '../shared/MonthYearPicker'; export type ContributeQuestionData = { company: string; @@ -35,8 +41,15 @@ export default function ContributeQuestionForm({ onSubmit, onDiscard, }: ContributeQuestionFormProps) { - const { register: formRegister, handleSubmit } = - useForm(); + const { + control, + register: formRegister, + handleSubmit, + } = useForm({ + defaultValues: { + date: startOfMonth(new Date()), + }, + }); const register = useFormRegister(formRegister); const selectRegister = useSelectRegister(formRegister); @@ -66,24 +79,35 @@ export default function ContributeQuestionForm({ />
- ( + { + // TODO: To change from using company name to company id (i.e., value) + field.onChange(label); + }} + /> + )} />
- ( + + field.onChange(startOfMonth(new Date(year, month - 1))) + } + /> + )} />
@@ -130,10 +154,11 @@ export default function ContributeQuestionForm({
*/}
- + value={canSubmit} + onChange={handleCheckSimilarQuestions} + />
- + +
{JSON.stringify(data.data)}
+ + {/* */} + ); } diff --git a/apps/portal/src/server/router/offers-profile-router.ts b/apps/portal/src/server/router/offers-profile-router.ts index 3434d854..002117b4 100644 --- a/apps/portal/src/server/router/offers-profile-router.ts +++ b/apps/portal/src/server/router/offers-profile-router.ts @@ -50,7 +50,59 @@ const education = z.object({ type: z.string().optional(), }) -export const offersProfileRouter = createRouter().mutation( +export const offersProfileRouter = createRouter() + .query('listOne', { + input: z.object({ + profileId: z.string(), + }), + async resolve({ ctx, input }) { + return await ctx.prisma.offersProfile.findFirst({ + include: { + background: { + include: { + educations: true, + experiences: { + include: { + company: true, + monthlySalary: true, + totalCompensation: true + } + }, + specificYoes: true + } + }, + discussion: { + include: { + replies: true, + replyingTo: true + } + }, + offers: { + include: { + OffersFullTime: { + include: { + baseSalary: true, + bonus: true, + stocks: true, + totalCompensation: true + } + }, + OffersIntern: { + include: { + monthlySalary: true + } + }, + company: true + } + } + }, + where: { + id: input.profileId + } + }) + } + }) + .mutation( 'create', { input: z.object({ @@ -59,7 +111,7 @@ export const offersProfileRouter = createRouter().mutation( experiences: z.array(experience), specificYoes: z.array(z.object({ domain: z.string(), - yoe: z.number() + yoe: z.number(), })), totalYoe: z.number().optional(), }), @@ -136,10 +188,12 @@ export const offersProfileRouter = createRouter().mutation( }, specificYoes: { create: - input.background.specificYoes.map((x) => ({ - domain: x.domain, - yoe: x.yoe - })) + input.background.specificYoes.map((x) => { + return { + domain: x.domain, + yoe: x.yoe + } + }) }, totalYoe: input.background.totalYoe, } @@ -226,7 +280,7 @@ export const offersProfileRouter = createRouter().mutation( throw Prisma.PrismaClientKnownRequestError }) }, - profileName: randomUUID(), + profileName: randomUUID().substring(0,10), }, include: { background: { From b5c930ed6895c9477e41b7cb10629b37109ff812 Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Tue, 11 Oct 2022 06:44:29 +0800 Subject: [PATCH 05/90] [portal] disable react query refetch on window focus --- apps/portal/src/pages/_app.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/portal/src/pages/_app.tsx b/apps/portal/src/pages/_app.tsx index 5de4ac99..11606124 100644 --- a/apps/portal/src/pages/_app.tsx +++ b/apps/portal/src/pages/_app.tsx @@ -53,13 +53,18 @@ export default withTRPC({ }), httpBatchLink({ url }), ], - transformer: superjson, - url, /** - * @link https://react-query.tanstack.com/reference/QueryClient + * @link https://tanstack.com/query/v4/docs/reference/QueryClient */ - // queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } }, - + queryClientConfig: { + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + }, + }, + }, + transformer: superjson, + url, // To use SSR properly you need to forward the client's headers to the server // headers: () => { // if (ctx?.req) { From dfdd27cb855c8c3c5b777a874964680161e97cdf Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Tue, 11 Oct 2022 07:27:41 +0800 Subject: [PATCH 06/90] [infra] add GitHub actions for typechecking (#356) --- .github/workflows/tsc.yml | 37 +++++++++++++++++++++++++++++++++++++ apps/storybook/package.json | 2 +- package.json | 2 +- 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/tsc.yml diff --git a/.github/workflows/tsc.yml b/.github/workflows/tsc.yml new file mode 100644 index 00000000..784c4d76 --- /dev/null +++ b/.github/workflows/tsc.yml @@ -0,0 +1,37 @@ +# Copied from https://github.com/facebook/docusaurus/blob/main/.github/workflows/lint.yml +name: Typecheck + +on: + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + tsc: + name: Typecheck + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: '16' + cache: yarn + - name: Installation + run: yarn + - name: Check immutable yarn.lock + run: git diff --exit-code + # Build the shared types in dependent packages. + - name: Build dependencies + run: yarn turbo run build --filter=ui + - name: Typecheck + run: yarn tsc diff --git a/apps/storybook/package.json b/apps/storybook/package.json index df648014..546cf92c 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "start-storybook -p 6001", - "build": "build-storybook --docs", + "build": "tsc && build-storybook --docs", "preview-storybook": "serve storybook-static", "clean": "rm -rf .turbo && rm -rf node_modules", "lint": "eslint stories/**/*.ts* --fix", diff --git a/package.json b/package.json index b2b855c5..4c0a4f70 100755 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dev:ui": "turbo dev --filter=storybook... --filter=ui...", "dev:website": "turbo dev --filter=website...", "dev:all": "turbo dev --no-cache --parallel --continue", - "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,md,mdx}\"", "lint": "turbo lint", "test": "turbo test", "tsc": "turbo tsc" From 77ad8950986aebcd8ed5f966492e710d6bde7ae3 Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Tue, 11 Oct 2022 07:43:29 +0800 Subject: [PATCH 07/90] [infra] add GitHub actions for linting (#357) --- .github/workflows/lint.yml | 44 ++++++++++++++++++++++++++++++++++ apps/portal/src/env/schema.mjs | 6 ++++- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..7c2fd46d --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,44 @@ +# Copied from https://github.com/facebook/docusaurus/blob/main/.github/workflows/lint.yml +name: Lint + +on: + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +env: + DATABASE_URL: 'postgresql://postgres:password@localhost:5432/postgres' + GITHUB_CLIENT_ID: '1234' + GITHUB_CLIENT_SECRET: 'abcd' + NEXTAUTH_SECRET: 'efgh' + NEXTAUTH_URL: 'http://localhost:3000' + NODE_ENV: test + SUPABASE_ANON_KEY: 'ijkl' + SUPABASE_URL: 'https://abcd.supabase.co' + +jobs: + lint: + name: Lint + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: '16' + cache: yarn + - name: Installation + run: yarn + - name: Check immutable yarn.lock + run: git diff --exit-code + - name: Lint + run: yarn lint diff --git a/apps/portal/src/env/schema.mjs b/apps/portal/src/env/schema.mjs index 69064437..404f12cf 100644 --- a/apps/portal/src/env/schema.mjs +++ b/apps/portal/src/env/schema.mjs @@ -4,6 +4,8 @@ import { z } from 'zod'; /** * Specify your server-side environment variables schema here. * This way you can ensure the app isn't built with invalid env vars. + * + * Remember to update existing GitHub workflows that use env vars! */ export const serverSchema = z.object({ DATABASE_URL: z.string().url(), @@ -20,13 +22,15 @@ export const serverSchema = z.object({ * Specify your client-side environment variables schema here. * This way you can ensure the app isn't built with invalid env vars. * To expose them to the client, prefix them with `NEXT_PUBLIC_`. + * + * Remember to update existing GitHub workflows that use env vars! */ export const clientSchema = z.object({ // NEXT_PUBLIC_BAR: z.string(), }); /** - * You can't destruct `process.env` as a regular object, so you have to do + * You can't destructure `process.env` as a regular object, so you have to do * it manually here. This is because Next.js evaluates this at build time, * and only used environment variables are included in the build. * @type {{ [k in keyof z.infer]: z.infer[k] | undefined }} From d5edb6da604acad13cedeb59c84afe5914df440e Mon Sep 17 00:00:00 2001 From: BryannYeap Date: Tue, 11 Oct 2022 12:19:01 +0800 Subject: [PATCH 08/90] [offers][feat] Add delete OfferProfile API --- apps/portal/prisma/schema.prisma | 27 +- apps/portal/src/pages/offers/test.tsx | 34 +- .../src/pages/offers/testCreateProfile.tsx | 266 ++++---- .../server/router/offers-profile-router.ts | 628 +++++++++--------- apps/portal/src/server/router/offers.ts | 2 + 5 files changed, 517 insertions(+), 440 deletions(-) diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index 70a88985..1fba7c6a 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -201,7 +201,7 @@ model OffersBackground { educations OffersEducation[] // For extensibility in the future - profile OffersProfile @relation(fields: [offersProfileId], references: [id]) + profile OffersProfile @relation(fields: [offersProfileId], references: [id], onDelete: Cascade) offersProfileId String @unique } @@ -211,7 +211,7 @@ model OffersSpecificYoe { yoe Int domain String - background OffersBackground @relation(fields: [backgroundId], references: [id]) + background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) backgroundId String } @@ -237,7 +237,7 @@ model OffersExperience { monthlySalary OffersCurrency? @relation("ExperienceMonthlySalary", fields: [monthlySalaryId], references: [id]) monthlySalaryId String? @unique - background OffersBackground @relation(fields: [backgroundId], references: [id]) + background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) backgroundId String } @@ -270,12 +270,11 @@ model OffersEducation { type String? field String? - // Add more fields school String? startDate DateTime? endDate DateTime? - background OffersBackground @relation(fields: [backgroundId], references: [id]) + background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) backgroundId String } @@ -289,14 +288,14 @@ model OffersReply { replyingTo OffersReply? @relation("ReplyThread", fields: [replyingToId], references: [id]) replies OffersReply[] @relation("ReplyThread") - profile OffersProfile @relation(fields: [profileId], references: [id]) + profile OffersProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) profileId String } model OffersOffer { id String @id @default(cuid()) - profile OffersProfile @relation(fields: [profileId], references: [id]) + profile OffersProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) profileId String company Company @relation(fields: [companyId], references: [id]) @@ -309,10 +308,10 @@ model OffersOffer { jobType JobType - OffersIntern OffersIntern? @relation(fields: [offersInternId], references: [id]) + OffersIntern OffersIntern? @relation(fields: [offersInternId], references: [id], onDelete: Cascade) offersInternId String? @unique - OffersFullTime OffersFullTime? @relation(fields: [offersFullTimeId], references: [id]) + OffersFullTime OffersFullTime? @relation(fields: [offersFullTimeId], references: [id], onDelete: Cascade) offersFullTimeId String? @unique } @@ -323,7 +322,7 @@ model OffersIntern { specialization String internshipCycle String startYear Int - monthlySalary OffersCurrency @relation(fields: [monthlySalaryId], references: [id]) + monthlySalary OffersCurrency @relation(fields: [monthlySalaryId], references: [id], onDelete: Cascade) monthlySalaryId String @unique OffersOffer OffersOffer? @@ -334,13 +333,13 @@ model OffersFullTime { title String specialization String level String - totalCompensation OffersCurrency @relation("OfferTotalCompensation", fields: [totalCompensationId], references: [id]) + totalCompensation OffersCurrency @relation("OfferTotalCompensation", fields: [totalCompensationId], references: [id], onDelete: Cascade) totalCompensationId String @unique - baseSalary OffersCurrency @relation("OfferBaseSalary", fields: [baseSalaryId], references: [id]) + baseSalary OffersCurrency @relation("OfferBaseSalary", fields: [baseSalaryId], references: [id], onDelete: Cascade) baseSalaryId String @unique - bonus OffersCurrency @relation("OfferBonus", fields: [bonusId], references: [id]) + bonus OffersCurrency @relation("OfferBonus", fields: [bonusId], references: [id], onDelete: Cascade) bonusId String @unique - stocks OffersCurrency @relation("OfferStocks", fields: [stocksId], references: [id]) + stocks OffersCurrency @relation("OfferStocks", fields: [stocksId], references: [id], onDelete: Cascade) stocksId String @unique OffersOffer OffersOffer? diff --git a/apps/portal/src/pages/offers/test.tsx b/apps/portal/src/pages/offers/test.tsx index fa6880fc..e226fb97 100644 --- a/apps/portal/src/pages/offers/test.tsx +++ b/apps/portal/src/pages/offers/test.tsx @@ -14,7 +14,39 @@ function Test() { }, ]); - return <>{JSON.stringify(data.data)}; + const deleteMutation = trpc.useMutation(['offers.profile.delete']); + + const handleDelete = (id: string) => { + deleteMutation.mutate({ id }); + }; + + return ( +
    +
  • + {JSON.stringify(data.data?.paging)} +
  • +
  • +
      + {data.data?.data.map((offer) => { + return ( +
    • + +
      {JSON.stringify(offer)}
      +
      +
    • + ); + })} +
    +
  • +
+ ); } export default Test; diff --git a/apps/portal/src/pages/offers/testCreateProfile.tsx b/apps/portal/src/pages/offers/testCreateProfile.tsx index d2f9e496..d1fe399a 100644 --- a/apps/portal/src/pages/offers/testCreateProfile.tsx +++ b/apps/portal/src/pages/offers/testCreateProfile.tsx @@ -3,136 +3,142 @@ import React, { useState } from 'react'; import { trpc } from '~/utils/trpc'; function Test() { -// F const data = trpc.useQuery([ -// 'offers.profile.', -// { -// limit: 3, -// location: 'Singapore, Singapore', -// offset: 0, -// yoeCategory: 0, -// }, -// ]); + // F const data = trpc.useQuery([ + // 'offers.profile.', + // { + // limit: 3, + // location: 'Singapore, Singapore', + // offset: 0, + // yoeCategory: 0, + // }, + // ]); - const [createdData, setCreatedData] = useState("") + const [createdData, setCreatedData] = useState(''); - const createMutation = trpc.useMutation(['offers.profile.create'], { - onError(error: any) { - alert(error) + const createMutation = trpc.useMutation(['offers.profile.create'], { + onError(error: any) { + alert(error); + }, + onSuccess(data) { + setCreatedData(JSON.stringify(data)); + }, + }); + + const handleClick = () => { + createMutation.mutate({ + background: { + educations: [ + { + endDate: new Date('2018-09-30T07:58:54.000Z'), + field: 'Computer Science', + school: 'National University of Singapore', + startDate: new Date('2014-09-30T07:58:54.000Z'), + type: 'Bachelors', + }, + ], + experiences: [ + { + companyId: 'cl93m87pl0000tx1ofbafqz6f', + durationInMonths: 24, + jobType: 'FULLTIME', + level: 'Junior', + // "monthlySalary": undefined, + specialization: 'Front End', + title: 'Software Engineer', + totalCompensation: { + currency: 'SGD', + value: 104100, + }, + }, + ], + specificYoes: [ + { + domain: 'Front End', + yoe: 2, + }, + { + domain: 'Full Stack', + yoe: 2, + }, + ], + totalYoe: 4, + }, + offers: [ + { + comments: '', + companyId: 'cl93m87pl0000tx1ofbafqz6f', + job: { + base: { + currency: 'SGD', + value: 84000, + }, + bonus: { + currency: 'SGD', + value: 20000, + }, + level: 'Junior', + specialization: 'Front End', + stocks: { + currency: 'SGD', + value: 100, + }, + title: 'Software Engineer', + totalCompensation: { + currency: 'SGD', + value: 104100, + }, + }, + jobType: 'FULLTIME', + location: 'Singapore, Singapore', + monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), + negotiationStrategy: 'Leveraged having multiple offers', }, - onSuccess(data) { - setCreatedData(JSON.stringify(data)) + { + comments: '', + companyId: 'cl93m87pl0000tx1ofbafqz6f', + job: { + base: { + currency: 'SGD', + value: 84000, + }, + bonus: { + currency: 'SGD', + value: 20000, + }, + level: 'Junior', + specialization: 'Front End', + stocks: { + currency: 'SGD', + value: 100, + }, + title: 'Software Engineer', + totalCompensation: { + currency: 'SGD', + value: 104100, + }, + }, + jobType: 'FULLTIME', + location: 'Singapore, Singapore', + monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), + negotiationStrategy: 'Leveraged having multiple offers', }, + ], }); + }; - const handleClick = () => { - createMutation.mutate({ -"background": { - "educations": [ - { - "endDate": new Date("2018-09-30T07:58:54.000Z"), - "field": "Computer Science", - "school": "National University of Singapore", - "startDate": new Date("2014-09-30T07:58:54.000Z"), - "type": "Bachelors" - } - ], - "experiences": [ - { - "companyId": "cl92szctf0008i9nfxk54bhxn", - "durationInMonths": 24, - "jobType": "FULLTIME", - "level": "Junior", - // "monthlySalary": undefined, - "specialization": "Front End", - "title": "Software Engineer", - "totalCompensation": { - "currency": "SGD", - "value": 104100 - } - } - ], - "specificYoes": [ - { - "domain": "Front End", - "yoe": 2 - }, - { - "domain": "Full Stack", - "yoe": 2 - } - ], - "totalYoe": 4 - }, - "offers": [ + const profileId = 'cl92wiw30006vw3hg7dxa14fo'; // Remember to change this filed after testing deleting + const data = trpc.useQuery([ + `offers.profile.listOne`, { - "comments": "", - "companyId": "cl92szctf0008i9nfxk54bhxn", - "job": { - "base": { - "currency": "SGD", - "value": 84000 - }, - "bonus": { - "currency": "SGD", - "value": 20000 - }, - "level": "Junior", - "specialization": "Front End", - "stocks": { - "currency": "SGD", - "value": 100 - }, - "title": "Software Engineer", - "totalCompensation": { - "currency": "SGD", - "value": 104100 - } - }, - "jobType": "FULLTIME", - "location": "Singapore, Singapore", - "monthYearReceived": new Date("2022-09-30T07:58:54.000Z"), - "negotiationStrategy": "Leveraged having multiple offers" + profileId, }, - { - "comments": "", - "companyId": "cl92szctf0008i9nfxk54bhxn", - "job": { - "base": { - "currency": "SGD", - "value": 84000 - }, - "bonus": { - "currency": "SGD", - "value": 20000 - }, - "level": "Junior", - "specialization": "Front End", - "stocks": { - "currency": "SGD", - "value": 100 - }, - "title": "Software Engineer", - "totalCompensation": { - "currency": "SGD", - "value": 104100 - } - }, - "jobType": "FULLTIME", - "location": "Singapore, Singapore", - "monthYearReceived": new Date("2022-09-30T07:58:54.000Z"), - "negotiationStrategy": "Leveraged having multiple offers" - } - ] - }); - }; + ]); - const data = trpc.useQuery([ - `offers.profile.listOne`, - { - profileId: "cl92wc64a004gw3hgq4pfln2m" - } - ]) + const deleteMutation = trpc.useMutation(['offers.profile.delete']); + const handleDelete = (id: string) => { + deleteMutation.mutate({ id }); + }; return ( //
    @@ -140,14 +146,22 @@ function Test() { // return
  • {JSON.stringify(x)}
  • ; // })} //
- <> -
- {createdData} -
- + <> +
{createdData}
+ +
{JSON.stringify(data.data)}
- {/* */} + {/* */} ); } diff --git a/apps/portal/src/server/router/offers-profile-router.ts b/apps/portal/src/server/router/offers-profile-router.ts index 002117b4..85a0bd01 100644 --- a/apps/portal/src/server/router/offers-profile-router.ts +++ b/apps/portal/src/server/router/offers-profile-router.ts @@ -1,323 +1,353 @@ -import crypto, { randomUUID } from "crypto"; -import { z } from "zod"; -import { Prisma } from "@prisma/client"; +import crypto, { randomUUID } from 'crypto'; +import { z } from 'zod'; +import { Prisma } from '@prisma/client'; -import { createRouter } from "./context"; +import { createRouter } from './context'; const valuation = z.object({ - currency: z.string(), - value: z.number(), -}) + currency: z.string(), + value: z.number(), +}); // TODO: handle both full time and intern const offer = z.object({ - comments: z.string(), - companyId: z.string(), - job: z.object({ - base: valuation.optional(), // Full time - bonus: valuation.optional(), // Full time - internshipCycle: z.string().optional(), // Intern - level: z.string().optional(), // Full time - monthlySalary: valuation.optional(), // Intern - specialization: z.string(), - startYear: z.number().optional(), // Intern - stocks: valuation.optional(), // Full time - title: z.string(), - totalCompensation: valuation.optional(), // Full time - }), - jobType: z.string(), - location: z.string(), - monthYearReceived: z.date(), - negotiationStrategy: z.string(), -}) + comments: z.string(), + companyId: z.string(), + job: z.object({ + base: valuation.optional(), // Full time + bonus: valuation.optional(), // Full time + internshipCycle: z.string().optional(), // Intern + level: z.string().optional(), // Full time + monthlySalary: valuation.optional(), // Intern + specialization: z.string(), + startYear: z.number().optional(), // Intern + stocks: valuation.optional(), // Full time + title: z.string(), + totalCompensation: valuation.optional(), // Full time + }), + jobType: z.string(), + location: z.string(), + monthYearReceived: z.date(), + negotiationStrategy: z.string(), +}); const experience = z.object({ - companyId: z.string().optional(), - durationInMonths: z.number().optional(), - jobType: z.string().optional(), - level: z.string().optional(), - monthlySalary: valuation.optional(), - specialization: z.string().optional(), - title: z.string().optional(), - totalCompensation: valuation.optional(), -}) + companyId: z.string().optional(), + durationInMonths: z.number().optional(), + jobType: z.string().optional(), + level: z.string().optional(), + monthlySalary: valuation.optional(), + specialization: z.string().optional(), + title: z.string().optional(), + totalCompensation: valuation.optional(), +}); const education = z.object({ - endDate: z.date().optional(), - field: z.string().optional(), - school: z.string().optional(), - startDate: z.date().optional(), - type: z.string().optional(), -}) + endDate: z.date().optional(), + field: z.string().optional(), + school: z.string().optional(), + startDate: z.date().optional(), + type: z.string().optional(), +}); export const offersProfileRouter = createRouter() - .query('listOne', { - input: z.object({ - profileId: z.string(), - }), - async resolve({ ctx, input }) { - return await ctx.prisma.offersProfile.findFirst({ + .query('listOne', { + input: z.object({ + profileId: z.string(), + }), + async resolve({ ctx, input }) { + return await ctx.prisma.offersProfile.findFirst({ + include: { + background: { + include: { + educations: true, + experiences: { include: { - background: { - include: { - educations: true, - experiences: { - include: { - company: true, - monthlySalary: true, - totalCompensation: true - } - }, - specificYoes: true - } - }, - discussion: { - include: { - replies: true, - replyingTo: true - } - }, - offers: { - include: { - OffersFullTime: { - include: { - baseSalary: true, - bonus: true, - stocks: true, - totalCompensation: true - } - }, - OffersIntern: { - include: { - monthlySalary: true - } - }, - company: true - } - } + company: true, + monthlySalary: true, + totalCompensation: true, }, - where: { - id: input.profileId - } - }) - } - }) - .mutation( - 'create', - { - input: z.object({ - background: z.object({ - educations: z.array(education), - experiences: z.array(experience), - specificYoes: z.array(z.object({ - domain: z.string(), - yoe: z.number(), - })), - totalYoe: z.number().optional(), - }), - offers: z.array(offer) - }), - async resolve({ ctx, input }) { - - // TODO: add more - const token = crypto - .createHash("sha256") - .update(Date.now().toString()) - .digest("hex"); + }, + specificYoes: true, + }, + }, + discussion: { + include: { + replies: true, + replyingTo: true, + }, + }, + offers: { + include: { + OffersFullTime: { + include: { + baseSalary: true, + bonus: true, + stocks: true, + totalCompensation: true, + }, + }, + OffersIntern: { + include: { + monthlySalary: true, + }, + }, + company: true, + }, + }, + }, + where: { + id: input.profileId, + }, + }); + }, + }) + .mutation('create', { + input: z.object({ + background: z.object({ + educations: z.array(education), + experiences: z.array(experience), + specificYoes: z.array( + z.object({ + domain: z.string(), + yoe: z.number(), + }), + ), + totalYoe: z.number().optional(), + }), + offers: z.array(offer), + }), + async resolve({ ctx, input }) { + // TODO: add more + const token = crypto + .createHash('sha256') + .update(Date.now().toString()) + .digest('hex'); - const profile = await ctx.prisma.offersProfile.create({ - data: { - background: { + const profile = await ctx.prisma.offersProfile.create({ + data: { + background: { + create: { + educations: { + create: input.background.educations.map((x) => ({ + endDate: x.endDate, + field: x.field, + school: x.school, + startDate: x.startDate, + type: x.type, + })), + }, + experiences: { + create: input.background.experiences.map((x) => { + if ( + x.jobType === 'FULLTIME' && + x.totalCompensation?.currency !== undefined && + x.totalCompensation.value !== undefined + ) { + return { + company: { + connect: { + id: x.companyId, + }, + }, + durationInMonths: x.durationInMonths, + jobType: x.jobType, + level: x.level, + specialization: x.specialization, + title: x.title, + totalCompensation: { create: { - educations: { - create: - input.background.educations.map((x) => ({ - endDate: x.endDate, - field: x.field, - school: x.school, - startDate: x.startDate, - type: x.type - })) - }, - experiences: { - create: - input.background.experiences.map((x) => { - if (x.jobType === "FULLTIME" && x.totalCompensation?.currency !== undefined && x.totalCompensation.value !== undefined) { - return { - company: { - connect: { - id: x.companyId - } - }, - durationInMonths: x.durationInMonths, - jobType: x.jobType, - level: x.level, - specialization: x.specialization, - title: x.title, - totalCompensation: { - create: { - currency: x.totalCompensation?.currency, - value: x.totalCompensation?.value, - } - }, - } - } - if (x.jobType === "INTERN" && x.monthlySalary?.currency !== undefined && x.monthlySalary.value !== undefined) { - return { - company: { - connect: { - id: x.companyId - } - }, - durationInMonths: x.durationInMonths, - jobType: x.jobType, - monthlySalary: { - create: { - currency: x.monthlySalary?.currency, - value: x.monthlySalary?.value - } - }, - specialization: x.specialization, - title: x.title, - } - } - - throw Prisma.PrismaClientKnownRequestError + currency: x.totalCompensation?.currency, + value: x.totalCompensation?.value, + }, + }, + }; + } + if ( + x.jobType === 'INTERN' && + x.monthlySalary?.currency !== undefined && + x.monthlySalary.value !== undefined + ) { + return { + company: { + connect: { + id: x.companyId, + }, + }, + durationInMonths: x.durationInMonths, + jobType: x.jobType, + monthlySalary: { + create: { + currency: x.monthlySalary?.currency, + value: x.monthlySalary?.value, + }, + }, + specialization: x.specialization, + title: x.title, + }; + } - }) - }, - specificYoes: { - create: - input.background.specificYoes.map((x) => { - return { - domain: x.domain, - yoe: x.yoe - } - }) - }, - totalYoe: input.background.totalYoe, - } + throw Prisma.PrismaClientKnownRequestError; + }), + }, + specificYoes: { + create: input.background.specificYoes.map((x) => { + return { + domain: x.domain, + yoe: x.yoe, + }; + }), + }, + totalYoe: input.background.totalYoe, + }, + }, + editToken: token, + offers: { + create: input.offers.map((x) => { + if ( + x.jobType === 'INTERN' && + x.job.internshipCycle !== undefined && + x.job.monthlySalary?.currency !== undefined && + x.job.monthlySalary.value !== undefined && + x.job.startYear !== undefined + ) { + return { + OffersIntern: { + create: { + internshipCycle: x.job.internshipCycle, + monthlySalary: { + create: { + currency: x.job.monthlySalary?.currency, + value: x.job.monthlySalary?.value, + }, + }, + specialization: x.job.specialization, + startYear: x.job.startYear, + title: x.job.title, }, - editToken: token, - offers: { - create: - input.offers.map((x) => { - if (x.jobType === "INTERN" && x.job.internshipCycle !== undefined && x.job.monthlySalary?.currency !== undefined && x.job.monthlySalary.value !== undefined && x.job.startYear !== undefined) { - return { - OffersIntern: { - create: { - internshipCycle: x.job.internshipCycle, - monthlySalary: { - create: { - currency: x.job.monthlySalary?.currency, - value: x.job.monthlySalary?.value - } - }, - specialization: x.job.specialization, - startYear: x.job.startYear, - title: x.job.title, - } - }, - comments: x.comments, - company: { - connect: { - id: x.companyId - } - }, - jobType: x.jobType, - location: x.location, - monthYearReceived: x.monthYearReceived, - negotiationStrategy: x.negotiationStrategy - } - } - if (x.jobType === "FULLTIME" && x.job.base?.currency !== undefined && x.job.base?.value !== undefined && x.job.bonus?.currency !== undefined && x.job.bonus?.value !== undefined && x.job.stocks?.currency !== undefined && x.job.stocks?.value !== undefined && x.job.totalCompensation?.currency !== undefined && x.job.totalCompensation?.value !== undefined && x.job.level !== undefined) { - return { - OffersFullTime: { - create: { - baseSalary: { - create: { - currency: x.job.base?.currency, - value: x.job.base?.value - } - }, - bonus: { - create: { - currency: x.job.bonus?.currency, - value: x.job.bonus?.value - } - }, - level: x.job.level, - specialization: x.job.specialization, - stocks: { - create: { - currency: x.job.stocks?.currency, - value: x.job.stocks?.value, - } - }, - title: x.job.title, - totalCompensation: { - create: { - currency: x.job.totalCompensation?.currency, - value: x.job.totalCompensation?.value, - } - }, - } - }, - comments: x.comments, - company: { - connect: { - id: x.companyId - } - }, - jobType: x.jobType, - location: x.location, - monthYearReceived: x.monthYearReceived, - negotiationStrategy: x.negotiationStrategy - } - } - - // Throw error - throw Prisma.PrismaClientKnownRequestError - }) + }, + comments: x.comments, + company: { + connect: { + id: x.companyId, + }, + }, + jobType: x.jobType, + location: x.location, + monthYearReceived: x.monthYearReceived, + negotiationStrategy: x.negotiationStrategy, + }; + } + if ( + x.jobType === 'FULLTIME' && + x.job.base?.currency !== undefined && + x.job.base?.value !== undefined && + x.job.bonus?.currency !== undefined && + x.job.bonus?.value !== undefined && + x.job.stocks?.currency !== undefined && + x.job.stocks?.value !== undefined && + x.job.totalCompensation?.currency !== undefined && + x.job.totalCompensation?.value !== undefined && + x.job.level !== undefined + ) { + return { + OffersFullTime: { + create: { + baseSalary: { + create: { + currency: x.job.base?.currency, + value: x.job.base?.value, + }, + }, + bonus: { + create: { + currency: x.job.bonus?.currency, + value: x.job.bonus?.value, + }, + }, + level: x.job.level, + specialization: x.job.specialization, + stocks: { + create: { + currency: x.job.stocks?.currency, + value: x.job.stocks?.value, + }, + }, + title: x.job.title, + totalCompensation: { + create: { + currency: x.job.totalCompensation?.currency, + value: x.job.totalCompensation?.value, + }, + }, + }, + }, + comments: x.comments, + company: { + connect: { + id: x.companyId, }, - profileName: randomUUID().substring(0,10), + }, + jobType: x.jobType, + location: x.location, + monthYearReceived: x.monthYearReceived, + negotiationStrategy: x.negotiationStrategy, + }; + } + + // Throw error + throw Prisma.PrismaClientKnownRequestError; + }), + }, + profileName: randomUUID().substring(0, 10), + }, + include: { + background: { + include: { + educations: true, + experiences: { + include: { + company: true, + monthlySalary: true, + totalCompensation: true, }, + }, + specificYoes: true, + }, + }, + offers: { + include: { + OffersFullTime: { include: { - background: { - include: { - educations: true, - experiences: { - include: { - company: true, - monthlySalary: true, - totalCompensation: true - } - }, - specificYoes: true - } - }, - offers: { - include: { - OffersFullTime: { - include: { - baseSalary: true, - bonus: true, - stocks: true, - totalCompensation: true - } - }, - OffersIntern: { - include: { - monthlySalary: true - } - } - } - } + baseSalary: true, + bonus: true, + stocks: true, + totalCompensation: true, }, - }); + }, + OffersIntern: { + include: { + monthlySalary: true, + }, + }, + }, + }, + }, + }); - // TODO: add analysis to profile object then return - return profile - } + // TODO: add analysis to profile object then return + return profile; + }, + }) + .mutation('delete', { + input: z.object({ + id: z.string(), + }), + async resolve({ ctx, input }) { + return await ctx.prisma.offersProfile.delete({ + where: { + id: input.id, + }, + }); }, -); + }); diff --git a/apps/portal/src/server/router/offers.ts b/apps/portal/src/server/router/offers.ts index a4332d05..2d48f28c 100644 --- a/apps/portal/src/server/router/offers.ts +++ b/apps/portal/src/server/router/offers.ts @@ -200,6 +200,7 @@ export const offersRouter = createRouter().query('list', { }, }); + // FILTERING data = data.filter((offer) => { let validRecord = true; @@ -235,6 +236,7 @@ export const offersRouter = createRouter().query('list', { return validRecord; }); + // SORTING data = data.sort((offer1, offer2) => { const defaultReturn = offer2.monthYearReceived.getTime() - offer1.monthYearReceived.getTime(); From f88e8e8409f59dd5279c54d11d1fa855f7024394 Mon Sep 17 00:00:00 2001 From: BryannYeap Date: Tue, 11 Oct 2022 12:22:57 +0800 Subject: [PATCH 09/90] [offers][chore] Update migration records --- .../migrations/20221011042156_/migration.sql | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 apps/portal/prisma/migrations/20221011042156_/migration.sql diff --git a/apps/portal/prisma/migrations/20221011042156_/migration.sql b/apps/portal/prisma/migrations/20221011042156_/migration.sql new file mode 100644 index 00000000..07cf1b58 --- /dev/null +++ b/apps/portal/prisma/migrations/20221011042156_/migration.sql @@ -0,0 +1,120 @@ +/* + Warnings: + + - You are about to drop the column `isAttending` on the `OffersEducation` table. All the data in the column will be lost. + - The primary key for the `OffersFullTime` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `offerId` on the `OffersFullTime` table. All the data in the column will be lost. + - The primary key for the `OffersIntern` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `offerId` on the `OffersIntern` table. All the data in the column will be lost. + - A unique constraint covering the columns `[offersInternId]` on the table `OffersOffer` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[offersFullTimeId]` on the table `OffersOffer` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[profileName]` on the table `OffersProfile` will be added. If there are existing duplicate values, this will fail. + - The required column `id` was added to the `OffersFullTime` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. + - The required column `id` was added to the `OffersIntern` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. + +*/ +-- DropForeignKey +ALTER TABLE "OffersBackground" DROP CONSTRAINT "OffersBackground_offersProfileId_fkey"; + +-- DropForeignKey +ALTER TABLE "OffersEducation" DROP CONSTRAINT "OffersEducation_backgroundId_fkey"; + +-- DropForeignKey +ALTER TABLE "OffersExperience" DROP CONSTRAINT "OffersExperience_backgroundId_fkey"; + +-- DropForeignKey +ALTER TABLE "OffersFullTime" DROP CONSTRAINT "OffersFullTime_baseSalaryId_fkey"; + +-- DropForeignKey +ALTER TABLE "OffersFullTime" DROP CONSTRAINT "OffersFullTime_bonusId_fkey"; + +-- DropForeignKey +ALTER TABLE "OffersFullTime" DROP CONSTRAINT "OffersFullTime_offerId_fkey"; + +-- DropForeignKey +ALTER TABLE "OffersFullTime" DROP CONSTRAINT "OffersFullTime_stocksId_fkey"; + +-- DropForeignKey +ALTER TABLE "OffersFullTime" DROP CONSTRAINT "OffersFullTime_totalCompensationId_fkey"; + +-- DropForeignKey +ALTER TABLE "OffersIntern" DROP CONSTRAINT "OffersIntern_monthlySalaryId_fkey"; + +-- DropForeignKey +ALTER TABLE "OffersIntern" DROP CONSTRAINT "OffersIntern_offerId_fkey"; + +-- DropForeignKey +ALTER TABLE "OffersOffer" DROP CONSTRAINT "OffersOffer_profileId_fkey"; + +-- DropForeignKey +ALTER TABLE "OffersReply" DROP CONSTRAINT "OffersReply_profileId_fkey"; + +-- DropForeignKey +ALTER TABLE "OffersSpecificYoe" DROP CONSTRAINT "OffersSpecificYoe_backgroundId_fkey"; + +-- AlterTable +ALTER TABLE "OffersEducation" DROP COLUMN "isAttending"; + +-- AlterTable +ALTER TABLE "OffersFullTime" DROP CONSTRAINT "OffersFullTime_pkey", +DROP COLUMN "offerId", +ADD COLUMN "id" TEXT NOT NULL, +ADD CONSTRAINT "OffersFullTime_pkey" PRIMARY KEY ("id"); + +-- AlterTable +ALTER TABLE "OffersIntern" DROP CONSTRAINT "OffersIntern_pkey", +DROP COLUMN "offerId", +ADD COLUMN "id" TEXT NOT NULL, +ADD CONSTRAINT "OffersIntern_pkey" PRIMARY KEY ("id"); + +-- AlterTable +ALTER TABLE "OffersOffer" ADD COLUMN "offersFullTimeId" TEXT, +ADD COLUMN "offersInternId" TEXT; + +-- CreateIndex +CREATE UNIQUE INDEX "OffersOffer_offersInternId_key" ON "OffersOffer"("offersInternId"); + +-- CreateIndex +CREATE UNIQUE INDEX "OffersOffer_offersFullTimeId_key" ON "OffersOffer"("offersFullTimeId"); + +-- CreateIndex +CREATE UNIQUE INDEX "OffersProfile_profileName_key" ON "OffersProfile"("profileName"); + +-- AddForeignKey +ALTER TABLE "OffersBackground" ADD CONSTRAINT "OffersBackground_offersProfileId_fkey" FOREIGN KEY ("offersProfileId") REFERENCES "OffersProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OffersSpecificYoe" ADD CONSTRAINT "OffersSpecificYoe_backgroundId_fkey" FOREIGN KEY ("backgroundId") REFERENCES "OffersBackground"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OffersExperience" ADD CONSTRAINT "OffersExperience_backgroundId_fkey" FOREIGN KEY ("backgroundId") REFERENCES "OffersBackground"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OffersEducation" ADD CONSTRAINT "OffersEducation_backgroundId_fkey" FOREIGN KEY ("backgroundId") REFERENCES "OffersBackground"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OffersReply" ADD CONSTRAINT "OffersReply_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "OffersProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OffersOffer" ADD CONSTRAINT "OffersOffer_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "OffersProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OffersOffer" ADD CONSTRAINT "OffersOffer_offersInternId_fkey" FOREIGN KEY ("offersInternId") REFERENCES "OffersIntern"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OffersOffer" ADD CONSTRAINT "OffersOffer_offersFullTimeId_fkey" FOREIGN KEY ("offersFullTimeId") REFERENCES "OffersFullTime"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OffersIntern" ADD CONSTRAINT "OffersIntern_monthlySalaryId_fkey" FOREIGN KEY ("monthlySalaryId") REFERENCES "OffersCurrency"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_totalCompensationId_fkey" FOREIGN KEY ("totalCompensationId") REFERENCES "OffersCurrency"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_baseSalaryId_fkey" FOREIGN KEY ("baseSalaryId") REFERENCES "OffersCurrency"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_bonusId_fkey" FOREIGN KEY ("bonusId") REFERENCES "OffersCurrency"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_stocksId_fkey" FOREIGN KEY ("stocksId") REFERENCES "OffersCurrency"("id") ON DELETE CASCADE ON UPDATE CASCADE; From 0822bee33b1bba6974d14358b5ed41e79e32f546 Mon Sep 17 00:00:00 2001 From: Stuart Long Chay Boon Date: Tue, 11 Oct 2022 13:15:22 +0800 Subject: [PATCH 10/90] [offers][fix] modify create profile endpoint to accept optional --- apps/portal/src/pages/offers/testCreateProfile.tsx | 14 +++++++------- .../src/server/router/offers-profile-router.ts | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/portal/src/pages/offers/testCreateProfile.tsx b/apps/portal/src/pages/offers/testCreateProfile.tsx index d1fe399a..8528cdac 100644 --- a/apps/portal/src/pages/offers/testCreateProfile.tsx +++ b/apps/portal/src/pages/offers/testCreateProfile.tsx @@ -38,7 +38,7 @@ function Test() { ], experiences: [ { - companyId: 'cl93m87pl0000tx1ofbafqz6f', + companyId: 'cl93patjt0003txewyiaky7xx', durationInMonths: 24, jobType: 'FULLTIME', level: 'Junior', @@ -65,8 +65,8 @@ function Test() { }, offers: [ { - comments: '', - companyId: 'cl93m87pl0000tx1ofbafqz6f', + // Comments: '', + companyId: 'cl93patjt0003txewyiaky7xx', job: { base: { currency: 'SGD', @@ -94,8 +94,8 @@ function Test() { negotiationStrategy: 'Leveraged having multiple offers', }, { - comments: '', - companyId: 'cl93m87pl0000tx1ofbafqz6f', + comments: undefined, + companyId: 'cl93patjt0003txewyiaky7xx', job: { base: { currency: 'SGD', @@ -120,13 +120,13 @@ function Test() { jobType: 'FULLTIME', location: 'Singapore, Singapore', monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), - negotiationStrategy: 'Leveraged having multiple offers', + // NegotiationStrategy: 'Leveraged having multiple offers', }, ], }); }; - const profileId = 'cl92wiw30006vw3hg7dxa14fo'; // Remember to change this filed after testing deleting + const profileId = 'cl93qtuyc0000w3ideermqtcz'; // Remember to change this filed after testing deleting const data = trpc.useQuery([ `offers.profile.listOne`, { diff --git a/apps/portal/src/server/router/offers-profile-router.ts b/apps/portal/src/server/router/offers-profile-router.ts index 85a0bd01..aa4261e5 100644 --- a/apps/portal/src/server/router/offers-profile-router.ts +++ b/apps/portal/src/server/router/offers-profile-router.ts @@ -11,7 +11,7 @@ const valuation = z.object({ // TODO: handle both full time and intern const offer = z.object({ - comments: z.string(), + comments: z.string().optional(), companyId: z.string(), job: z.object({ base: valuation.optional(), // Full time @@ -28,7 +28,7 @@ const offer = z.object({ jobType: z.string(), location: z.string(), monthYearReceived: z.date(), - negotiationStrategy: z.string(), + negotiationStrategy: z.string().optional(), }); const experience = z.object({ From 34c8c7d60524fd5ee8377041adb3934f762b3ab8 Mon Sep 17 00:00:00 2001 From: Stuart Long Chay Boon Date: Tue, 11 Oct 2022 15:02:16 +0800 Subject: [PATCH 11/90] [offers][chore] add editToken check for backend --- .../src/pages/offers/testCreateProfile.tsx | 3 +- .../server/router/offers-profile-router.ts | 110 +++++++++-------- apps/portal/src/types/offers-profile.d.ts | 113 ++++++++++++++++++ 3 files changed, 178 insertions(+), 48 deletions(-) create mode 100644 apps/portal/src/types/offers-profile.d.ts diff --git a/apps/portal/src/pages/offers/testCreateProfile.tsx b/apps/portal/src/pages/offers/testCreateProfile.tsx index 8528cdac..fea8cdcc 100644 --- a/apps/portal/src/pages/offers/testCreateProfile.tsx +++ b/apps/portal/src/pages/offers/testCreateProfile.tsx @@ -126,11 +126,12 @@ function Test() { }); }; - const profileId = 'cl93qtuyc0000w3ideermqtcz'; // Remember to change this filed after testing deleting + const profileId = 'cl93tvejz00bei9qinzmjgy75'; // Remember to change this filed after testing deleting const data = trpc.useQuery([ `offers.profile.listOne`, { profileId, + token: "6c8d53530163bb765c42bd9f441aa7e345f607c4e1892edbc64e5bbbbe7ee916" }, ]); diff --git a/apps/portal/src/server/router/offers-profile-router.ts b/apps/portal/src/server/router/offers-profile-router.ts index aa4261e5..cdd01558 100644 --- a/apps/portal/src/server/router/offers-profile-router.ts +++ b/apps/portal/src/server/router/offers-profile-router.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { Prisma } from '@prisma/client'; import { createRouter } from './context'; +import type { offersProfile } from '../../types/offers-profile'; const valuation = z.object({ currency: z.string(), @@ -53,54 +54,70 @@ const education = z.object({ export const offersProfileRouter = createRouter() .query('listOne', { input: z.object({ - profileId: z.string(), + profileId: z.string(), + token: z.string().optional() }), - async resolve({ ctx, input }) { - return await ctx.prisma.offersProfile.findFirst({ - include: { - background: { - include: { - educations: true, - experiences: { - include: { - company: true, - monthlySalary: true, - totalCompensation: true, - }, - }, - specificYoes: true, - }, - }, - discussion: { - include: { - replies: true, - replyingTo: true, - }, - }, - offers: { - include: { - OffersFullTime: { - include: { - baseSalary: true, - bonus: true, - stocks: true, - totalCompensation: true, - }, - }, - OffersIntern: { - include: { - monthlySalary: true, - }, + async resolve({ ctx, input }) { + const result = await ctx.prisma.offersProfile.findFirst({ + include: { + background: { + include: { + educations: true, + experiences: { + include: { + company: true, + monthlySalary: true, + totalCompensation: true, + }, + }, + specificYoes: true, + }, + }, + discussion: { + include: { + replies: true, + replyingTo: true, + }, + }, + offers: { + include: { + OffersFullTime: { + include: { + baseSalary: true, + bonus: true, + stocks: true, + totalCompensation: true, + }, + }, + OffersIntern: { + include: { + monthlySalary: true, + }, + }, + company: true, + }, + }, }, - company: true, - }, - }, - }, - where: { - id: input.profileId, - }, - }); - }, + where: { + id: input.profileId, + } + }); + // Extend the T generic with the fullName attribute + type WithIsEditable = T & { + isEditable: boolean + } + + // Take objects that satisfy FirstLastName and computes a full name + function computeIsEditable( + profileInput: offersProfile + ): WithIsEditable { + return { + ...profileInput, + isEditable: profileInput["editToken" as keyof typeof profileInput] === input.token, + } + } + return result ? computeIsEditable(result) : result; + }, }) .mutation('create', { input: z.object({ @@ -334,7 +351,6 @@ export const offersProfileRouter = createRouter() }, }, }); - // TODO: add analysis to profile object then return return profile; }, diff --git a/apps/portal/src/types/offers-profile.d.ts b/apps/portal/src/types/offers-profile.d.ts new file mode 100644 index 00000000..0990612a --- /dev/null +++ b/apps/portal/src/types/offers-profile.d.ts @@ -0,0 +1,113 @@ +export type offersProfile = { + background?: background | null; + createdAt: Date; +// Discussions: Array; + editToken: string; + id: string; + offers: Array; + profileName: string; + userId?: string | null; +}; + +export type background = { + educations: Array; + experiences: Array; + id: string; + offersProfileId: string; + specificYoes: Array; + totalYoe?: number | null; +} + +export type experience = { + backgroundId: string; + company?: company | null; + companyId?: string | null; + durationInMonths?: number | null; + id: string; + jobType?: string | null; + level?: string | null; + monthlySalary?: valuation | null; + monthlySalaryId?: string | null; + specialization?: string | null; + title?: string | null; + totalCompensation?: valuation | null; + totalCompensationId?: string | null; +} + +export type company = { + createdAt: Date; + description: string | null; + id: string; + logoUrl: string | null; + name: string; + slug: string; + updatedAt: Date +} + +export type valuation = { + currency: string; + id: string; + value: number; +} + +export type education = { + backgroundId: string; + endDate?: Date | null; + field?: string | null; + id: string; + school?: string | null; + startDate?: Date | null; + type?: string | null; +} + +export type specificYoe = { + backgroundId: string; + domain: string; + id: string; + yoe: number; +} + +export type offers = { + OffersFullTime?: offersFullTime | null; + OffersIntern?: offersIntern | null; + comments?: string | null; + company: company; + companyId: string; + id: string; + jobType: string; + location: string; + monthYearReceived: string; + negotiationStrategy?: string | null; + offersFullTimeId?: string | null; + offersInternId?: string | null; + profileId: string; +} + +export type offersFullTime = { + baseSalary: valuation; + baseSalaryId: string; + bonus: valuation; + bonusId: string; + id: string; + level: string; + specialization: string; + stocks: valuation; + stocksId: string; + title?: string | null; + totalCompensation: valuation; + totalCompensationId: string; +} + +export type offersIntern = { + id: string; + internshipCycle: string; + monthlySalary: valuation; + monthlySalaryId: string; + specialization: string; + startYear: number; +} + +// TODO: fill in next time +export type discussion = { + id: string; +} \ No newline at end of file From 4330fb54481638342d39ed3279d16bac4133854b Mon Sep 17 00:00:00 2001 From: Ai Ling <50992674+ailing35@users.noreply.github.com> Date: Tue, 11 Oct 2022 15:08:44 +0800 Subject: [PATCH 12/90] [offers][feat] Integrate offers create API and fix form UI (#358) --- .../src/components/offers/OffersTable.tsx | 2 +- .../portal/src/components/offers/constants.ts | 20 +-- .../offers/forms/BackgroundForm.tsx | 64 +++++---- .../components/offers/forms/OfferAnalysis.tsx | 3 +- .../offers/forms/OfferDetailsForm.tsx | 101 +++++++++------ .../forms/components/FormMonthYearPicker.tsx | 37 ++++++ .../forms/{ => components}/FormRadioList.tsx | 7 +- .../forms/{ => components}/FormSelect.tsx | 0 .../forms/{ => components}/FormTextArea.tsx | 0 .../forms/{ => components}/FormTextInput.tsx | 0 apps/portal/src/components/offers/types.ts | 36 +++--- .../src/components/offers/util/time/index.tsx | 7 - apps/portal/src/pages/offers/index.tsx | 25 +--- apps/portal/src/pages/offers/submit.tsx | 122 ++++++++++++------ .../offers}/currency/CurrencyEnum.tsx | 0 .../offers}/currency/CurrencySelector.tsx | 2 +- apps/portal/src/utils/offers/form.tsx | 56 ++++++++ apps/portal/src/utils/offers/time.tsx | 25 ++++ 18 files changed, 345 insertions(+), 162 deletions(-) create mode 100644 apps/portal/src/components/offers/forms/components/FormMonthYearPicker.tsx rename apps/portal/src/components/offers/forms/{ => components}/FormRadioList.tsx (69%) rename apps/portal/src/components/offers/forms/{ => components}/FormSelect.tsx (100%) rename apps/portal/src/components/offers/forms/{ => components}/FormTextArea.tsx (100%) rename apps/portal/src/components/offers/forms/{ => components}/FormTextInput.tsx (100%) delete mode 100644 apps/portal/src/components/offers/util/time/index.tsx rename apps/portal/src/{components/offers/util => utils/offers}/currency/CurrencyEnum.tsx (100%) rename apps/portal/src/{components/offers/util => utils/offers}/currency/CurrencySelector.tsx (89%) create mode 100644 apps/portal/src/utils/offers/form.tsx create mode 100644 apps/portal/src/utils/offers/time.tsx diff --git a/apps/portal/src/components/offers/OffersTable.tsx b/apps/portal/src/components/offers/OffersTable.tsx index ded79547..5ef39748 100644 --- a/apps/portal/src/components/offers/OffersTable.tsx +++ b/apps/portal/src/components/offers/OffersTable.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { HorizontalDivider, Pagination, Select, Tabs } from '@tih/ui'; -import CurrencySelector from '~/components/offers/util/currency/CurrencySelector'; +import CurrencySelector from '~/utils/offers/currency/CurrencySelector'; type TableRow = { company: string; diff --git a/apps/portal/src/components/offers/constants.ts b/apps/portal/src/components/offers/constants.ts index 2a09e9b5..a5dc2706 100644 --- a/apps/portal/src/components/offers/constants.ts +++ b/apps/portal/src/components/offers/constants.ts @@ -29,24 +29,24 @@ export const titleOptions = [ export const companyOptions = [ emptyOption, { - label: 'Bytedance', - value: 'id-abc123', + label: 'Amazon', + value: 'cl93patjt0000txewdi601mub', }, { - label: 'Google', - value: 'id-abc567', + label: 'Microsoft', + value: 'cl93patjt0001txewkglfjsro', }, { - label: 'Meta', - value: 'id-abc456', + label: 'Apple', + value: 'cl93patjt0002txewf3ug54m8', }, { - label: 'Shopee', - value: 'id-abc345', + label: 'Google', + value: 'cl93patjt0003txewyiaky7xx', }, { - label: 'Tik Tok', - value: 'id-abc678', + label: 'Meta', + value: 'cl93patjt0004txew88wkcqpu', }, ]; diff --git a/apps/portal/src/components/offers/forms/BackgroundForm.tsx b/apps/portal/src/components/offers/forms/BackgroundForm.tsx index 0acbbe69..171823f1 100644 --- a/apps/portal/src/components/offers/forms/BackgroundForm.tsx +++ b/apps/portal/src/components/offers/forms/BackgroundForm.tsx @@ -1,9 +1,9 @@ import { useFormContext, useWatch } from 'react-hook-form'; import { Collapsible, RadioList } from '@tih/ui'; -import FormRadioList from './FormRadioList'; -import FormSelect from './FormSelect'; -import FormTextInput from './FormTextInput'; +import FormRadioList from './components/FormRadioList'; +import FormSelect from './components/FormSelect'; +import FormTextInput from './components/FormTextInput'; import { companyOptions, educationFieldOptions, @@ -12,7 +12,7 @@ import { titleOptions, } from '../constants'; import { JobType } from '../types'; -import { CURRENCY_OPTIONS } from '../util/currency/CurrencyEnum'; +import { CURRENCY_OPTIONS } from '../../../utils/offers/currency/CurrencyEnum'; function YoeSection() { const { register } = useFormContext(); @@ -28,7 +28,9 @@ function YoeSection() { label="Total YOE" placeholder="0" type="number" - {...register(`background.totalYoe`)} + {...register(`background.totalYoe`, { + valueAsNumber: true, + })} />
@@ -37,7 +39,9 @@ function YoeSection() {
@@ -90,7 +96,9 @@ function FullTimeJobFields() { isLabelHidden={true} label="Currency" options={CURRENCY_OPTIONS} - {...register(`background.experience.totalCompensation.currency`)} + {...register( + `background.experiences.0.totalCompensation.currency`, + )} /> } endAddOnType="element" @@ -99,7 +107,9 @@ function FullTimeJobFields() { startAddOn="$" startAddOnType="label" type="number" - {...register(`background.experience.totalCompensation.value`)} + {...register(`background.experiences.0.totalCompensation.value`, { + valueAsNumber: true, + })} />
@@ -107,12 +117,12 @@ function FullTimeJobFields() {
@@ -120,12 +130,14 @@ function FullTimeJobFields() { display="block" label="Location" options={locationOptions} - {...register(`background.experience.location`)} + {...register(`background.experiences.0.location`)} />
@@ -142,13 +154,13 @@ function InternshipJobFields() { display="block" label="Title" options={titleOptions} - {...register(`background.experience.title`)} + {...register(`background.experiences.0.title`)} />
@@ -159,7 +171,7 @@ function InternshipJobFields() { isLabelHidden={true} label="Currency" options={CURRENCY_OPTIONS} - {...register(`background.experience.monthlySalary.currency`)} + {...register(`background.experiences.0.monthlySalary.currency`)} /> } endAddOnType="element" @@ -168,7 +180,7 @@ function InternshipJobFields() { startAddOn="$" startAddOnType="label" type="number" - {...register(`background.experience.monthlySalary.value`)} + {...register(`background.experiences.0.monthlySalary.value`)} />
@@ -176,13 +188,13 @@ function InternshipJobFields() {
@@ -194,7 +206,7 @@ function CurrentJobSection() { const { register } = useFormContext(); const watchJobType = useWatch({ defaultValue: JobType.FullTime, - name: 'background.experience.jobType', + name: 'background.experiences.0.jobType', }); return ( @@ -209,7 +221,7 @@ function CurrentJobSection() { isLabelHidden={true} label="Job Type" orientation="horizontal" - {...register('background.experience.jobType')}> + {...register('background.experiences.0.jobType')}>
@@ -259,7 +271,7 @@ function EducationSection() {
diff --git a/apps/portal/src/components/offers/forms/OfferAnalysis.tsx b/apps/portal/src/components/offers/forms/OfferAnalysis.tsx index 3147bdc2..b0b7133c 100644 --- a/apps/portal/src/components/offers/forms/OfferAnalysis.tsx +++ b/apps/portal/src/components/offers/forms/OfferAnalysis.tsx @@ -86,8 +86,7 @@ export default function OfferAnalysis() {
Result
- -
+
; + offers: Array; }>(); return ( @@ -81,10 +82,7 @@ function FullTimeOfferDetailsForm({ required={true} {...register(`offers.${index}.location`, { required: true })} /> -
@@ -110,6 +108,7 @@ function FullTimeOfferDetailsForm({ type="number" {...register(`offers.${index}.job.totalCompensation.value`, { required: true, + valueAsNumber: true, })} />
@@ -121,7 +120,9 @@ function FullTimeOfferDetailsForm({ isLabelHidden={true} label="Currency" options={CURRENCY_OPTIONS} - {...register(`offers.${index}.job.base.currency`)} + {...register(`offers.${index}.job.base.currency`, { + required: true, + })} /> } endAddOnType="element" @@ -131,7 +132,10 @@ function FullTimeOfferDetailsForm({ startAddOn="$" startAddOnType="label" type="number" - {...register(`offers.${index}.job.base.value`)} + {...register(`offers.${index}.job.base.value`, { + required: true, + valueAsNumber: true, + })} /> } endAddOnType="element" @@ -150,7 +156,10 @@ function FullTimeOfferDetailsForm({ startAddOn="$" startAddOnType="label" type="number" - {...register(`offers.${index}.job.bonus.value`)} + {...register(`offers.${index}.job.bonus.value`, { + required: true, + valueAsNumber: true, + })} />
@@ -161,7 +170,9 @@ function FullTimeOfferDetailsForm({ isLabelHidden={true} label="Currency" options={CURRENCY_OPTIONS} - {...register(`offers.${index}.job.stocks.currency`)} + {...register(`offers.${index}.job.stocks.currency`, { + required: true, + })} /> } endAddOnType="element" @@ -171,7 +182,10 @@ function FullTimeOfferDetailsForm({ startAddOn="$" startAddOnType="label" type="number" - {...register(`offers.${index}.job.stocks.value`)} + {...register(`offers.${index}.job.stocks.value`, { + required: true, + valueAsNumber: true, + })} />
@@ -251,7 +265,7 @@ function InternshipOfferDetailsForm({ remove, }: InternshipOfferDetailsFormProps) { const { register } = useFormContext<{ - offers: Array; + offers: Array; }>(); return ( @@ -262,13 +276,19 @@ function InternshipOfferDetailsForm({ label="Title" options={titleOptions} required={true} - {...register(`offers.${index}.job.title`)} + {...register(`offers.${index}.job.title`, { + minLength: 1, + required: true, + })} />
@@ -277,40 +297,44 @@ function InternshipOfferDetailsForm({ label="Company" options={companyOptions} required={true} - value="Shopee" - {...register(`offers.${index}.companyId`)} + {...register(`offers.${index}.companyId`, { + required: true, + })} />
-
- +
+
+
+

Date received:

+
@@ -321,7 +345,9 @@ function InternshipOfferDetailsForm({ isLabelHidden={true} label="Currency" options={CURRENCY_OPTIONS} - {...register(`offers.${index}.job.monthlySalary.currency`)} + {...register(`offers.${index}.job.monthlySalary.currency`, { + required: true, + })} /> } endAddOnType="element" @@ -331,7 +357,10 @@ function InternshipOfferDetailsForm({ startAddOn="$" startAddOnType="label" type="number" - {...register(`offers.${index}.job.monthlySalary.value`)} + {...register(`offers.${index}.job.monthlySalary.value`, { + required: true, + valueAsNumber: true, + })} />
diff --git a/apps/portal/src/components/offers/forms/components/FormMonthYearPicker.tsx b/apps/portal/src/components/offers/forms/components/FormMonthYearPicker.tsx new file mode 100644 index 00000000..62967117 --- /dev/null +++ b/apps/portal/src/components/offers/forms/components/FormMonthYearPicker.tsx @@ -0,0 +1,37 @@ +import type { ComponentProps } from 'react'; +import { useFormContext, useWatch } from 'react-hook-form'; + +import MonthYearPicker from '~/components/shared/MonthYearPicker'; + +import { getCurrentMonth, getCurrentYear } from '../../../../utils/offers/time'; + +type MonthYearPickerProps = ComponentProps; + +type FormMonthYearPickerProps = Omit< + MonthYearPickerProps, + 'onChange' | 'value' +> & { + name: string; +}; + +export default function FormMonthYearPicker({ + name, + ...rest +}: FormMonthYearPickerProps) { + const { setValue } = useFormContext(); + + const value = useWatch({ + defaultValue: { month: getCurrentMonth(), year: getCurrentYear() }, + name, + }); + + return ( + { + setValue(name, val); + }} + /> + ); +} diff --git a/apps/portal/src/components/offers/forms/FormRadioList.tsx b/apps/portal/src/components/offers/forms/components/FormRadioList.tsx similarity index 69% rename from apps/portal/src/components/offers/forms/FormRadioList.tsx rename to apps/portal/src/components/offers/forms/components/FormRadioList.tsx index 9ce3065d..5fbbd53d 100644 --- a/apps/portal/src/components/offers/forms/FormRadioList.tsx +++ b/apps/portal/src/components/offers/forms/components/FormRadioList.tsx @@ -1,5 +1,4 @@ import type { ComponentProps } from 'react'; -import { forwardRef } from 'react'; import { useFormContext } from 'react-hook-form'; import { RadioList } from '@tih/ui'; @@ -7,7 +6,7 @@ type RadioListProps = ComponentProps; type FormRadioListProps = Omit; -function FormRadioListWithRef({ name, ...rest }: FormRadioListProps) { +export default function FormRadioList({ name, ...rest }: FormRadioListProps) { const { setValue } = useFormContext(); return ( ); } - -const FormRadioList = forwardRef(FormRadioListWithRef); - -export default FormRadioList; diff --git a/apps/portal/src/components/offers/forms/FormSelect.tsx b/apps/portal/src/components/offers/forms/components/FormSelect.tsx similarity index 100% rename from apps/portal/src/components/offers/forms/FormSelect.tsx rename to apps/portal/src/components/offers/forms/components/FormSelect.tsx diff --git a/apps/portal/src/components/offers/forms/FormTextArea.tsx b/apps/portal/src/components/offers/forms/components/FormTextArea.tsx similarity index 100% rename from apps/portal/src/components/offers/forms/FormTextArea.tsx rename to apps/portal/src/components/offers/forms/components/FormTextArea.tsx diff --git a/apps/portal/src/components/offers/forms/FormTextInput.tsx b/apps/portal/src/components/offers/forms/components/FormTextInput.tsx similarity index 100% rename from apps/portal/src/components/offers/forms/FormTextInput.tsx rename to apps/portal/src/components/offers/forms/components/FormTextInput.tsx diff --git a/apps/portal/src/components/offers/types.ts b/apps/portal/src/components/offers/types.ts index 17bda427..82ee7140 100644 --- a/apps/portal/src/components/offers/types.ts +++ b/apps/portal/src/components/offers/types.ts @@ -1,4 +1,6 @@ /* eslint-disable no-shadow */ +import type { MonthYear } from '../shared/MonthYearPicker'; + /* * Offer Profile */ @@ -18,7 +20,7 @@ export enum EducationBackgroundType { SelfTaught = 'Self-taught', } -type Money = { +export type Money = { currency: string; value: number; }; @@ -33,16 +35,6 @@ type FullTimeJobData = { totalCompensation: Money; }; -export type FullTimeOfferFormData = { - comments: string; - companyId: string; - job: FullTimeJobData; - jobType: string; - location: string; - monthYearReceived: string; - negotiationStrategy: string; -}; - type InternshipJobData = { internshipCycle: string; monthlySalary: Money; @@ -51,17 +43,22 @@ type InternshipJobData = { title: string; }; -export type InternshipOfferFormData = { +export type OfferDetailsFormData = { comments: string; companyId: string; - job: InternshipJobData; + job: FullTimeJobData | InternshipJobData; jobType: string; location: string; - monthYearReceived: string; + monthYearReceived: MonthYear; negotiationStrategy: string; }; -type OfferDetailsFormData = FullTimeOfferFormData | InternshipOfferFormData; +export type OfferDetailsPostData = Omit< + OfferDetailsFormData, + 'monthYearReceived' +> & { + monthYearReceived: Date; +}; type SpecificYoe = { domain: string; @@ -98,8 +95,8 @@ type Education = { }; type BackgroundFormData = { - education: Education; - experience: Experience; + educations: Array; + experiences: Array; specificYoes: Array; totalYoe: number; }; @@ -108,3 +105,8 @@ export type SubmitOfferFormData = { background: BackgroundFormData; offers: Array; }; + +export type OfferPostData = { + background: BackgroundFormData; + offers: Array; +}; diff --git a/apps/portal/src/components/offers/util/time/index.tsx b/apps/portal/src/components/offers/util/time/index.tsx deleted file mode 100644 index 86f21ab9..00000000 --- a/apps/portal/src/components/offers/util/time/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export function formatDate(value: Date | number | string) { - const date = new Date(value); - // Const day = date.toLocaleString('default', { day: '2-digit' }); - const month = date.toLocaleString('default', { month: 'short' }); - const year = date.toLocaleString('default', { year: 'numeric' }); - return `${month} ${year}`; -} diff --git a/apps/portal/src/pages/offers/index.tsx b/apps/portal/src/pages/offers/index.tsx index 27166716..c4dfe8b5 100644 --- a/apps/portal/src/pages/offers/index.tsx +++ b/apps/portal/src/pages/offers/index.tsx @@ -3,9 +3,11 @@ import { Select } from '@tih/ui'; import OffersTable from '~/components/offers/OffersTable'; import OffersTitle from '~/components/offers/OffersTitle'; +import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; export default function OffersHomePage() { const [jobTitleFilter, setjobTitleFilter] = useState('Software engineers'); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [companyFilter, setCompanyFilter] = useState('All companies'); return ( @@ -13,7 +15,7 @@ export default function OffersHomePage() {
-
+
Viewing offers for
setCompanyFilter(value)} />
diff --git a/apps/portal/src/pages/offers/submit.tsx b/apps/portal/src/pages/offers/submit.tsx index b85e098c..c4949bb2 100644 --- a/apps/portal/src/pages/offers/submit.tsx +++ b/apps/portal/src/pages/offers/submit.tsx @@ -8,7 +8,15 @@ import BackgroundForm from '~/components/offers/forms/BackgroundForm'; import OfferAnalysis from '~/components/offers/forms/OfferAnalysis'; import OfferDetailsForm from '~/components/offers/forms/OfferDetailsForm'; import OfferProfileSave from '~/components/offers/forms/OfferProfileSave'; -import type { SubmitOfferFormData } from '~/components/offers/types'; +import type { + OfferDetailsFormData, + SubmitOfferFormData, +} 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'; function Breadcrumbs() { return ( @@ -23,53 +31,94 @@ const defaultOfferValues = { { comments: '', companyId: '', - job: { - base: { - currency: 'USD', - value: 0, - }, - bonus: { - currency: 'USD', - value: 0, - }, - level: '', - specialization: '', - stocks: { - currency: 'USD', - value: 0, - }, - title: '', - totalCompensation: { - currency: 'USD', - value: 0, - }, - }, + job: {}, jobType: 'FULLTIME', location: '', - monthYearReceived: '', + monthYearReceived: { + month: getCurrentMonth() as Month, + year: getCurrentYear(), + }, negotiationStrategy: '', }, ], }; +type FormStep = { + component: JSX.Element; + hasNext: boolean; + hasPrevious: boolean; +}; + export default function OffersSubmissionPage() { const [formStep, setFormStep] = useState(0); const formMethods = useForm({ defaultValues: defaultOfferValues, + mode: 'all', }); + const { handleSubmit, trigger } = formMethods; + + const formSteps: Array = [ + { + component: , + hasNext: true, + hasPrevious: false, + }, + { + component: , + hasNext: false, + hasPrevious: true, + }, + { component: , hasNext: true, hasPrevious: false }, + { + component: , + hasNext: false, + hasPrevious: false, + }, + ]; + + const nextStep = async (currStep: number) => { + if (currStep === 0) { + const result = await trigger('offers'); + if (!result) { + return; + } + } + setFormStep(formStep + 1); + }; - const nextStep = () => setFormStep(formStep + 1); const previousStep = () => setFormStep(formStep - 1); - const formComponents = [ - , - , - , - , - ]; + const createMutation = trpc.useMutation(['offers.profile.create'], { + onError(error) { + console.error(error.message); + }, + onSuccess() { + alert('offer profile submit success!'); + setFormStep(formStep + 1); + }, + }); + + const onSubmit: SubmitHandler = async (data) => { + const result = await trigger(); + if (!result) { + return; + } + data = removeInvalidMoneyData(data); + const background = cleanObject(data.background); + const offers = data.offers.map((offer: OfferDetailsFormData) => ({ + ...offer, + monthYearReceived: new Date( + offer.monthYearReceived.year, + offer.monthYearReceived.month, + ), + })); + const postData = { background, offers }; + + postData.background.specificYoes = data.background.specificYoes.filter( + (specificYoe) => specificYoe.domain && specificYoe.yoe > 0, + ); - const onSubmit: SubmitHandler = async () => { - nextStep(); + createMutation.mutate(postData); }; return ( @@ -78,16 +127,17 @@ export default function OffersSubmissionPage() {
-
- {formComponents[formStep]} + + {formSteps[formStep].component} {/*
{JSON.stringify(formMethods.watch(), null, 2)}
*/} - {(formStep === 0 || formStep === 2) && ( + {formSteps[formStep].hasNext && (
)} diff --git a/apps/portal/src/components/offers/util/currency/CurrencyEnum.tsx b/apps/portal/src/utils/offers/currency/CurrencyEnum.tsx similarity index 100% rename from apps/portal/src/components/offers/util/currency/CurrencyEnum.tsx rename to apps/portal/src/utils/offers/currency/CurrencyEnum.tsx diff --git a/apps/portal/src/components/offers/util/currency/CurrencySelector.tsx b/apps/portal/src/utils/offers/currency/CurrencySelector.tsx similarity index 89% rename from apps/portal/src/components/offers/util/currency/CurrencySelector.tsx rename to apps/portal/src/utils/offers/currency/CurrencySelector.tsx index 2ebe5bd5..2ba883b4 100644 --- a/apps/portal/src/components/offers/util/currency/CurrencySelector.tsx +++ b/apps/portal/src/utils/offers/currency/CurrencySelector.tsx @@ -1,6 +1,6 @@ import { Select } from '@tih/ui'; -import { Currency } from '~/components/offers/util/currency/CurrencyEnum'; +import { Currency } from '~/utils/offers/currency/CurrencyEnum'; const currencyOptions = Object.entries(Currency).map(([key, value]) => ({ label: key, diff --git a/apps/portal/src/utils/offers/form.tsx b/apps/portal/src/utils/offers/form.tsx new file mode 100644 index 00000000..16def65d --- /dev/null +++ b/apps/portal/src/utils/offers/form.tsx @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Removes empty objects, empty strings, `null`, `undefined`, and `NaN` values from an object. + * Does not remove empty arrays. + * @param object + * @returns object without empty values or objects. + */ +export function cleanObject(object: any) { + Object.entries(object).forEach(([k, v]) => { + if ((v && typeof v === 'object') || Array.isArray(v)) { + cleanObject(v); + } + if ( + (v && + typeof v === 'object' && + !Object.keys(v).length && + !Array.isArray(v)) || + v === null || + v === undefined || + v === '' || + v !== v + ) { + if (Array.isArray(object)) { + const index = object.indexOf(v); + object.splice(index, 1); + } else if (!(v instanceof Date)) { + delete object[k]; + } + } + }); + return object; +} + +/** + * Removes invalid money data from an object. + * If currency is present but value is not present, money object is removed. + * @param object + * @returns object without invalid money data. + */ +export function removeInvalidMoneyData(object: any) { + Object.entries(object).forEach(([k, v]) => { + if ((v && typeof v === 'object') || Array.isArray(v)) { + removeInvalidMoneyData(v); + } + if (k === 'currency') { + if (object.value === undefined) { + delete object[k]; + } else if (object.value === null || object.value !== object.value) { + delete object[k]; + delete object.value; + } + } + }); + return object; +} diff --git a/apps/portal/src/utils/offers/time.tsx b/apps/portal/src/utils/offers/time.tsx new file mode 100644 index 00000000..c13a6efe --- /dev/null +++ b/apps/portal/src/utils/offers/time.tsx @@ -0,0 +1,25 @@ +import { getMonth, getYear } from 'date-fns'; + +import type { MonthYear } from '~/components/shared/MonthYearPicker'; + +export function formatDate(value: Date | number | string) { + const date = new Date(value); + // Const day = date.toLocaleString('default', { day: '2-digit' }); + const month = date.toLocaleString('default', { month: 'short' }); + const year = date.toLocaleString('default', { year: 'numeric' }); + return `${month} ${year}`; +} + +export function formatMonthYear({ month, year }: MonthYear) { + const monthString = month < 10 ? month.toString() : `0${month}`; + const yearString = year.toString(); + return `${monthString}/${yearString}`; +} + +export function getCurrentMonth() { + return getMonth(Date.now()); +} + +export function getCurrentYear() { + return getYear(Date.now()); +} From 6a6c939953b3e85516eb69b237ff860ef4b21458 Mon Sep 17 00:00:00 2001 From: Stuart Long Chay Boon Date: Tue, 11 Oct 2022 15:25:42 +0800 Subject: [PATCH 13/90] [offers][fix] fix create endpoint no company bug --- .../server/router/offers-profile-router.ts | 113 +++++++++++------- 1 file changed, 72 insertions(+), 41 deletions(-) diff --git a/apps/portal/src/server/router/offers-profile-router.ts b/apps/portal/src/server/router/offers-profile-router.ts index cdd01558..20343ac6 100644 --- a/apps/portal/src/server/router/offers-profile-router.ts +++ b/apps/portal/src/server/router/offers-profile-router.ts @@ -161,48 +161,79 @@ export const offersProfileRouter = createRouter() x.totalCompensation?.currency !== undefined && x.totalCompensation.value !== undefined ) { - return { - company: { - connect: { - id: x.companyId, - }, - }, - durationInMonths: x.durationInMonths, - jobType: x.jobType, - level: x.level, - specialization: x.specialization, - title: x.title, - totalCompensation: { - create: { - currency: x.totalCompensation?.currency, - value: x.totalCompensation?.value, - }, - }, - }; - } - if ( - x.jobType === 'INTERN' && - x.monthlySalary?.currency !== undefined && - x.monthlySalary.value !== undefined - ) { - return { - company: { - connect: { - id: x.companyId, - }, - }, - durationInMonths: x.durationInMonths, - jobType: x.jobType, - monthlySalary: { - create: { - currency: x.monthlySalary?.currency, - value: x.monthlySalary?.value, - }, - }, - specialization: x.specialization, - title: x.title, - }; + if (x.companyId) { + return { + company: { + connect: { + id: x.companyId, + }, + }, + durationInMonths: x.durationInMonths, + jobType: x.jobType, + level: x.level, + specialization: x.specialization, + title: x.title, + totalCompensation: { + create: { + currency: x.totalCompensation?.currency, + value: x.totalCompensation?.value, + }, + }, + }; + } + return { + durationInMonths: x.durationInMonths, + jobType: x.jobType, + level: x.level, + specialization: x.specialization, + title: x.title, + totalCompensation: { + create: { + currency: x.totalCompensation?.currency, + value: x.totalCompensation?.value, + }, + }, + }; + } + if ( + x.jobType === 'INTERN' && + x.monthlySalary?.currency !== undefined && + x.monthlySalary.value !== undefined + ) { + if (x.companyId) { + return { + company: { + connect: { + id: x.companyId, + }, + }, + durationInMonths: x.durationInMonths, + jobType: x.jobType, + monthlySalary: { + create: { + currency: x.monthlySalary?.currency, + value: x.monthlySalary?.value, + }, + }, + specialization: x.specialization, + title: x.title, + }; + } + return { + durationInMonths: x.durationInMonths, + jobType: x.jobType, + monthlySalary: { + create: { + currency: x.monthlySalary?.currency, + value: x.monthlySalary?.value, + }, + }, + specialization: x.specialization, + title: x.title, + }; + + } throw Prisma.PrismaClientKnownRequestError; }), From a905f31b2c650f0380fa0bb360fdcd1d187cbe25 Mon Sep 17 00:00:00 2001 From: Peirong <35712975+peironggg@users.noreply.github.com> Date: Tue, 11 Oct 2022 16:36:09 +0800 Subject: [PATCH 14/90] [resumes][fix] fix resumes starring lag + add zoom controls (#359) * [resumes][fix] Fix star button delay * [resumes][feat] add zoom controls for pdf --- .../src/components/resumes/ResumePdf.tsx | 46 +++++++++--- .../resumes/comments/CommentListItems.tsx | 2 +- .../resumes/comments/CommentsForm.tsx | 2 +- apps/portal/src/pages/resumes/[resumeId].tsx | 72 +++++++++++++------ apps/portal/src/server/router/index.ts | 2 +- .../router/resumes/resumes-resume-router.ts | 4 +- .../resumes/resumes-star-user-router.ts | 46 +++++------- 7 files changed, 113 insertions(+), 61 deletions(-) diff --git a/apps/portal/src/components/resumes/ResumePdf.tsx b/apps/portal/src/components/resumes/ResumePdf.tsx index a190606e..668d0f6f 100644 --- a/apps/portal/src/components/resumes/ResumePdf.tsx +++ b/apps/portal/src/components/resumes/ResumePdf.tsx @@ -1,7 +1,12 @@ import { useState } from 'react'; import { Document, Page, pdfjs } from 'react-pdf'; import type { PDFDocumentProxy } from 'react-pdf/node_modules/pdfjs-dist'; -import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid'; +import { + ArrowLeftIcon, + ArrowRightIcon, + MagnifyingGlassMinusIcon, + MagnifyingGlassPlusIcon, +} from '@heroicons/react/20/solid'; import { Button, Spinner } from '@tih/ui'; pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`; @@ -13,6 +18,7 @@ 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 onPdfLoadSuccess = (pdf: PDFDocumentProxy) => { setNumPages(pdf.numPages); @@ -20,14 +26,36 @@ export default function ResumePdf({ url }: Props) { return (
- } - noData="" - onLoadSuccess={onPdfLoadSuccess}> - - +
+ } + noData="" + onLoadSuccess={onPdfLoadSuccess}> + +
+
+
+
diff --git a/apps/portal/src/server/router/index.ts b/apps/portal/src/server/router/index.ts index c018c3f7..8dd5e08c 100644 --- a/apps/portal/src/server/router/index.ts +++ b/apps/portal/src/server/router/index.ts @@ -28,7 +28,7 @@ export const appRouter = createRouter() .merge('companies.', companiesRouter) .merge('resumes.resume.', resumesRouter) .merge('resumes.resume.user.', resumesResumeUserRouter) - .merge('resumes.star.user.', resumesStarUserRouter) + .merge('resumes.resume.', resumesStarUserRouter) .merge('resumes.reviews.', resumeReviewsRouter) .merge('resumes.reviews.user.', resumesReviewsUserRouter) .merge('questions.answers.comments.', questionsAnswerCommentRouter) diff --git a/apps/portal/src/server/router/resumes/resumes-resume-router.ts b/apps/portal/src/server/router/resumes/resumes-resume-router.ts index 177b076e..34d7d6f0 100644 --- a/apps/portal/src/server/router/resumes/resumes-resume-router.ts +++ b/apps/portal/src/server/router/resumes/resumes-resume-router.ts @@ -62,7 +62,9 @@ export const resumesRouter = createRouter() }, stars: { where: { - userId, + OR: { + userId, + }, }, }, user: { diff --git a/apps/portal/src/server/router/resumes/resumes-star-user-router.ts b/apps/portal/src/server/router/resumes/resumes-star-user-router.ts index 40daea7d..9e780858 100644 --- a/apps/portal/src/server/router/resumes/resumes-star-user-router.ts +++ b/apps/portal/src/server/router/resumes/resumes-star-user-router.ts @@ -2,22 +2,15 @@ import { z } from 'zod'; import { createProtectedRouter } from '../context'; -export const resumesStarUserRouter = createProtectedRouter().mutation( - 'create_or_delete', - { +export const resumesStarUserRouter = createProtectedRouter() + .mutation('unstar', { input: z.object({ resumeId: z.string(), }), async resolve({ ctx, input }) { const { resumeId } = input; - // Update_star will only be called if user is logged in - const userId = ctx.session!.user!.id; - - // Use the resumeId and resumeProfileId to check if star exists - const resumesStar = await ctx.prisma.resumesStar.findUnique({ - select: { - id: true, - }, + const userId = ctx.session.user.id; + return await ctx.prisma.resumesStar.delete({ where: { userId_resumeId: { resumeId, @@ -25,23 +18,20 @@ export const resumesStarUserRouter = createProtectedRouter().mutation( }, }, }); - - if (resumesStar === null) { - return await ctx.prisma.resumesStar.create({ - data: { - resumeId, - userId, - }, - }); - } - return await ctx.prisma.resumesStar.delete({ - where: { - userId_resumeId: { - resumeId, - userId, - }, + }, + }) + .mutation('star', { + input: z.object({ + resumeId: z.string(), + }), + async resolve({ ctx, input }) { + const { resumeId } = input; + const userId = ctx.session.user.id; + return await ctx.prisma.resumesStar.create({ + data: { + resumeId, + userId, }, }); }, - }, -); + }); From b52db4196506cfbb1360d26574e4c8c8bb97e20e Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Tue, 11 Oct 2022 17:46:21 +0800 Subject: [PATCH 15/90] [portal][ui] improve product navbar --- .../portal/src/components/global/AppShell.tsx | 2 ++ .../src/components/global/HomeNavigation.ts | 1 + .../components/global/ProductNavigation.tsx | 29 +++++++++++++------ .../src/components/offers/OffersNavigation.ts | 1 + .../questions/QuestionBankTitle.tsx | 5 ---- .../questions/QuestionsNavigation.ts | 2 +- .../components/resumes/ResumesNavigation.ts | 2 ++ 7 files changed, 27 insertions(+), 15 deletions(-) delete mode 100644 apps/portal/src/components/questions/QuestionBankTitle.tsx diff --git a/apps/portal/src/components/global/AppShell.tsx b/apps/portal/src/components/global/AppShell.tsx index 0275dbb5..bcb9af67 100644 --- a/apps/portal/src/components/global/AppShell.tsx +++ b/apps/portal/src/components/global/AppShell.tsx @@ -109,6 +109,7 @@ export default function AppShell({ children }: Props) { navigation: ProductNavigationItems; showGlobalNav: boolean; title: string; + titleHref: string; }> = (() => { const path = router.pathname; if (path.startsWith('/resumes')) { @@ -190,6 +191,7 @@ export default function AppShell({ children }: Props) {
diff --git a/apps/portal/src/components/global/HomeNavigation.ts b/apps/portal/src/components/global/HomeNavigation.ts index 073a7b36..eb41d372 100644 --- a/apps/portal/src/components/global/HomeNavigation.ts +++ b/apps/portal/src/components/global/HomeNavigation.ts @@ -17,6 +17,7 @@ const config = { navigation, showGlobalNav: true, title: 'Tech Interview Handbook', + titleHref: '/', }; export default config; diff --git a/apps/portal/src/components/global/ProductNavigation.tsx b/apps/portal/src/components/global/ProductNavigation.tsx index caae6c08..43670585 100644 --- a/apps/portal/src/components/global/ProductNavigation.tsx +++ b/apps/portal/src/components/global/ProductNavigation.tsx @@ -1,5 +1,6 @@ import clsx from 'clsx'; import Link from 'next/link'; +import { useRouter } from 'next/router'; import { Fragment } from 'react'; import { Menu, Transition } from '@headlessui/react'; import { ChevronDownIcon } from '@heroicons/react/20/solid'; @@ -8,6 +9,7 @@ type NavigationItem = Readonly<{ children?: ReadonlyArray; href: string; name: string; + target?: '_blank'; }>; export type ProductNavigationItems = ReadonlyArray; @@ -15,15 +17,21 @@ export type ProductNavigationItems = ReadonlyArray; type Props = Readonly<{ items: ProductNavigationItems; title: string; + titleHref: string; }>; -export default function ProductNavigation({ items, title }: Props) { +export default function ProductNavigation({ items, title, titleHref }: Props) { + const router = useRouter(); + return ( - ); @@ -167,20 +270,7 @@ export default function OffersTable() { {renderHeader()} - {renderRow({ - company: 'Shopee', - date: 'May 2022', - salary: 'TC/yr', - title: 'SWE', - yoe: '5', - })} - {renderRow({ - company: 'Shopee', - date: 'May 2022', - salary: 'TC/yr', - title: 'SWE', - yoe: '5', - })} + {offers.map((offer: OfferTableRow) => renderRow(offer))}
{renderPagination()} diff --git a/apps/portal/src/components/offers/profile/EducationCard.tsx b/apps/portal/src/components/offers/profile/EducationCard.tsx index c7b31f12..7dd5b155 100644 --- a/apps/portal/src/components/offers/profile/EducationCard.tsx +++ b/apps/portal/src/components/offers/profile/EducationCard.tsx @@ -3,14 +3,14 @@ import { LightBulbIcon, } from '@heroicons/react/24/outline'; -import type { EducationBackgroundType } from '../types'; +import type { EducationBackgroundType } from '~/components/offers/types'; type EducationEntity = { - backgroundType?: EducationBackgroundType; + endDate?: string; field?: string; - fromMonth?: string; school?: string; - toMonth?: string; + startDate?: string; + type?: EducationBackgroundType; }; type Props = Readonly<{ @@ -18,7 +18,7 @@ type Props = Readonly<{ }>; export default function EducationCard({ - education: { backgroundType, field, fromMonth, school, toMonth }, + education: { type, field, startDate, endDate, school }, }: Props) { return (
@@ -27,9 +27,7 @@ export default function EducationCard({
- {field - ? `${backgroundType ?? 'N/A'}, ${field}` - : backgroundType ?? `N/A`} + {field ? `${type ?? 'N/A'}, ${field}` : type ?? `N/A`}
{school && ( @@ -39,9 +37,11 @@ export default function EducationCard({
)}
- {(fromMonth || toMonth) && ( + {(startDate || endDate) && (
-

{`${fromMonth ?? 'N/A'} - ${toMonth ?? 'N/A'}`}

+

{`${startDate ? startDate : 'N/A'} - ${ + endDate ? endDate : 'N/A' + }`}

)}
diff --git a/apps/portal/src/components/offers/profile/OfferCard.tsx b/apps/portal/src/components/offers/profile/OfferCard.tsx index 875c38db..2cc64e9b 100644 --- a/apps/portal/src/components/offers/profile/OfferCard.tsx +++ b/apps/portal/src/components/offers/profile/OfferCard.tsx @@ -6,18 +6,19 @@ import { } from '@heroicons/react/24/outline'; import { HorizontalDivider } from '@tih/ui'; -type OfferEntity = { +export type OfferEntity = { base?: string; bonus?: string; companyName: string; - duration?: string; // For background + duration?: string; + id?: string; jobLevel?: string; jobTitle: string; - location: string; + location?: string; monthlySalary?: string; negotiationStrategy?: string; otherComment?: string; - receivedMonth: string; + receivedMonth?: string; stocks?: string; totalCompensation?: string; }; @@ -57,14 +58,14 @@ export default function OfferCard({

{jobLevel ? `${jobTitle}, ${jobLevel}` : jobTitle}

- {receivedMonth && ( + {!duration && receivedMonth && (

{receivedMonth}

)} {duration && (
-

{duration}

+

{`${duration} months`}

)}
diff --git a/apps/portal/src/components/offers/types.ts b/apps/portal/src/components/offers/types.ts index 82ee7140..096fe70e 100644 --- a/apps/portal/src/components/offers/types.ts +++ b/apps/portal/src/components/offers/types.ts @@ -1,5 +1,6 @@ /* eslint-disable no-shadow */ -import type { MonthYear } from '../shared/MonthYearPicker'; +import type { OfferEntity } from '~/components/offers/profile/OfferCard'; +import type { MonthYear } from '~/components/shared/MonthYearPicker'; /* * Offer Profile @@ -82,7 +83,7 @@ type GeneralExperience = { title: string; }; -type Experience = +export type Experience = | (FullTimeExperience & GeneralExperience) | (GeneralExperience & InternshipExperience); @@ -110,3 +111,19 @@ export type OfferPostData = { background: BackgroundFormData; offers: Array; }; + +type EducationDisplay = { + endDate?: string; + field: string; + school: string; + startDate?: string; + type: string; +}; + +export type BackgroundCard = { + educations: Array; + experiences: Array; + profileName: string; + specificYoes: Array; + totalYoe: string; +}; diff --git a/apps/portal/src/pages/offers/index.tsx b/apps/portal/src/pages/offers/index.tsx index c4dfe8b5..8fc3ae18 100644 --- a/apps/portal/src/pages/offers/index.tsx +++ b/apps/portal/src/pages/offers/index.tsx @@ -6,8 +6,7 @@ import OffersTitle from '~/components/offers/OffersTitle'; import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; export default function OffersHomePage() { - const [jobTitleFilter, setjobTitleFilter] = useState('Software engineers'); - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [jobTitleFilter, setjobTitleFilter] = useState('Software Engineer'); const [companyFilter, setCompanyFilter] = useState('All companies'); return ( @@ -23,20 +22,20 @@ export default function OffersHomePage() { label="Select a job title" options={[ { - label: 'Software engineers', - value: 'Software engineers', + label: 'Software Engineer', + value: 'Software Engineer', }, { - label: 'Frontend engineers', - value: 'Frontend engineers', + label: 'Frontend Engineer', + value: 'Frontend Engineer', }, { - label: 'Backend engineers', - value: 'Backend engineers', + label: 'Backend Engineer', + value: 'Backend Engineer', }, { - label: 'Full-stack engineers', - value: 'Full-stack engineers', + label: 'Full-stack Engineer', + value: 'Full-stack Engineer', }, ]} value={jobTitleFilter} @@ -53,7 +52,10 @@ export default function OffersHomePage() {
- +
); diff --git a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx index b7baac84..7e8cdf98 100644 --- a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx +++ b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx @@ -1,3 +1,5 @@ +import Error from 'next/error'; +import { useRouter } from 'next/router'; import { useState } from 'react'; import { AcademicCapIcon, @@ -13,13 +15,129 @@ import { import { Button, Dialog, Tabs } from '@tih/ui'; import EducationCard from '~/components/offers/profile/EducationCard'; +import type { OfferEntity } from '~/components/offers/profile/OfferCard'; import OfferCard from '~/components/offers/profile/OfferCard'; import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder'; +import type { BackgroundCard } from '~/components/offers/types'; import { EducationBackgroundType } from '~/components/offers/types'; +import { formatDate } from '~/utils/offers/time'; +import { trpc } from '~/utils/trpc'; export default function OfferProfile() { + const ErrorPage = ( + + ); + const router = useRouter(); + const { offerProfileId, token = '' } = router.query; + const [isEditable, setIsEditable] = useState(false); + const [background, setBackground] = useState(); + const [offers, setOffers] = useState>([]); const [selectedTab, setSelectedTab] = useState('offers'); const [isDialogOpen, setIsDialogOpen] = useState(false); + + const detailsQuery = trpc.useQuery( + [ + 'offers.profile.listOne', + { profileId: offerProfileId as string, token: token as string }, + ], + { + enabled: typeof offerProfileId === 'string', + onSuccess: (data) => { + const filteredOffers: Array = data!.offers.map((res) => { + if (res.OffersFullTime) { + const filteredOffer: OfferEntity = { + base: res.OffersFullTime.baseSalary.value + ? `${res.OffersFullTime.baseSalary.value} ${res.OffersFullTime.baseSalary.currency}` + : '', + bonus: res.OffersFullTime.bonus.value + ? `${res.OffersFullTime.bonus.value} ${res.OffersFullTime.bonus.currency}` + : '', + companyName: res.company.name, + id: res.OffersFullTime.id, + jobLevel: res.OffersFullTime.level, + jobTitle: res.OffersFullTime.title, + location: res.location, + negotiationStrategy: res.negotiationStrategy || '', + otherComment: res.comments || '', + receivedMonth: formatDate(res.monthYearReceived), + stocks: res.OffersFullTime.stocks.value + ? `${res.OffersFullTime.stocks.value} ${res.OffersFullTime.stocks.currency}` + : '', + totalCompensation: res.OffersFullTime.totalCompensation.value + ? `${res.OffersFullTime.totalCompensation.value} ${res.OffersFullTime.totalCompensation.currency}` + : '', + }; + + return filteredOffer; + } + const filteredOffer: OfferEntity = { + companyName: res.company.name, + id: res.OffersIntern!.id, + jobTitle: res.OffersIntern!.title, + location: res.location, + monthlySalary: res.OffersIntern!.monthlySalary.value + ? `${res.OffersIntern!.monthlySalary.value} ${ + res.OffersIntern!.monthlySalary.currency + }` + : '', + negotiationStrategy: res.negotiationStrategy || '', + otherComment: res.comments || '', + receivedMonth: formatDate(res.monthYearReceived), + }; + return filteredOffer; + }); + + setOffers(filteredOffers ?? []); + + if (data?.background) { + const filteredBackground: BackgroundCard = { + educations: [ + { + endDate: data?.background.educations[0].endDate + ? formatDate(data.background.educations[0].endDate) + : '-', + field: data.background.educations[0].field || '-', + school: data.background.educations[0].school || '-', + startDate: data.background.educations[0].startDate + ? formatDate(data.background.educations[0].startDate) + : '-', + type: data.background.educations[0].type || '-', + }, + ], + + experiences: [ + { + companyName: + data.background.experiences[0].company?.name ?? '-', + duration: + String(data.background.experiences[0].durationInMonths) ?? + '-', + jobLevel: data.background.experiences[0].level ?? '', + jobTitle: data.background.experiences[0].title ?? '-', + monthlySalary: data.background.experiences[0].monthlySalary + ?.value + ? `${data.background.experiences[0].monthlySalary?.value} ${data.background.experiences[0].monthlySalary?.currency}` + : `-`, + totalCompensation: data.background.experiences[0] + .totalCompensation?.value + ? `${data.background.experiences[0].totalCompensation?.value} ${data.background.experiences[0].totalCompensation?.currency}` + : ``, + }, + ], + profileName: data.profileName, + specificYoes: data.background.specificYoes ?? [], + + totalYoe: String(data.background.totalYoe) || '-', + }; + + setBackground(filteredBackground); + } + + setIsEditable(data?.isEditable ?? false); + }, + }, + ); + function renderActionList() { return (
@@ -67,8 +185,8 @@ export default function OfferProfile() { title="Are you sure you want to delete this offer profile?" onClose={() => setIsDialogOpen(false)}>
- All comments will gone. You will not be able to access or recover - it. + All comments will be gone. You will not be able to access or + recover it.
)} @@ -84,20 +202,36 @@ export default function OfferProfile() {
-

anonymised-name

-
- {renderActionList()} -
+

+ {background?.profileName ?? 'anonymous'} +

+ {isEditable && ( +
+ {isEditable && renderActionList()} +
+ )}
Current: - Level 4 Google + {`${background?.experiences[0].companyName ?? '-'} ${ + background?.experiences[0].jobLevel + } ${background?.experiences[0].jobTitle}`}
YOE: - 4 + {background?.totalYoe} + {background?.specificYoes && + background?.specificYoes.length > 0 && + background?.specificYoes.map(({ domain, yoe }) => ( + <> + {`${domain} : ${yoe}`} + {background?.totalYoe} + + ))}
@@ -131,41 +265,7 @@ export default function OfferProfile() { if (selectedTab === 'offers') { return ( <> - {[ - { - base: undefined, - bonus: undefined, - companyName: 'Meta', - id: 1, - jobLevel: 'G5', - jobTitle: 'Software Engineer', - location: 'Singapore', - monthlySalary: undefined, - negotiationStrategy: - 'Nostrud nulla aliqua deserunt commodo id aute.', - otherComment: - 'Pariatur ut est voluptate incididunt consequat do veniam quis irure adipisicing. Deserunt laborum dolor quis voluptate enim.', - receivedMonth: 'Jun 2022', - stocks: undefined, - totalCompensation: undefined, - }, - { - companyName: 'Meta', - id: 2, - jobLevel: 'G5', - jobTitle: 'Software Engineer', - location: 'Singapore', - receivedMonth: 'Jun 2022', - }, - { - companyName: 'Meta', - id: 3, - jobLevel: 'G5', - jobTitle: 'Software Engineer', - location: 'Singapore', - receivedMonth: 'Jun 2022', - }, - ].map((offer) => ( + {[...offers].map((offer) => ( ))} @@ -174,37 +274,32 @@ export default function OfferProfile() { if (selectedTab === 'background') { return ( <> -
- - Work Experience -
- -
- - Education -
- + {background?.experiences && background?.experiences.length > 0 && ( + <> +
+ + Work Experience +
+ + + )} + {background?.educations && background?.educations.length > 0 && ( + <> +
+ + Education +
+ + + )} ); } @@ -215,14 +310,22 @@ export default function OfferProfile() { return (
-

@@ -241,16 +350,19 @@ export default function OfferProfile() { } return ( -
-
- -
- + <> + {detailsQuery.isError && ErrorPage} +
+
+ +
+ +
+
+
+
-
- -
-
+ ); } From 325a2d1f7ca9c10166e610599a4ac49091d6b776 Mon Sep 17 00:00:00 2001 From: Ai Ling <50992674+ailing35@users.noreply.github.com> Date: Tue, 11 Oct 2022 19:53:21 +0800 Subject: [PATCH 17/90] [ui][companies typeahead] Add isLabelHidden and placeHolder props (#361) * [ui][companies typeahead] add isLabelHidden and placeHolder props * [ui][companies typeahead] add isLabelHidden and placeHolder props --- .../src/components/shared/CompaniesTypeahead.tsx | 11 ++++++++++- packages/ui/src/Typeahead/Typeahead.tsx | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/portal/src/components/shared/CompaniesTypeahead.tsx b/apps/portal/src/components/shared/CompaniesTypeahead.tsx index b25f9cd8..2eacc357 100644 --- a/apps/portal/src/components/shared/CompaniesTypeahead.tsx +++ b/apps/portal/src/components/shared/CompaniesTypeahead.tsx @@ -6,10 +6,17 @@ import { trpc } from '~/utils/trpc'; type Props = Readonly<{ disabled?: boolean; + isLabelHidden?: boolean; onSelect: (option: TypeaheadOption) => void; + placeHolder?: string; }>; -export default function CompaniesTypeahead({ disabled, onSelect }: Props) { +export default function CompaniesTypeahead({ + disabled, + onSelect, + isLabelHidden, + placeHolder, +}: Props) { const [query, setQuery] = useState(''); const companies = trpc.useQuery([ 'companies.list', @@ -23,6 +30,7 @@ export default function CompaniesTypeahead({ disabled, onSelect }: Props) { return ( diff --git a/packages/ui/src/Typeahead/Typeahead.tsx b/packages/ui/src/Typeahead/Typeahead.tsx index 2197716d..54bf1f59 100644 --- a/packages/ui/src/Typeahead/Typeahead.tsx +++ b/packages/ui/src/Typeahead/Typeahead.tsx @@ -22,6 +22,7 @@ type Props = Readonly<{ ) => void; onSelect: (option: TypeaheadOption) => void; options: ReadonlyArray; + placeholder?: string; value?: TypeaheadOption; }>; @@ -35,6 +36,7 @@ export default function Typeahead({ onQueryChange, value, onSelect, + placeholder, }: Props) { const [query, setQuery] = useState(''); return ( @@ -77,6 +79,7 @@ export default function Typeahead({ displayValue={(option) => (option as unknown as TypeaheadOption)?.label } + placeholder={placeholder} onChange={(event) => { setQuery(event.target.value); onQueryChange(event.target.value, event); From 9285847bb7d7d15c3033748d950c122c26e082a1 Mon Sep 17 00:00:00 2001 From: Ai Ling <50992674+ailing35@users.noreply.github.com> Date: Wed, 12 Oct 2022 00:51:24 +0800 Subject: [PATCH 18/90] [offers][fix] Fix home page company filter UI (#362) --- apps/portal/src/pages/offers/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/portal/src/pages/offers/index.tsx b/apps/portal/src/pages/offers/index.tsx index 8fc3ae18..b948139e 100644 --- a/apps/portal/src/pages/offers/index.tsx +++ b/apps/portal/src/pages/offers/index.tsx @@ -14,7 +14,7 @@ export default function OffersHomePage() {
-
+
Viewing offers for
@@ -84,13 +99,18 @@ export default function MonthYearPicker({ value, onChange }: Props) { } /> -
+ + ); +} + +export function Error() { + const [value, setValue] = useState('banana'); + + return ( + ( onChange?.(event.target.value); }} {...props}> + {placeholder && ( + + )} {options.map(({ label: optionLabel, value: optionValue }) => ( ))} + {errorMessage && ( +

+ {errorMessage} +

+ )}
); } From 7d15aa43cfc842a75f114c1bed0577b3c5c7019f Mon Sep 17 00:00:00 2001 From: Zhang Ziqing <69516975+ziqing26@users.noreply.github.com> Date: Wed, 12 Oct 2022 19:12:14 +0800 Subject: [PATCH 27/90] [offers][feat] integrate profile delete API and set loading status (#367) --- .../src/components/offers/OffersTable.tsx | 18 +- .../components/offers/profile/OfferCard.tsx | 17 +- .../offers/profile/ProfileComments.tsx | 58 +++ .../offers/profile/ProfileDetails.tsx | 79 ++++ .../offers/profile/ProfileHeader.tsx | 167 ++++++++ apps/portal/src/components/offers/types.ts | 18 +- .../pages/offers/profile/[offerProfileId].tsx | 393 +++++------------- 7 files changed, 442 insertions(+), 308 deletions(-) create mode 100644 apps/portal/src/components/offers/profile/ProfileComments.tsx create mode 100644 apps/portal/src/components/offers/profile/ProfileDetails.tsx create mode 100644 apps/portal/src/components/offers/profile/ProfileHeader.tsx diff --git a/apps/portal/src/components/offers/OffersTable.tsx b/apps/portal/src/components/offers/OffersTable.tsx index e8a28b55..5bebccc9 100644 --- a/apps/portal/src/components/offers/OffersTable.tsx +++ b/apps/portal/src/components/offers/OffersTable.tsx @@ -1,4 +1,4 @@ -import { useRouter } from 'next/router'; +import Link from 'next/link'; import { useEffect, useState } from 'react'; import { HorizontalDivider, Pagination, Select, Tabs } from '@tih/ui'; @@ -40,7 +40,6 @@ type Pagination = { const NUMBER_OF_OFFERS_IN_PAGE = 10; export default function OffersTable({ jobTitleFilter }: OffersTableProps) { - const router = useRouter(); const [currency, setCurrency] = useState('SGD'); // TODO const [selectedTab, setSelectedTab] = useState(YOE_CATEGORY.ENTRY); const [pagination, setPagination] = useState({ @@ -180,10 +179,6 @@ export default function OffersTable({ jobTitleFilter }: OffersTableProps) { ); } - const handleClickViewProfile = (profileId: string) => { - router.push(`/offers/profile/${profileId}`); - }; - const handlePageChange = (currPage: number) => { setPagination({ ...pagination, currentPage: currPage }); }; @@ -211,11 +206,17 @@ export default function OffersTable({ jobTitleFilter }: OffersTableProps) { {salary} {date} - handleClickViewProfile(profileId)}> View Profile - + */} + + + View Profile + {/* @@ -244,7 +245,6 @@ export default function OffersTable({ jobTitleFilter }: OffersTableProps) { {`of `} {pagination.totalItems} - {/* {pagination.numOfPages * NUMBER_OF_OFFERS_IN_PAGE} */} void; + handleCopyPublicLink: () => void; + isDisabled: boolean; + isEditable: boolean; + isLoading: boolean; +}>; + +export default function ProfileComments({ + handleCopyEditLink, + handleCopyPublicLink, + isDisabled, + isEditable, + isLoading, +}: ProfileHeaderProps) { + if (isLoading) { + return ( +
+ +
+ ); + } + return ( +
+
+ {isEditable && ( +
+

+ Discussions feature coming soon +

+ {/*