From 73e1f7657002787062c92e23ca3067dd5d28e019 Mon Sep 17 00:00:00 2001 From: Zhang Ziqing <69516975+ziqing26@users.noreply.github.com> Date: Tue, 11 Oct 2022 19:27:18 +0800 Subject: [PATCH] [offers][feat] integrate profile API and offer API (#360) --- .../src/components/offers/OffersTable.tsx | 158 ++++++++-- .../offers/profile/EducationCard.tsx | 20 +- .../components/offers/profile/OfferCard.tsx | 13 +- apps/portal/src/components/offers/types.ts | 21 +- apps/portal/src/pages/offers/index.tsx | 24 +- .../pages/offers/profile/[offerProfileId].tsx | 294 ++++++++++++------ 6 files changed, 376 insertions(+), 154 deletions(-) diff --git a/apps/portal/src/components/offers/OffersTable.tsx b/apps/portal/src/components/offers/OffersTable.tsx index 5ef39748..e8a28b55 100644 --- a/apps/portal/src/components/offers/OffersTable.tsx +++ b/apps/portal/src/components/offers/OffersTable.tsx @@ -1,16 +1,22 @@ -import { useState } from 'react'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; import { HorizontalDivider, Pagination, Select, Tabs } from '@tih/ui'; import CurrencySelector from '~/utils/offers/currency/CurrencySelector'; +import { formatDate } from '~/utils/offers/time'; +import { trpc } from '~/utils/trpc'; -type TableRow = { +type OfferTableRow = { company: string; date: string; - salary: string; + id: string; + profileId: string; + salary: number | undefined; title: string; - yoe: string; + yoe: number; }; +// To be changed to backend enum // eslint-disable-next-line no-shadow enum YOE_CATEGORY { INTERN = 0, @@ -19,10 +25,81 @@ enum YOE_CATEGORY { SENIOR = 3, } -export default function OffersTable() { - const [currency, setCurrency] = useState('SGD'); +type OffersTableProps = { + companyFilter: string; + jobTitleFilter: string; +}; + +type Pagination = { + currentPage: number; + numOfItems: number; + numOfPages: number; + totalItems: number; +}; + +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 [selectedPage, setSelectedPage] = useState(1); + const [pagination, setPagination] = useState({ + currentPage: 1, + numOfItems: 1, + numOfPages: 0, + totalItems: 0, + }); + const [offers, setOffers] = useState>([]); + + useEffect(() => { + setPagination({ + currentPage: 1, + numOfItems: 1, + numOfPages: 0, + totalItems: 0, + }); + }, [selectedTab]); + trpc.useQuery( + [ + 'offers.list', + { + // Company: companyFilter, // TODO + limit: NUMBER_OF_OFFERS_IN_PAGE, + + location: 'Singapore, Singapore', + offset: pagination.currentPage - 1, + sortBy: '-monthYearReceived', + title: jobTitleFilter, + yoeCategory: selectedTab, + }, + ], + { + onSuccess: (response) => { + const filteredData = response.data.map((res) => { + return { + company: res.company.name, + date: formatDate(res.monthYearReceived), + id: res.OffersFullTime + ? res.OffersFullTime!.id + : res.OffersIntern!.id, + profileId: res.profileId, + salary: res.OffersFullTime + ? res.OffersFullTime?.totalCompensation.value + : res.OffersIntern?.monthlySalary.value, + title: res.OffersFullTime ? res.OffersFullTime?.level : '', + yoe: 100, + }; + }); + setOffers(filteredData); + setPagination({ + currentPage: (response.paging.currPage as number) + 1, + numOfItems: response.paging.numOfItemsInPage, + numOfPages: response.paging.numOfPages, + totalItems: response.paging.totalNumberOfOffers, + }); + }, + }, + ); function renderTabs() { return ( @@ -103,9 +180,27 @@ export default function OffersTable() { ); } - function renderRow({ company, title, yoe, salary, date }: TableRow) { + const handleClickViewProfile = (profileId: string) => { + router.push(`/offers/profile/${profileId}`); + }; + + const handlePageChange = (currPage: number) => { + setPagination({ ...pagination, currentPage: currPage }); + }; + + function renderRow({ + company, + title, + yoe, + salary, + date, + profileId, + id, + }: OfferTableRow) { return ( - + @@ -118,14 +213,14 @@ export default function OffersTable() { + onClick={() => handleClickViewProfile(profileId)}> View Profile - Comment - + */} ); @@ -137,22 +232,30 @@ export default function OffersTable() { aria-label="Table navigation" className="flex items-center justify-between p-4"> - Showing{' '} + Showing - 1-10 - {' '} - of{' '} + {` ${ + (pagination.currentPage - 1) * NUMBER_OF_OFFERS_IN_PAGE + 1 + } - ${ + (pagination.currentPage - 1) * NUMBER_OF_OFFERS_IN_PAGE + + offers.length + } `} + + {`of `} - 1000 + {pagination.totalItems} + {/* {pagination.numOfPages * NUMBER_OF_OFFERS_IN_PAGE} */} setSelectedPage(page)} + onSelect={(currPage) => { + handlePageChange(currPage); + }} /> ); @@ -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} +
+
+ +
+ +
+
+
+
-
- -
-
+ ); }