mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 12:43:12 +08:00
[offers][feat] save to user profile (#462)
This commit is contained in:
@ -9,7 +9,9 @@ import { Bars3BottomLeftIcon } from '@heroicons/react/24/outline';
|
|||||||
|
|
||||||
import GlobalNavigation from '~/components/global/GlobalNavigation';
|
import GlobalNavigation from '~/components/global/GlobalNavigation';
|
||||||
import HomeNavigation from '~/components/global/HomeNavigation';
|
import HomeNavigation from '~/components/global/HomeNavigation';
|
||||||
import OffersNavigation from '~/components/offers/OffersNavigation';
|
import OffersNavigation, {
|
||||||
|
OffersNavigationAuthenticated,
|
||||||
|
} from '~/components/offers/OffersNavigation';
|
||||||
import QuestionsNavigation from '~/components/questions/QuestionsNavigation';
|
import QuestionsNavigation from '~/components/questions/QuestionsNavigation';
|
||||||
import ResumesNavigation from '~/components/resumes/ResumesNavigation';
|
import ResumesNavigation from '~/components/resumes/ResumesNavigation';
|
||||||
|
|
||||||
@ -105,6 +107,7 @@ function ProfileJewel() {
|
|||||||
export default function AppShell({ children }: Props) {
|
export default function AppShell({ children }: Props) {
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const currentProductNavigation: Readonly<{
|
const currentProductNavigation: Readonly<{
|
||||||
googleAnalyticsMeasurementID: string;
|
googleAnalyticsMeasurementID: string;
|
||||||
@ -120,8 +123,11 @@ export default function AppShell({ children }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (path.startsWith('/offers')) {
|
if (path.startsWith('/offers')) {
|
||||||
|
if (session == null) {
|
||||||
return OffersNavigation;
|
return OffersNavigation;
|
||||||
}
|
}
|
||||||
|
return OffersNavigationAuthenticated;
|
||||||
|
}
|
||||||
|
|
||||||
if (path.startsWith('/questions')) {
|
if (path.startsWith('/questions')) {
|
||||||
return QuestionsNavigation;
|
return QuestionsNavigation;
|
||||||
|
@ -5,6 +5,12 @@ const navigation: ProductNavigationItems = [
|
|||||||
{ href: '/offers/features', name: 'Features' },
|
{ href: '/offers/features', name: 'Features' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const navigationAuthenticated: ProductNavigationItems = [
|
||||||
|
{ href: '/offers/submit', name: 'Analyze your offers' },
|
||||||
|
{ href: '/offers/dashboard', name: 'Your repository' },
|
||||||
|
{ href: '/offers/features', name: 'Features' },
|
||||||
|
];
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
// TODO: Change this to your own GA4 measurement ID.
|
// TODO: Change this to your own GA4 measurement ID.
|
||||||
googleAnalyticsMeasurementID: 'G-34XRGLEVCF',
|
googleAnalyticsMeasurementID: 'G-34XRGLEVCF',
|
||||||
@ -17,4 +23,9 @@ const config = {
|
|||||||
titleHref: '/offers',
|
titleHref: '/offers',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const OffersNavigationAuthenticated = {
|
||||||
|
...config,
|
||||||
|
navigation: navigationAuthenticated,
|
||||||
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -10,12 +10,15 @@ import {
|
|||||||
} from '@tih/ui';
|
} from '@tih/ui';
|
||||||
|
|
||||||
import ExpandableCommentCard from '~/components/offers/profile/comments/ExpandableCommentCard';
|
import ExpandableCommentCard from '~/components/offers/profile/comments/ExpandableCommentCard';
|
||||||
|
import Tooltip from '~/components/offers/util/Tooltip';
|
||||||
|
|
||||||
import { copyProfileLink } from '~/utils/offers/link';
|
import { copyProfileLink } from '~/utils/offers/link';
|
||||||
import { trpc } from '~/utils/trpc';
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
import type { OffersDiscussion, Reply } from '~/types/offers';
|
import type { OffersDiscussion, Reply } from '~/types/offers';
|
||||||
|
|
||||||
|
import 'react-popper-tooltip/dist/styles.css';
|
||||||
|
|
||||||
type ProfileHeaderProps = Readonly<{
|
type ProfileHeaderProps = Readonly<{
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
@ -107,6 +110,7 @@ export default function ProfileComments({
|
|||||||
<div className="m-4 h-full">
|
<div className="m-4 h-full">
|
||||||
<div className="flex-end flex justify-end space-x-4">
|
<div className="flex-end flex justify-end space-x-4">
|
||||||
{isEditable && (
|
{isEditable && (
|
||||||
|
<Tooltip tooltipContent="Copy this link to edit your profile later">
|
||||||
<Button
|
<Button
|
||||||
addonPosition="start"
|
addonPosition="start"
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
@ -123,7 +127,9 @@ export default function ProfileComments({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
<Tooltip tooltipContent="Share this profile with your friends">
|
||||||
<Button
|
<Button
|
||||||
addonPosition="start"
|
addonPosition="start"
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
@ -140,6 +146,7 @@ export default function ProfileComments({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="mt-2 mb-6 text-2xl font-bold">Discussions</h2>
|
<h2 className="mt-2 mb-6 text-2xl font-bold">Discussions</h2>
|
||||||
{isEditable || session?.user?.name ? (
|
{isEditable || session?.user?.name ? (
|
||||||
|
@ -1,27 +1,32 @@
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
BookmarkIcon as BookmarkIconOutline,
|
||||||
BuildingOffice2Icon,
|
BuildingOffice2Icon,
|
||||||
CalendarDaysIcon,
|
CalendarDaysIcon,
|
||||||
PencilSquareIcon,
|
PencilSquareIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { Button, Dialog, Spinner, Tabs } from '@tih/ui';
|
import { BookmarkIcon as BookmarkIconSolid } from '@heroicons/react/24/solid';
|
||||||
|
import { Button, Dialog, Spinner, Tabs, useToast } from '@tih/ui';
|
||||||
|
|
||||||
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
|
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
|
||||||
import type { BackgroundDisplayData } from '~/components/offers/types';
|
import type { BackgroundDisplayData } from '~/components/offers/types';
|
||||||
import { JobTypeLabel } from '~/components/offers/types';
|
import { JobTypeLabel } from '~/components/offers/types';
|
||||||
|
|
||||||
import { getProfileEditPath } from '~/utils/offers/link';
|
import { getProfileEditPath } from '~/utils/offers/link';
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
import type { ProfileDetailTab } from '../constants';
|
import type { ProfileDetailTab } from '../constants';
|
||||||
import { profileDetailTabs } from '../constants';
|
import { profileDetailTabs } from '../constants';
|
||||||
|
import Tooltip from '../util/Tooltip';
|
||||||
|
|
||||||
type ProfileHeaderProps = Readonly<{
|
type ProfileHeaderProps = Readonly<{
|
||||||
background?: BackgroundDisplayData;
|
background?: BackgroundDisplayData;
|
||||||
handleDelete: () => void;
|
handleDelete: () => void;
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
isSaved?: boolean;
|
||||||
selectedTab: ProfileDetailTab;
|
selectedTab: ProfileDetailTab;
|
||||||
setSelectedTab: (tab: ProfileDetailTab) => void;
|
setSelectedTab: (tab: ProfileDetailTab) => void;
|
||||||
}>;
|
}>;
|
||||||
@ -31,28 +36,91 @@ export default function ProfileHeader({
|
|||||||
handleDelete,
|
handleDelete,
|
||||||
isEditable,
|
isEditable,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isSaved = false,
|
||||||
selectedTab,
|
selectedTab,
|
||||||
setSelectedTab,
|
setSelectedTab,
|
||||||
}: ProfileHeaderProps) {
|
}: ProfileHeaderProps) {
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
// Const [saved, setSaved] = useState(isSaved);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const trpcContext = trpc.useContext();
|
||||||
const { offerProfileId = '', token = '' } = router.query;
|
const { offerProfileId = '', token = '' } = router.query;
|
||||||
|
const { showToast } = useToast();
|
||||||
const handleEditClick = () => {
|
const handleEditClick = () => {
|
||||||
router.push(getProfileEditPath(offerProfileId as string, token as string));
|
router.push(getProfileEditPath(offerProfileId as string, token as string));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveMutation = trpc.useMutation(
|
||||||
|
['offers.user.profile.addToUserProfile'],
|
||||||
|
{
|
||||||
|
onError: () => {
|
||||||
|
showToast({
|
||||||
|
title: `Failed to saved to dashboard!`,
|
||||||
|
variant: 'failure',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
// SetSaved(true);
|
||||||
|
showToast({
|
||||||
|
title: `Saved to dashboard!`,
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const unsaveMutation = trpc.useMutation(
|
||||||
|
['offers.user.profile.removeFromUserProfile'],
|
||||||
|
{
|
||||||
|
onError: () => {
|
||||||
|
showToast({
|
||||||
|
title: `Failed to saved to dashboard!`,
|
||||||
|
variant: 'failure',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
// SetSaved(false);
|
||||||
|
showToast({
|
||||||
|
title: `Removed from dashboard!`,
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
trpcContext.invalidateQueries(['offers.profile.listOne']);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleSaved = () => {
|
||||||
|
if (isSaved) {
|
||||||
|
unsaveMutation.mutate({ profileId: offerProfileId as string });
|
||||||
|
} else {
|
||||||
|
saveMutation.mutate({
|
||||||
|
profileId: offerProfileId as string,
|
||||||
|
token: token as string,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function renderActionList() {
|
function renderActionList() {
|
||||||
return (
|
return (
|
||||||
<div className="space-x-2">
|
<div className="flex justify-center space-x-2">
|
||||||
{/* <Button
|
<Tooltip
|
||||||
disabled={isLoading}
|
tooltipContent={
|
||||||
icon={BookmarkSquareIcon}
|
isSaved ? 'Remove from account' : 'Save to your account'
|
||||||
|
}>
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
isLoading || saveMutation.isLoading || unsaveMutation.isLoading
|
||||||
|
}
|
||||||
|
icon={isSaved ? BookmarkIconSolid : BookmarkIconOutline}
|
||||||
isLabelHidden={true}
|
isLabelHidden={true}
|
||||||
label="Save to user account"
|
isLoading={saveMutation.isLoading || unsaveMutation.isLoading}
|
||||||
|
label={isSaved ? 'Remove from account' : 'Save to your account'}
|
||||||
size="md"
|
size="md"
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
/> */}
|
onClick={toggleSaved}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip tooltipContent="Edit profile">
|
||||||
<Button
|
<Button
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
icon={PencilSquareIcon}
|
icon={PencilSquareIcon}
|
||||||
@ -62,6 +130,8 @@ export default function ProfileHeader({
|
|||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
onClick={handleEditClick}
|
onClick={handleEditClick}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip tooltipContent="Delete profile">
|
||||||
<Button
|
<Button
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
icon={TrashIcon}
|
icon={TrashIcon}
|
||||||
@ -71,6 +141,7 @@ export default function ProfileHeader({
|
|||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
onClick={() => setIsDialogOpen(true)}
|
onClick={() => setIsDialogOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
{isDialogOpen && (
|
{isDialogOpen && (
|
||||||
<Dialog
|
<Dialog
|
||||||
isShown={isDialogOpen}
|
isShown={isDialogOpen}
|
||||||
|
42
apps/portal/src/components/offers/util/Tooltip.tsx
Normal file
42
apps/portal/src/components/offers/util/Tooltip.tsx
Normal file
@ -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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -35,7 +35,8 @@ import type {
|
|||||||
SpecificYoe,
|
SpecificYoe,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
UserProfileOffer,
|
UserProfileOffer,
|
||||||
Valuation} from '~/types/offers';
|
Valuation,
|
||||||
|
} from '~/types/offers';
|
||||||
|
|
||||||
const analysisOfferDtoMapper = (
|
const analysisOfferDtoMapper = (
|
||||||
offer: OffersOffer & {
|
offer: OffersOffer & {
|
||||||
@ -530,7 +531,7 @@ export const profileDtoMapper = (
|
|||||||
user: User | null;
|
user: User | null;
|
||||||
},
|
},
|
||||||
inputToken: string | undefined,
|
inputToken: string | undefined,
|
||||||
inputUserId: string | null | undefined
|
inputUserId: string | null | undefined,
|
||||||
) => {
|
) => {
|
||||||
const profileDto: Profile = {
|
const profileDto: Profile = {
|
||||||
analysis: profileAnalysisDtoMapper(profile.analysis),
|
analysis: profileAnalysisDtoMapper(profile.analysis),
|
||||||
@ -547,7 +548,7 @@ export const profileDtoMapper = (
|
|||||||
profileDto.editToken = profile.editToken ?? null;
|
profileDto.editToken = profile.editToken ?? null;
|
||||||
profileDto.isEditable = true;
|
profileDto.isEditable = true;
|
||||||
|
|
||||||
const users = profile.user
|
const users = profile.user;
|
||||||
|
|
||||||
// TODO: BRYANN UNCOMMENT THIS ONCE U CHANGE THE SCHEMA
|
// TODO: BRYANN UNCOMMENT THIS ONCE U CHANGE THE SCHEMA
|
||||||
// for (let i = 0; i < users.length; i++) {
|
// for (let i = 0; i < users.length; i++) {
|
||||||
@ -558,7 +559,7 @@ export const profileDtoMapper = (
|
|||||||
|
|
||||||
// TODO: REMOVE THIS ONCE U CHANGE THE SCHEMA
|
// TODO: REMOVE THIS ONCE U CHANGE THE SCHEMA
|
||||||
if (users?.id === inputUserId) {
|
if (users?.id === inputUserId) {
|
||||||
profileDto.isSaved = true
|
profileDto.isSaved = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -645,38 +646,53 @@ export const getOffersResponseMapper = (
|
|||||||
return getOffersResponse;
|
return getOffersResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUserProfileResponseMapper = (res: User & {
|
export const getUserProfileResponseMapper = (
|
||||||
OffersProfile: Array<OffersProfile & {
|
res:
|
||||||
offers: Array<OffersOffer & {
|
| (User & {
|
||||||
|
OffersProfile: Array<
|
||||||
|
OffersProfile & {
|
||||||
|
offers: Array<
|
||||||
|
OffersOffer & {
|
||||||
company: Company;
|
company: Company;
|
||||||
offersFullTime: (OffersFullTime & { totalCompensation: OffersCurrency }) | null;
|
offersFullTime:
|
||||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||||
}>;
|
| null;
|
||||||
}>;
|
offersIntern:
|
||||||
} | null): Array<UserProfile> => {
|
| (OffersIntern & { monthlySalary: OffersCurrency })
|
||||||
|
| null;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
})
|
||||||
|
| null,
|
||||||
|
): Array<UserProfile> => {
|
||||||
if (res) {
|
if (res) {
|
||||||
return res.OffersProfile.map((profile) => {
|
return res.OffersProfile.map((profile) => {
|
||||||
return {
|
return {
|
||||||
createdAt: profile.createdAt,
|
createdAt: profile.createdAt,
|
||||||
id: profile.id,
|
id: profile.id,
|
||||||
offers: profile.offers.map((offer) => {
|
offers: profile.offers.map((offer) => {
|
||||||
return userProfileOfferDtoMapper(offer)
|
return userProfileOfferDtoMapper(offer);
|
||||||
}),
|
}),
|
||||||
profileName: profile.profileName,
|
profileName: profile.profileName,
|
||||||
token: profile.editToken
|
token: profile.editToken,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return [];
|
||||||
}
|
};
|
||||||
|
|
||||||
const userProfileOfferDtoMapper = (
|
const userProfileOfferDtoMapper = (
|
||||||
offer: OffersOffer & {
|
offer: OffersOffer & {
|
||||||
company: Company;
|
company: Company;
|
||||||
offersFullTime: (OffersFullTime & { totalCompensation: OffersCurrency }) | null;
|
offersFullTime:
|
||||||
|
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
||||||
|
| null;
|
||||||
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||||
}): UserProfileOffer => {
|
},
|
||||||
|
): UserProfileOffer => {
|
||||||
const mappedOffer: UserProfileOffer = {
|
const mappedOffer: UserProfileOffer = {
|
||||||
company: offersCompanyDtoMapper(offer.company),
|
company: offersCompanyDtoMapper(offer.company),
|
||||||
id: offer.id,
|
id: offer.id,
|
||||||
@ -695,11 +711,10 @@ const userProfileOfferDtoMapper = (
|
|||||||
offer.jobType === JobType.FULLTIME
|
offer.jobType === JobType.FULLTIME
|
||||||
? offer.offersFullTime?.title ?? ''
|
? offer.offersFullTime?.title ?? ''
|
||||||
: offer.offersIntern?.title ?? '',
|
: offer.offersIntern?.title ?? '',
|
||||||
}
|
};
|
||||||
|
|
||||||
if (offer.offersFullTime?.totalCompensation) {
|
if (offer.offersFullTime?.totalCompensation) {
|
||||||
mappedOffer.income.value =
|
mappedOffer.income.value = offer.offersFullTime.totalCompensation.value;
|
||||||
offer.offersFullTime.totalCompensation.value;
|
|
||||||
mappedOffer.income.currency =
|
mappedOffer.income.currency =
|
||||||
offer.offersFullTime.totalCompensation.currency;
|
offer.offersFullTime.totalCompensation.currency;
|
||||||
mappedOffer.income.id = offer.offersFullTime.totalCompensation.id;
|
mappedOffer.income.id = offer.offersFullTime.totalCompensation.id;
|
||||||
@ -709,11 +724,9 @@ const userProfileOfferDtoMapper = (
|
|||||||
offer.offersFullTime.totalCompensation.baseCurrency;
|
offer.offersFullTime.totalCompensation.baseCurrency;
|
||||||
} else if (offer.offersIntern?.monthlySalary) {
|
} else if (offer.offersIntern?.monthlySalary) {
|
||||||
mappedOffer.income.value = offer.offersIntern.monthlySalary.value;
|
mappedOffer.income.value = offer.offersIntern.monthlySalary.value;
|
||||||
mappedOffer.income.currency =
|
mappedOffer.income.currency = offer.offersIntern.monthlySalary.currency;
|
||||||
offer.offersIntern.monthlySalary.currency;
|
|
||||||
mappedOffer.income.id = offer.offersIntern.monthlySalary.id;
|
mappedOffer.income.id = offer.offersIntern.monthlySalary.id;
|
||||||
mappedOffer.income.baseValue =
|
mappedOffer.income.baseValue = offer.offersIntern.monthlySalary.baseValue;
|
||||||
offer.offersIntern.monthlySalary.baseValue;
|
|
||||||
mappedOffer.income.baseCurrency =
|
mappedOffer.income.baseCurrency =
|
||||||
offer.offersIntern.monthlySalary.baseCurrency;
|
offer.offersIntern.monthlySalary.baseCurrency;
|
||||||
} else {
|
} else {
|
||||||
@ -723,5 +736,5 @@ const userProfileOfferDtoMapper = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return mappedOffer
|
return mappedOffer;
|
||||||
}
|
};
|
||||||
|
95
apps/portal/src/pages/offers/dashboard.tsx
Normal file
95
apps/portal/src/pages/offers/dashboard.tsx
Normal file
@ -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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -188,6 +188,7 @@ export default function OfferProfile() {
|
|||||||
handleDelete={handleDelete}
|
handleDelete={handleDelete}
|
||||||
isEditable={isEditable}
|
isEditable={isEditable}
|
||||||
isLoading={getProfileQuery.isLoading}
|
isLoading={getProfileQuery.isLoading}
|
||||||
|
isSaved={getProfileQuery.data?.isSaved}
|
||||||
selectedTab={selectedTab}
|
selectedTab={selectedTab}
|
||||||
setSelectedTab={setSelectedTab}
|
setSelectedTab={setSelectedTab}
|
||||||
/>
|
/>
|
||||||
|
4
apps/portal/src/types/offers.d.ts
vendored
4
apps/portal/src/types/offers.d.ts
vendored
@ -191,7 +191,7 @@ export type UserProfile = {
|
|||||||
offers: Array<UserProfileOffer>;
|
offers: Array<UserProfileOffer>;
|
||||||
profileName: string;
|
profileName: string;
|
||||||
token: string;
|
token: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UserProfileOffer = {
|
export type UserProfileOffer = {
|
||||||
company: OffersCompany;
|
company: OffersCompany;
|
||||||
@ -202,4 +202,4 @@ export type UserProfileOffer = {
|
|||||||
location: string;
|
location: string;
|
||||||
monthYearReceived: Date;
|
monthYearReceived: Date;
|
||||||
title: string;
|
title: string;
|
||||||
}
|
};
|
||||||
|
Reference in New Issue
Block a user