[offers][feat] integrate profile delete API and set loading status (#367)

pull/368/head
Zhang Ziqing 2 years ago committed by GitHub
parent b87afb1383
commit 7d15aa43cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,4 @@
import { useRouter } from 'next/router'; import Link from 'next/link';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { HorizontalDivider, Pagination, Select, Tabs } from '@tih/ui'; import { HorizontalDivider, Pagination, Select, Tabs } from '@tih/ui';
@ -40,7 +40,6 @@ type Pagination = {
const NUMBER_OF_OFFERS_IN_PAGE = 10; const NUMBER_OF_OFFERS_IN_PAGE = 10;
export default function OffersTable({ jobTitleFilter }: OffersTableProps) { export default function OffersTable({ jobTitleFilter }: OffersTableProps) {
const router = useRouter();
const [currency, setCurrency] = useState('SGD'); // TODO const [currency, setCurrency] = useState('SGD'); // TODO
const [selectedTab, setSelectedTab] = useState(YOE_CATEGORY.ENTRY); const [selectedTab, setSelectedTab] = useState(YOE_CATEGORY.ENTRY);
const [pagination, setPagination] = useState<Pagination>({ const [pagination, setPagination] = useState<Pagination>({
@ -180,10 +179,6 @@ export default function OffersTable({ jobTitleFilter }: OffersTableProps) {
); );
} }
const handleClickViewProfile = (profileId: string) => {
router.push(`/offers/profile/${profileId}`);
};
const handlePageChange = (currPage: number) => { const handlePageChange = (currPage: number) => {
setPagination({ ...pagination, currentPage: currPage }); setPagination({ ...pagination, currentPage: currPage });
}; };
@ -211,11 +206,17 @@ export default function OffersTable({ jobTitleFilter }: OffersTableProps) {
<td className="py-4 px-6">{salary}</td> <td className="py-4 px-6">{salary}</td>
<td className="py-4 px-6">{date}</td> <td className="py-4 px-6">{date}</td>
<td className="space-x-4 py-4 px-6"> <td className="space-x-4 py-4 px-6">
<a {/* <a
className="font-medium text-indigo-600 hover:underline dark:text-indigo-500" className="font-medium text-indigo-600 hover:underline dark:text-indigo-500"
onClick={() => handleClickViewProfile(profileId)}> onClick={() => handleClickViewProfile(profileId)}>
View Profile View Profile
</a> </a> */}
<Link
className="font-medium text-indigo-600 hover:underline dark:text-indigo-500"
href={`/offers/profile/${profileId}`}>
View Profile
</Link>
{/* <a {/* <a
className="font-medium text-indigo-600 hover:underline dark:text-indigo-500" className="font-medium text-indigo-600 hover:underline dark:text-indigo-500"
href="#"> href="#">
@ -244,7 +245,6 @@ export default function OffersTable({ jobTitleFilter }: OffersTableProps) {
{`of `} {`of `}
<span className="font-semibold text-gray-900 dark:text-white"> <span className="font-semibold text-gray-900 dark:text-white">
{pagination.totalItems} {pagination.totalItems}
{/* {pagination.numOfPages * NUMBER_OF_OFFERS_IN_PAGE} */}
</span> </span>
</span> </span>
<Pagination <Pagination

@ -6,22 +6,7 @@ import {
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { HorizontalDivider } from '@tih/ui'; import { HorizontalDivider } from '@tih/ui';
export type OfferEntity = { import type { OfferEntity } from '~/components/offers/types';
base?: string;
bonus?: string;
companyName: string;
duration?: string;
id?: string;
jobLevel?: string;
jobTitle: string;
location?: string;
monthlySalary?: string;
negotiationStrategy?: string;
otherComment?: string;
receivedMonth?: string;
stocks?: string;
totalCompensation?: string;
};
type Props = Readonly<{ type Props = Readonly<{
offer: OfferEntity; offer: OfferEntity;

@ -0,0 +1,58 @@
import { ClipboardDocumentIcon, ShareIcon } from '@heroicons/react/24/outline';
import { Button, Spinner } from '@tih/ui';
type ProfileHeaderProps = Readonly<{
handleCopyEditLink: () => void;
handleCopyPublicLink: () => void;
isDisabled: boolean;
isEditable: boolean;
isLoading: boolean;
}>;
export default function ProfileComments({
handleCopyEditLink,
handleCopyPublicLink,
isDisabled,
isEditable,
isLoading,
}: ProfileHeaderProps) {
if (isLoading) {
return (
<div className="col-span-10 pt-4">
<Spinner display="block" size="lg" />
</div>
);
}
return (
<div className="m-4">
<div className="flex-end flex justify-end space-x-4">
{isEditable && (
<Button
addonPosition="start"
disabled={isDisabled}
icon={ClipboardDocumentIcon}
isLabelHidden={false}
label="Copy profile edit link"
size="sm"
variant="secondary"
onClick={handleCopyEditLink}
/>
)}
<Button
addonPosition="start"
disabled={isDisabled}
icon={ShareIcon}
isLabelHidden={false}
label="Copy public link"
size="sm"
variant="secondary"
onClick={handleCopyPublicLink}
/>
</div>
<h2 className="mt-2 text-2xl font-bold">
Discussions feature coming soon
</h2>
{/* <TextArea label="Comment" placeholder="Type your comment here" /> */}
</div>
);
}

@ -0,0 +1,79 @@
import { AcademicCapIcon, BriefcaseIcon } from '@heroicons/react/24/outline';
import { Spinner } from '@tih/ui';
import EducationCard from '~/components/offers/profile/EducationCard';
import OfferCard from '~/components/offers/profile/OfferCard';
import type { BackgroundCard, OfferEntity } from '~/components/offers/types';
import { EducationBackgroundType } from '~/components/offers/types';
type ProfileHeaderProps = Readonly<{
background?: BackgroundCard;
isLoading: boolean;
offers: Array<OfferEntity>;
selectedTab: string;
}>;
export default function ProfileDetails({
background,
isLoading,
offers,
selectedTab,
}: ProfileHeaderProps) {
if (isLoading) {
return (
<div className="col-span-10 pt-4">
<Spinner display="block" size="lg" />
</div>
);
}
if (selectedTab === 'offers') {
if (offers && offers.length !== 0) {
return (
<>
{[...offers].map((offer) => (
<OfferCard key={offer.id} offer={offer} />
))}
</>
);
}
return (
<div className="mx-8 my-4 flex flex-row">
<BriefcaseIcon className="mr-1 h-5" />
<span className="font-bold">No offer is attached.</span>
</div>
);
}
if (selectedTab === 'background') {
return (
<>
{background?.experiences && background?.experiences.length > 0 && (
<>
<div className="mx-8 my-4 flex flex-row">
<BriefcaseIcon className="mr-1 h-5" />
<span className="font-bold">Work Experience</span>
</div>
<OfferCard offer={background?.experiences[0]} />
</>
)}
{background?.educations && background?.educations.length > 0 && (
<>
<div className="mx-8 my-4 flex flex-row">
<AcademicCapIcon className="mr-1 h-5" />
<span className="font-bold">Education</span>
</div>
<EducationCard
education={{
endDate: background.educations[0].endDate,
field: background.educations[0].field,
school: background.educations[0].school,
startDate: background.educations[0].startDate,
type: EducationBackgroundType.Bachelor,
}}
/>
</>
)}
</>
);
}
return <div>Detail page for {selectedTab}</div>;
}

@ -0,0 +1,167 @@
import { useState } from 'react';
import {
BookmarkSquareIcon,
BuildingOffice2Icon,
CalendarDaysIcon,
PencilSquareIcon,
TrashIcon,
} from '@heroicons/react/24/outline';
import { Button, Dialog, Spinner, Tabs } from '@tih/ui';
import type { BackgroundCard } from '~/components/offers/types';
import ProfilePhotoHolder from './ProfilePhotoHolder';
type ProfileHeaderProps = Readonly<{
background?: BackgroundCard;
handleDelete: () => void;
isEditable: boolean;
isLoading: boolean;
selectedTab: string;
setSelectedTab: (tab: string) => void;
}>;
export default function ProfileHeader({
background,
handleDelete,
isEditable,
isLoading,
selectedTab,
setSelectedTab,
}: ProfileHeaderProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
function renderActionList() {
return (
<div className="space-x-2">
<Button
disabled={isLoading}
icon={BookmarkSquareIcon}
isLabelHidden={true}
label="Save to user account"
size="md"
variant="tertiary"
/>
<Button
disabled={isLoading}
icon={PencilSquareIcon}
isLabelHidden={true}
label="Edit"
size="md"
variant="tertiary"
/>
<Button
disabled={isLoading}
icon={TrashIcon}
isLabelHidden={true}
label="Delete"
size="md"
variant="tertiary"
onClick={() => setIsDialogOpen(true)}
/>
{isDialogOpen && (
<Dialog
isShown={isDialogOpen}
primaryButton={
<Button
display="block"
label="Delete"
variant="primary"
onClick={() => {
setIsDialogOpen(false);
handleDelete();
}}
/>
}
secondaryButton={
<Button
display="block"
label="Cancel"
variant="tertiary"
onClick={() => setIsDialogOpen(false)}
/>
}
title="Are you sure you want to delete this offer profile?"
onClose={() => setIsDialogOpen(false)}>
<div>
All comments will be gone. You will not be able to access or
recover it.
</div>
</Dialog>
)}
</div>
);
}
if (isLoading) {
return (
<div className="col-span-10 pt-4">
<Spinner display="block" size="lg" />
</div>
);
}
return (
<div className="h-40 bg-white p-4">
<div className="justify-left flex h-1/2">
<div className="mx-4 mt-2">
<ProfilePhotoHolder />
</div>
<div className="w-full">
<div className="justify-left flex flex-1">
<h2 className="flex w-4/5 text-2xl font-bold">
{background?.profileName ?? 'anonymous'}
</h2>
{isEditable && (
<div className="flex h-8 w-1/5 justify-end">
{renderActionList()}
</div>
)}
</div>
<div className="flex flex-row">
<BuildingOffice2Icon className="mr-2.5 h-5" />
<span className="mr-2 font-bold">Current:</span>
<span>{`${background?.experiences[0].companyName ?? '-'} ${
background?.experiences[0].jobLevel
} ${background?.experiences[0].jobTitle}`}</span>
</div>
<div className="flex flex-row">
<CalendarDaysIcon className="mr-2.5 h-5" />
<span className="mr-2 font-bold">YOE:</span>
<span className="mr-4">{background?.totalYoe}</span>
{background?.specificYoes &&
background?.specificYoes.length > 0 &&
background?.specificYoes.map(({ domain, yoe }) => {
return (
<span
key={domain}
className="mr-4">{`${domain}: ${yoe}`}</span>
);
})}
</div>
</div>
</div>
<div className="mt-8">
<Tabs
label="Profile Detail Navigation"
tabs={[
{
label: 'Offers',
value: 'offers',
},
{
label: 'Background',
value: 'background',
},
{
label: 'Offer Engine Analysis',
value: 'offerEngineAnalysis',
},
]}
value={selectedTab}
onChange={(value) => setSelectedTab(value)}
/>
</div>
</div>
);
}

@ -1,5 +1,4 @@
/* eslint-disable no-shadow */ /* eslint-disable no-shadow */
import type { OfferEntity } from '~/components/offers/profile/OfferCard';
import type { MonthYear } from '~/components/shared/MonthYearPicker'; import type { MonthYear } from '~/components/shared/MonthYearPicker';
/* /*
@ -120,6 +119,23 @@ type EducationDisplay = {
type: string; type: string;
}; };
export type OfferEntity = {
base?: string;
bonus?: string;
companyName: string;
duration?: string;
id?: string;
jobLevel?: string;
jobTitle: string;
location?: string;
monthlySalary?: string;
negotiationStrategy?: string;
otherComment?: string;
receivedMonth?: string;
stocks?: string;
totalCompensation?: string;
};
export type BackgroundCard = { export type BackgroundCard = {
educations: Array<EducationDisplay>; educations: Array<EducationDisplay>;
experiences: Array<OfferEntity>; experiences: Array<OfferEntity>;

@ -1,28 +1,16 @@
import Error from 'next/error'; import Error from 'next/error';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import {
AcademicCapIcon,
BookmarkSquareIcon,
BriefcaseIcon,
BuildingOffice2Icon,
CalendarDaysIcon,
ClipboardDocumentIcon,
PencilSquareIcon,
ShareIcon,
TrashIcon,
} from '@heroicons/react/24/outline';
import { Button, Dialog, Tabs } from '@tih/ui';
import EducationCard from '~/components/offers/profile/EducationCard'; import ProfileHeader from '~/components/offers/profile/ProfileHeader';
import type { OfferEntity } from '~/components/offers/profile/OfferCard'; import type { OfferEntity } from '~/components/offers/types';
import OfferCard from '~/components/offers/profile/OfferCard';
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
import type { BackgroundCard } from '~/components/offers/types'; import type { BackgroundCard } from '~/components/offers/types';
import { EducationBackgroundType } from '~/components/offers/types';
import { formatDate } from '~/utils/offers/time'; import { formatDate } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import ProfileComments from '../../../components/offers/profile/ProfileComments';
import ProfileDetails from '../../../components/offers/profile/ProfileDetails';
export default function OfferProfile() { export default function OfferProfile() {
const ErrorPage = ( const ErrorPage = (
<Error statusCode={404} title="Requested profile does not exist." /> <Error statusCode={404} title="Requested profile does not exist." />
@ -32,10 +20,10 @@ export default function OfferProfile() {
const [isEditable, setIsEditable] = useState(false); const [isEditable, setIsEditable] = useState(false);
const [background, setBackground] = useState<BackgroundCard>(); const [background, setBackground] = useState<BackgroundCard>();
const [offers, setOffers] = useState<Array<OfferEntity>>([]); const [offers, setOffers] = useState<Array<OfferEntity>>([]);
const [selectedTab, setSelectedTab] = useState('offers'); const [selectedTab, setSelectedTab] = useState('offers');
const [isDialogOpen, setIsDialogOpen] = useState(false);
const detailsQuery = trpc.useQuery( const getProfileQuery = trpc.useQuery(
[ [
'offers.profile.listOne', 'offers.profile.listOne',
{ profileId: offerProfileId as string, token: token as string }, { profileId: offerProfileId as string, token: token as string },
@ -43,7 +31,17 @@ export default function OfferProfile() {
{ {
enabled: typeof offerProfileId === 'string', enabled: typeof offerProfileId === 'string',
onSuccess: (data) => { onSuccess: (data) => {
const filteredOffers: Array<OfferEntity> = data!.offers.map((res) => { if (!data) {
router.push('/offers');
}
if (!data?.isEditable && token !== '') {
router.push(`/offers/profile/${offerProfileId}`);
}
setIsEditable(data?.isEditable ?? false);
const filteredOffers: Array<OfferEntity> = data
? data?.offers.map((res) => {
if (res.OffersFullTime) { if (res.OffersFullTime) {
const filteredOffer: OfferEntity = { const filteredOffer: OfferEntity = {
base: res.OffersFullTime.baseSalary.value base: res.OffersFullTime.baseSalary.value
@ -85,12 +83,12 @@ export default function OfferProfile() {
receivedMonth: formatDate(res.monthYearReceived), receivedMonth: formatDate(res.monthYearReceived),
}; };
return filteredOffer; return filteredOffer;
}); })
: [];
setOffers(filteredOffers ?? []); setOffers(filteredOffers);
if (data?.background) { if (data?.background) {
const filteredBackground: BackgroundCard = { const transformedBackground = {
educations: [ educations: [
{ {
endDate: data?.background.educations[0].endDate endDate: data?.background.educations[0].endDate
@ -104,7 +102,6 @@ export default function OfferProfile() {
type: data.background.educations[0].type || '-', type: data.background.educations[0].type || '-',
}, },
], ],
experiences: [ experiences: [
{ {
companyName: companyName:
@ -126,243 +123,75 @@ export default function OfferProfile() {
], ],
profileName: data.profileName, profileName: data.profileName,
specificYoes: data.background.specificYoes ?? [], specificYoes: data.background.specificYoes ?? [],
totalYoe: String(data.background.totalYoe) || '-', totalYoe: String(data.background.totalYoe) || '-',
}; };
setBackground(transformedBackground);
setBackground(filteredBackground);
} }
setIsEditable(data?.isEditable ?? false);
}, },
}, },
); );
function renderActionList() { const trpcContext = trpc.useContext();
return ( const deleteMutation = trpc.useMutation(['offers.profile.delete']);
<div className="space-x-2">
<Button
icon={BookmarkSquareIcon}
isLabelHidden={true}
label="Save to user account"
size="md"
variant="tertiary"
/>
<Button
icon={PencilSquareIcon}
isLabelHidden={true}
label="Edit"
size="md"
variant="tertiary"
/>
<Button
icon={TrashIcon}
isLabelHidden={true}
label="Delete"
size="md"
variant="tertiary"
onClick={() => setIsDialogOpen(true)}
/>
{isDialogOpen && (
<Dialog
isShown={isDialogOpen}
primaryButton={
<Button
display="block"
label="Delete"
variant="primary"
onClick={() => setIsDialogOpen(false)}
/>
}
secondaryButton={
<Button
display="block"
label="Cancel"
variant="tertiary"
onClick={() => setIsDialogOpen(false)}
/>
}
title="Are you sure you want to delete this offer profile?"
onClose={() => setIsDialogOpen(false)}>
<div>
All comments will be gone. You will not be able to access or
recover it.
</div>
</Dialog>
)}
</div>
);
}
function ProfileHeader() {
return (
<div className="relative h-40 bg-white p-4">
<div className="justify-left flex h-1/2">
<div className="mx-4 mt-2">
<ProfilePhotoHolder />
</div>
<div className="w-full">
<div className="justify-left flex ">
<h2 className="flex w-4/5 text-2xl font-bold">
{background?.profileName ?? 'anonymous'}
</h2>
{isEditable && (
<div className="flex h-8 w-1/5 justify-end">
{isEditable && renderActionList()}
</div>
)}
</div>
<div className="flex flex-row">
<BuildingOffice2Icon className="mr-2.5 h-5" />
<span className="mr-2 font-bold">Current:</span>
<span>{`${background?.experiences[0].companyName ?? '-'} ${
background?.experiences[0].jobLevel
} ${background?.experiences[0].jobTitle}`}</span>
</div>
<div className="flex flex-row">
<CalendarDaysIcon className="mr-2.5 h-5" />
<span className="mr-2 font-bold">YOE:</span>
<span className="mr-4">{background?.totalYoe}</span>
{background?.specificYoes &&
background?.specificYoes.length > 0 &&
background?.specificYoes.map(({ domain, yoe }) => (
<>
<span
key={domain}
className="mr-2">{`${domain} : ${yoe}`}</span>
<span>{background?.totalYoe}</span>
</>
))}
</div>
</div>
</div>
<div className="absolute left-8 bottom-1 content-center"> function handleDelete() {
<Tabs if (isEditable) {
label="Profile Detail Navigation" deleteMutation.mutate({
tabs={[ id: offerProfileId as string,
{ // TODO: token: token as string,
label: 'Offers', });
value: 'offers', trpcContext.invalidateQueries(['offers.profile.listOne']);
}, router.push('/offers');
{
label: 'Background',
value: 'background',
},
{
label: 'Offer Engine Analysis',
value: 'offerEngineAnalysis',
},
]}
value={selectedTab}
onChange={(value) => setSelectedTab(value)}
/>
</div>
</div>
);
}
function ProfileDetails() {
if (selectedTab === 'offers') {
return (
<>
{[...offers].map((offer) => (
<OfferCard key={offer.id} offer={offer} />
))}
</>
);
}
if (selectedTab === 'background') {
return (
<>
{background?.experiences && background?.experiences.length > 0 && (
<>
<div className="mx-8 my-4 flex flex-row">
<BriefcaseIcon className="mr-1 h-5" />
<span className="font-bold">Work Experience</span>
</div>
<OfferCard offer={background?.experiences[0]} />
</>
)}
{background?.educations && background?.educations.length > 0 && (
<>
<div className="mx-8 my-4 flex flex-row">
<AcademicCapIcon className="mr-1 h-5" />
<span className="font-bold">Education</span>
</div>
<EducationCard
education={{
endDate: background.educations[0].endDate,
field: background.educations[0].field,
school: background.educations[0].school,
startDate: background.educations[0].startDate,
type: EducationBackgroundType.Bachelor,
}}
/>
</>
)}
</>
);
} }
return <div>Detail page for {selectedTab}</div>;
} }
function ProfileComments() { function handleCopyEditLink() {
return (
<div className="m-4">
<div className="flex-end flex justify-end space-x-4">
{isEditable && (
<Button
addonPosition="start"
icon={ClipboardDocumentIcon}
isLabelHidden={false}
label="Copy profile edit link"
size="sm"
variant="secondary"
onClick={() => {
// TODO: Add notification // TODO: Add notification
navigator.clipboard.writeText( navigator.clipboard.writeText(
`${router.pathname}/${offerProfileId}?token=${token}`, `${window.location.origin}/offers/profile/${offerProfileId}?token=${token}`,
); );
}} }
/>
)} function handleCopyPublicLink() {
<Button
addonPosition="start"
icon={ShareIcon}
isLabelHidden={false}
label="Copy public link"
size="sm"
variant="secondary"
onClick={() => {
// TODO: Add notification
navigator.clipboard.writeText( navigator.clipboard.writeText(
`${window.location.origin}/offers/profile/${offerProfileId}`, `${window.location.origin}/offers/profile/${offerProfileId}`,
); );
}}
/>
</div>
<h2 className="mt-2 text-2xl font-bold">
Discussions feature coming soon
</h2>
{/* <TextArea label="Comment" placeholder="Type your comment here" /> */}
</div>
);
} }
return ( return (
<> <>
{detailsQuery.isError && ErrorPage} {getProfileQuery.isError && ErrorPage}
{!getProfileQuery.isError && (
<div className="mb-4 flex flex h-screen w-screen items-center justify-center divide-x"> <div className="mb-4 flex flex h-screen w-screen items-center justify-center divide-x">
<div className="h-full w-2/3 divide-y"> <div className="h-full w-2/3 divide-y">
<ProfileHeader /> <ProfileHeader
background={background}
handleDelete={handleDelete}
isEditable={isEditable}
isLoading={getProfileQuery.isLoading}
selectedTab={selectedTab}
setSelectedTab={setSelectedTab}
/>
<div className="h-4/5 w-full overflow-y-scroll pb-32"> <div className="h-4/5 w-full overflow-y-scroll pb-32">
<ProfileDetails /> <ProfileDetails
background={background}
isLoading={getProfileQuery.isLoading}
offers={offers}
selectedTab={selectedTab}
/>
</div> </div>
</div> </div>
<div className="h-full w-1/3 bg-white"> <div className="h-full w-1/3 bg-white">
<ProfileComments /> <ProfileComments
handleCopyEditLink={handleCopyEditLink}
handleCopyPublicLink={handleCopyPublicLink}
isDisabled={deleteMutation.isLoading}
isEditable={isEditable}
isLoading={getProfileQuery.isLoading}
/>
</div> </div>
</div> </div>
)}
</> </>
); );
} }

Loading…
Cancel
Save