[offers][feat] save to user profile (#462)
parent
3ecc756052
commit
e152de2284
@ -0,0 +1,55 @@
|
|||||||
|
import { JobType } from '@prisma/client';
|
||||||
|
import { HorizontalDivider } from '@tih/ui';
|
||||||
|
|
||||||
|
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||||
|
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||||
|
|
||||||
|
import { convertMoneyToString } from '~/utils/offers/currency';
|
||||||
|
import { formatDate } from '~/utils/offers/time';
|
||||||
|
|
||||||
|
import type { UserProfileOffer } from '~/types/offers';
|
||||||
|
|
||||||
|
type Props = Readonly<{
|
||||||
|
disableTopDivider?: boolean;
|
||||||
|
offer: UserProfileOffer;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function DashboardProfileCard({
|
||||||
|
disableTopDivider,
|
||||||
|
offer: {
|
||||||
|
company,
|
||||||
|
income,
|
||||||
|
jobType,
|
||||||
|
level,
|
||||||
|
location,
|
||||||
|
monthYearReceived,
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!disableTopDivider && <HorizontalDivider />}
|
||||||
|
<div className="flex items-end justify-between">
|
||||||
|
<div className="col-span-1 row-span-3">
|
||||||
|
<p className="font-bold">
|
||||||
|
{getLabelForJobTitleType(title as JobTitleType)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{location
|
||||||
|
? `Company: ${company.name}, ${location}`
|
||||||
|
: `Company: ${company.name}`}
|
||||||
|
</p>
|
||||||
|
{level && <p>Level: {level}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1 row-span-3">
|
||||||
|
<p className="text-end">{formatDate(monthYearReceived)}</p>
|
||||||
|
<p className="text-end text-xl">
|
||||||
|
{jobType === JobType.FULLTIME
|
||||||
|
? `${convertMoneyToString(income)} / year`
|
||||||
|
: `${convertMoneyToString(income)} / month`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { ArrowRightIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { Button, useToast } from '@tih/ui';
|
||||||
|
|
||||||
|
import DashboardOfferCard from '~/components/offers/dashboard/DashboardOfferCard';
|
||||||
|
|
||||||
|
import { formatDate } from '~/utils/offers/time';
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
import ProfilePhotoHolder from '../profile/ProfilePhotoHolder';
|
||||||
|
|
||||||
|
import type { UserProfile, UserProfileOffer } from '~/types/offers';
|
||||||
|
|
||||||
|
type Props = Readonly<{
|
||||||
|
profile: UserProfile;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function DashboardProfileCard({
|
||||||
|
profile: { createdAt, id, offers, profileName, token },
|
||||||
|
}: Props) {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
const trpcContext = trpc.useContext();
|
||||||
|
const PROFILE_URL = `/offers/profile/${id}?token=${token}`;
|
||||||
|
const removeSavedProfileMutation = trpc.useMutation(
|
||||||
|
'offers.user.profile.removeFromUserProfile',
|
||||||
|
{
|
||||||
|
onError: () => {
|
||||||
|
showToast({
|
||||||
|
title: `Server error.`,
|
||||||
|
variant: 'failure',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
trpcContext.invalidateQueries(['offers.user.profile.getUserProfiles']);
|
||||||
|
showToast({
|
||||||
|
title: `Profile removed from your dashboard successfully!`,
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleRemoveProfile() {
|
||||||
|
removeSavedProfileMutation.mutate({ profileId: id });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 bg-white px-4 pt-5 sm:px-4">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="-ml-4 -mt-2 flex flex-wrap items-center justify-between border-b border-gray-300 pb-4 sm:flex-nowrap">
|
||||||
|
<div className="flex items-center gap-x-5">
|
||||||
|
<div>
|
||||||
|
<ProfilePhotoHolder size="sm" />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-10">
|
||||||
|
<p className="text-xl font-bold">{profileName}</p>
|
||||||
|
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<span>Created at {formatDate(createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex self-start">
|
||||||
|
<Button
|
||||||
|
disabled={removeSavedProfileMutation.isLoading}
|
||||||
|
icon={XMarkIcon}
|
||||||
|
isLabelHidden={true}
|
||||||
|
label="Remove Profile"
|
||||||
|
size="md"
|
||||||
|
variant="tertiary"
|
||||||
|
onClick={handleRemoveProfile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Offers */}
|
||||||
|
<div>
|
||||||
|
{offers.map((offer: UserProfileOffer, index) =>
|
||||||
|
index === 0 ? (
|
||||||
|
<DashboardOfferCard
|
||||||
|
key={offer.id}
|
||||||
|
disableTopDivider={true}
|
||||||
|
offer={offer}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DashboardOfferCard key={offer.id} offer={offer} />
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end pt-1">
|
||||||
|
<Button
|
||||||
|
disabled={removeSavedProfileMutation.isLoading}
|
||||||
|
icon={ArrowRightIcon}
|
||||||
|
isLabelHidden={false}
|
||||||
|
label="Read full profile"
|
||||||
|
size="md"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => router.push(PROFILE_URL)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { usePopperTooltip } from 'react-popper-tooltip';
|
||||||
|
import type { Placement } from '@popperjs/core';
|
||||||
|
|
||||||
|
type TooltipProps = Readonly<{
|
||||||
|
children: ReactNode;
|
||||||
|
placement?: Placement;
|
||||||
|
tooltipContent: ReactNode;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function Tooltip({
|
||||||
|
children,
|
||||||
|
tooltipContent,
|
||||||
|
placement = 'bottom-start',
|
||||||
|
}: TooltipProps) {
|
||||||
|
const {
|
||||||
|
getTooltipProps,
|
||||||
|
getArrowProps,
|
||||||
|
setTooltipRef,
|
||||||
|
setTriggerRef,
|
||||||
|
visible,
|
||||||
|
} = usePopperTooltip({
|
||||||
|
interactive: true,
|
||||||
|
placement,
|
||||||
|
trigger: ['focus', 'hover'],
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={setTriggerRef}>{children}</div>
|
||||||
|
{visible && (
|
||||||
|
<div
|
||||||
|
ref={setTooltipRef}
|
||||||
|
{...getTooltipProps({
|
||||||
|
className: 'tooltip-container ',
|
||||||
|
})}>
|
||||||
|
{tooltipContent}
|
||||||
|
<div {...getArrowProps({ className: 'tooltip-arrow' })} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { signIn, useSession } from 'next-auth/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button, Spinner } from '@tih/ui';
|
||||||
|
|
||||||
|
import DashboardOfferCard from '~/components/offers/dashboard/DashboardProfileCard';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
import type { UserProfile } from '~/types/offers';
|
||||||
|
|
||||||
|
export default function ProfilesDashboard() {
|
||||||
|
const { status } = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
const [userProfiles, setUserProfiles] = useState<Array<UserProfile>>([]);
|
||||||
|
|
||||||
|
const userProfilesQuery = trpc.useQuery(
|
||||||
|
['offers.user.profile.getUserProfiles'],
|
||||||
|
{
|
||||||
|
onError: (error) => {
|
||||||
|
if (error.data?.code === 'UNAUTHORIZED') {
|
||||||
|
signIn();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: (response: Array<UserProfile>) => {
|
||||||
|
setUserProfiles(response);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status === 'loading' || userProfilesQuery.isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-screen">
|
||||||
|
<div className="m-auto mx-auto w-full justify-center">
|
||||||
|
<Spinner className="m-10" display="block" size="lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status === 'unauthenticated') {
|
||||||
|
signIn();
|
||||||
|
}
|
||||||
|
if (userProfiles.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-screen">
|
||||||
|
<div className="m-auto mx-auto w-full justify-center text-xl">
|
||||||
|
<div className="mb-8 flex w-full flex-row justify-center">
|
||||||
|
<h2>You have not saved any offer profiles yet.</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-center">
|
||||||
|
<Button
|
||||||
|
label="Submit your offers now!"
|
||||||
|
size="lg"
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => router.push('/offers/submit')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{userProfilesQuery.isLoading && (
|
||||||
|
<div className="flex h-screen w-screen">
|
||||||
|
<div className="m-auto mx-auto w-full justify-center">
|
||||||
|
<Spinner className="m-10" display="block" size="lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!userProfilesQuery.isLoading && (
|
||||||
|
<div className="mt-8 overflow-y-auto">
|
||||||
|
<h1 className="mx-auto mb-4 w-3/4 text-start text-4xl font-bold text-slate-900">
|
||||||
|
Your repository
|
||||||
|
</h1>
|
||||||
|
<p className="mx-auto w-3/4 text-start text-xl text-slate-900">
|
||||||
|
Save your offer profiles to respository to easily access and edit
|
||||||
|
them later.
|
||||||
|
</p>
|
||||||
|
<div className="justfy-center mt-8 flex w-screen">
|
||||||
|
<ul className="mx-auto w-3/4 space-y-3" role="list">
|
||||||
|
{userProfiles?.map((profile) => (
|
||||||
|
<li
|
||||||
|
key={profile.id}
|
||||||
|
className="overflow-hidden bg-white px-4 py-4 shadow sm:rounded-md sm:px-6">
|
||||||
|
<DashboardOfferCard key={profile.id} profile={profile} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in new issue