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 ( -
- {/*
+
+ + ); + } + return ( + <> + {userProfilesQuery.isLoading && ( +
+
+ +
+
+ )} + {!userProfilesQuery.isLoading && ( +
+

+ Your repository +

+

+ Save your offer profiles to respository to easily access and edit + them later. +

+
+
    + {userProfiles?.map((profile) => ( +
  • + +
  • + ))} +
+
+
+ )} + + ); +} diff --git a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx index b5d90466..8c75f77e 100644 --- a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx +++ b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx @@ -188,6 +188,7 @@ export default function OfferProfile() { handleDelete={handleDelete} isEditable={isEditable} isLoading={getProfileQuery.isLoading} + isSaved={getProfileQuery.data?.isSaved} selectedTab={selectedTab} setSelectedTab={setSelectedTab} /> diff --git a/apps/portal/src/types/offers.d.ts b/apps/portal/src/types/offers.d.ts index 5a95406b..4d4c9080 100644 --- a/apps/portal/src/types/offers.d.ts +++ b/apps/portal/src/types/offers.d.ts @@ -191,7 +191,7 @@ export type UserProfile = { offers: Array; profileName: string; token: string; -} +}; export type UserProfileOffer = { company: OffersCompany; @@ -202,4 +202,4 @@ export type UserProfileOffer = { location: string; monthYearReceived: Date; title: string; -} \ No newline at end of file +};