[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 { 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<Pagination>({
@ -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) {
<td className="py-4 px-6">{salary}</td>
<td className="py-4 px-6">{date}</td>
<td className="space-x-4 py-4 px-6">
<a
{/* <a
className="font-medium text-indigo-600 hover:underline dark:text-indigo-500"
onClick={() => handleClickViewProfile(profileId)}>
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
className="font-medium text-indigo-600 hover:underline dark:text-indigo-500"
href="#">
@ -244,7 +245,6 @@ export default function OffersTable({ jobTitleFilter }: OffersTableProps) {
{`of `}
<span className="font-semibold text-gray-900 dark:text-white">
{pagination.totalItems}
{/* {pagination.numOfPages * NUMBER_OF_OFFERS_IN_PAGE} */}
</span>
</span>
<Pagination

@ -6,22 +6,7 @@ import {
} from '@heroicons/react/24/outline';
import { HorizontalDivider } from '@tih/ui';
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;
};
import type { OfferEntity } from '~/components/offers/types';
type Props = Readonly<{
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 */
import type { OfferEntity } from '~/components/offers/profile/OfferCard';
import type { MonthYear } from '~/components/shared/MonthYearPicker';
/*
@ -120,6 +119,23 @@ type EducationDisplay = {
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 = {
educations: Array<EducationDisplay>;
experiences: Array<OfferEntity>;

@ -1,28 +1,16 @@
import Error from 'next/error';
import { useRouter } from 'next/router';
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 type { OfferEntity } from '~/components/offers/profile/OfferCard';
import OfferCard from '~/components/offers/profile/OfferCard';
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
import ProfileHeader from '~/components/offers/profile/ProfileHeader';
import type { OfferEntity } from '~/components/offers/types';
import type { BackgroundCard } from '~/components/offers/types';
import { EducationBackgroundType } from '~/components/offers/types';
import { formatDate } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc';
import ProfileComments from '../../../components/offers/profile/ProfileComments';
import ProfileDetails from '../../../components/offers/profile/ProfileDetails';
export default function OfferProfile() {
const ErrorPage = (
<Error statusCode={404} title="Requested profile does not exist." />
@ -32,10 +20,10 @@ export default function OfferProfile() {
const [isEditable, setIsEditable] = useState(false);
const [background, setBackground] = useState<BackgroundCard>();
const [offers, setOffers] = useState<Array<OfferEntity>>([]);
const [selectedTab, setSelectedTab] = useState('offers');
const [isDialogOpen, setIsDialogOpen] = useState(false);
const detailsQuery = trpc.useQuery(
const getProfileQuery = trpc.useQuery(
[
'offers.profile.listOne',
{ profileId: offerProfileId as string, token: token as string },
@ -43,54 +31,64 @@ export default function OfferProfile() {
{
enabled: typeof offerProfileId === 'string',
onSuccess: (data) => {
const filteredOffers: Array<OfferEntity> = 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}`
: '',
};
if (!data) {
router.push('/offers');
}
if (!data?.isEditable && token !== '') {
router.push(`/offers/profile/${offerProfileId}`);
}
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;
});
setIsEditable(data?.isEditable ?? false);
setOffers(filteredOffers ?? []);
const filteredOffers: Array<OfferEntity> = data
? 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 = {
const transformedBackground = {
educations: [
{
endDate: data?.background.educations[0].endDate
@ -104,7 +102,6 @@ export default function OfferProfile() {
type: data.background.educations[0].type || '-',
},
],
experiences: [
{
companyName:
@ -126,243 +123,75 @@ export default function OfferProfile() {
],
profileName: data.profileName,
specificYoes: data.background.specificYoes ?? [],
totalYoe: String(data.background.totalYoe) || '-',
};
setBackground(filteredBackground);
setBackground(transformedBackground);
}
setIsEditable(data?.isEditable ?? false);
},
},
);
function renderActionList() {
return (
<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>
const trpcContext = trpc.useContext();
const deleteMutation = trpc.useMutation(['offers.profile.delete']);
<div className="absolute left-8 bottom-1 content-center">
<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>
);
function handleDelete() {
if (isEditable) {
deleteMutation.mutate({
id: offerProfileId as string,
// TODO: token: token as string,
});
trpcContext.invalidateQueries(['offers.profile.listOne']);
router.push('/offers');
}
}
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 handleCopyEditLink() {
// TODO: Add notification
navigator.clipboard.writeText(
`${window.location.origin}/offers/profile/${offerProfileId}?token=${token}`,
);
}
function ProfileComments() {
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
navigator.clipboard.writeText(
`${router.pathname}/${offerProfileId}?token=${token}`,
);
}}
/>
)}
<Button
addonPosition="start"
icon={ShareIcon}
isLabelHidden={false}
label="Copy public link"
size="sm"
variant="secondary"
onClick={() => {
// TODO: Add notification
navigator.clipboard.writeText(
`${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>
function handleCopyPublicLink() {
navigator.clipboard.writeText(
`${window.location.origin}/offers/profile/${offerProfileId}`,
);
}
return (
<>
{detailsQuery.isError && ErrorPage}
<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">
<ProfileHeader />
<div className="h-4/5 w-full overflow-y-scroll pb-32">
<ProfileDetails />
{getProfileQuery.isError && ErrorPage}
{!getProfileQuery.isError && (
<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">
<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">
<ProfileDetails
background={background}
isLoading={getProfileQuery.isLoading}
offers={offers}
selectedTab={selectedTab}
/>
</div>
</div>
<div className="h-full w-1/3 bg-white">
<ProfileComments
handleCopyEditLink={handleCopyEditLink}
handleCopyPublicLink={handleCopyPublicLink}
isDisabled={deleteMutation.isLoading}
isEditable={isEditable}
isLoading={getProfileQuery.isLoading}
/>
</div>
</div>
<div className="h-full w-1/3 bg-white">
<ProfileComments />
</div>
</div>
)}
</>
);
}

Loading…
Cancel
Save