From e152de22849db5303fb5215163718c58c6114d8f Mon Sep 17 00:00:00 2001
From: Zhang Ziqing <69516975+ziqing26@users.noreply.github.com>
Date: Sun, 30 Oct 2022 23:30:24 +0800
Subject: [PATCH] [offers][feat] save to user profile (#462)
---
.../portal/src/components/global/AppShell.tsx | 10 +-
.../components/offers/OffersNavigation.tsx | 11 ++
.../offers/dashboard/DashboardOfferCard.tsx | 55 +++++++
.../offers/dashboard/DashboardProfileCard.tsx | 105 ++++++++++++
.../offers/profile/ProfileComments.tsx | 49 +++---
.../offers/profile/ProfileHeader.tsx | 129 +++++++++++----
.../src/components/offers/util/Tooltip.tsx | 42 +++++
apps/portal/src/mappers/offers-mappers.ts | 151 ++++++++++--------
apps/portal/src/pages/offers/dashboard.tsx | 95 +++++++++++
.../pages/offers/profile/[offerProfileId].tsx | 1 +
apps/portal/src/types/offers.d.ts | 4 +-
11 files changed, 529 insertions(+), 123 deletions(-)
create mode 100644 apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx
create mode 100644 apps/portal/src/components/offers/dashboard/DashboardProfileCard.tsx
create mode 100644 apps/portal/src/components/offers/util/Tooltip.tsx
create mode 100644 apps/portal/src/pages/offers/dashboard.tsx
diff --git a/apps/portal/src/components/global/AppShell.tsx b/apps/portal/src/components/global/AppShell.tsx
index 41e943d4..3c36e0d9 100644
--- a/apps/portal/src/components/global/AppShell.tsx
+++ b/apps/portal/src/components/global/AppShell.tsx
@@ -9,7 +9,9 @@ import { Bars3BottomLeftIcon } from '@heroicons/react/24/outline';
import GlobalNavigation from '~/components/global/GlobalNavigation';
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 ResumesNavigation from '~/components/resumes/ResumesNavigation';
@@ -105,6 +107,7 @@ function ProfileJewel() {
export default function AppShell({ children }: Props) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const router = useRouter();
+ const { data: session } = useSession();
const currentProductNavigation: Readonly<{
googleAnalyticsMeasurementID: string;
@@ -120,7 +123,10 @@ export default function AppShell({ children }: Props) {
}
if (path.startsWith('/offers')) {
- return OffersNavigation;
+ if (session == null) {
+ return OffersNavigation;
+ }
+ return OffersNavigationAuthenticated;
}
if (path.startsWith('/questions')) {
diff --git a/apps/portal/src/components/offers/OffersNavigation.tsx b/apps/portal/src/components/offers/OffersNavigation.tsx
index acc3f906..4de1fc5e 100644
--- a/apps/portal/src/components/offers/OffersNavigation.tsx
+++ b/apps/portal/src/components/offers/OffersNavigation.tsx
@@ -5,6 +5,12 @@ const navigation: ProductNavigationItems = [
{ 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 = {
// TODO: Change this to your own GA4 measurement ID.
googleAnalyticsMeasurementID: 'G-34XRGLEVCF',
@@ -17,4 +23,9 @@ const config = {
titleHref: '/offers',
};
+export const OffersNavigationAuthenticated = {
+ ...config,
+ navigation: navigationAuthenticated,
+};
+
export default config;
diff --git a/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx b/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx
new file mode 100644
index 00000000..df49ada9
--- /dev/null
+++ b/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx
@@ -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 && }
+
+
+
+ {getLabelForJobTitleType(title as JobTitleType)}
+
+
+ {location
+ ? `Company: ${company.name}, ${location}`
+ : `Company: ${company.name}`}
+
+ {level &&
Level: {level}
}
+
+
+
{formatDate(monthYearReceived)}
+
+ {jobType === JobType.FULLTIME
+ ? `${convertMoneyToString(income)} / year`
+ : `${convertMoneyToString(income)} / month`}
+
+
+
+ >
+ );
+}
diff --git a/apps/portal/src/components/offers/dashboard/DashboardProfileCard.tsx b/apps/portal/src/components/offers/dashboard/DashboardProfileCard.tsx
new file mode 100644
index 00000000..af4a0cfe
--- /dev/null
+++ b/apps/portal/src/components/offers/dashboard/DashboardProfileCard.tsx
@@ -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 (
+
+ {/* Header */}
+
+
+
+
+
{profileName}
+
+
+ Created at {formatDate(createdAt)}
+
+
+
+
+
+
+
+
+
+ {/* Offers */}
+
+ {offers.map((offer: UserProfileOffer, index) =>
+ index === 0 ? (
+
+ ) : (
+
+ ),
+ )}
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/components/offers/profile/ProfileComments.tsx b/apps/portal/src/components/offers/profile/ProfileComments.tsx
index b26f78ae..bc9c3c8e 100644
--- a/apps/portal/src/components/offers/profile/ProfileComments.tsx
+++ b/apps/portal/src/components/offers/profile/ProfileComments.tsx
@@ -10,12 +10,15 @@ import {
} from '@tih/ui';
import ExpandableCommentCard from '~/components/offers/profile/comments/ExpandableCommentCard';
+import Tooltip from '~/components/offers/util/Tooltip';
import { copyProfileLink } from '~/utils/offers/link';
import { trpc } from '~/utils/trpc';
import type { OffersDiscussion, Reply } from '~/types/offers';
+import 'react-popper-tooltip/dist/styles.css';
+
type ProfileHeaderProps = Readonly<{
isDisabled: boolean;
isEditable: boolean;
@@ -107,39 +110,43 @@ export default function ProfileComments({
{isEditable && (
+
+
+ )}
+
Discussions
{isEditable || session?.user?.name ? (
diff --git a/apps/portal/src/components/offers/profile/ProfileHeader.tsx b/apps/portal/src/components/offers/profile/ProfileHeader.tsx
index dc5e8ddb..54c6a9aa 100644
--- a/apps/portal/src/components/offers/profile/ProfileHeader.tsx
+++ b/apps/portal/src/components/offers/profile/ProfileHeader.tsx
@@ -1,27 +1,32 @@
import { useRouter } from 'next/router';
import { useState } from 'react';
import {
+ BookmarkIcon as BookmarkIconOutline,
BuildingOffice2Icon,
CalendarDaysIcon,
PencilSquareIcon,
TrashIcon,
} 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 type { BackgroundDisplayData } from '~/components/offers/types';
import { JobTypeLabel } from '~/components/offers/types';
import { getProfileEditPath } from '~/utils/offers/link';
+import { trpc } from '~/utils/trpc';
import type { ProfileDetailTab } from '../constants';
import { profileDetailTabs } from '../constants';
+import Tooltip from '../util/Tooltip';
type ProfileHeaderProps = Readonly<{
background?: BackgroundDisplayData;
handleDelete: () => void;
isEditable: boolean;
isLoading: boolean;
+ isSaved?: boolean;
selectedTab: ProfileDetailTab;
setSelectedTab: (tab: ProfileDetailTab) => void;
}>;
@@ -31,46 +36,112 @@ export default function ProfileHeader({
handleDelete,
isEditable,
isLoading,
+ isSaved = false,
selectedTab,
setSelectedTab,
}: ProfileHeaderProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
+ // Const [saved, setSaved] = useState(isSaved);
const router = useRouter();
+ const trpcContext = trpc.useContext();
const { offerProfileId = '', token = '' } = router.query;
-
+ const { showToast } = useToast();
const handleEditClick = () => {
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() {
return (
-
- {/*
*/}
-
-
setIsDialogOpen(true)}
- />
+
+
+
+
+
+
+
+
+ setIsDialogOpen(true)}
+ />
+
{isDialogOpen && (