mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-14 18:05:55 +08:00
[offers][feat] integrate isSaved API (#475)
This commit is contained in:
@ -1,3 +1,4 @@
|
|||||||
|
import { signIn, useSession } from 'next-auth/react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { DocumentDuplicateIcon } from '@heroicons/react/20/solid';
|
import { DocumentDuplicateIcon } from '@heroicons/react/20/solid';
|
||||||
import { BookmarkSquareIcon, CheckIcon } from '@heroicons/react/24/outline';
|
import { BookmarkSquareIcon, CheckIcon } from '@heroicons/react/24/outline';
|
||||||
@ -20,6 +21,7 @@ export default function OffersProfileSave({
|
|||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const { event: gaEvent } = useGoogleAnalytics();
|
const { event: gaEvent } = useGoogleAnalytics();
|
||||||
const [isSaved, setSaved] = useState(false);
|
const [isSaved, setSaved] = useState(false);
|
||||||
|
const { data: session, status } = useSession();
|
||||||
|
|
||||||
const saveMutation = trpc.useMutation(
|
const saveMutation = trpc.useMutation(
|
||||||
['offers.user.profile.addToUserProfile'],
|
['offers.user.profile.addToUserProfile'],
|
||||||
@ -32,7 +34,10 @@ export default function OffersProfileSave({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setSaved(true);
|
trpcContext.invalidateQueries([
|
||||||
|
'offers.profile.isSaved',
|
||||||
|
{ profileId, userId: session?.user?.id },
|
||||||
|
]);
|
||||||
showToast({
|
showToast({
|
||||||
title: `Saved to your dashboard!`,
|
title: `Saved to your dashboard!`,
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
@ -41,7 +46,20 @@ export default function OffersProfileSave({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isSavedQuery = trpc.useQuery(
|
||||||
|
[`offers.profile.isSaved`, { profileId, userId: session?.user?.id }],
|
||||||
|
{
|
||||||
|
onSuccess: (res) => {
|
||||||
|
setSaved(res);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const trpcContext = trpc.useContext();
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
if (status === 'unauthenticated') {
|
||||||
|
signIn();
|
||||||
|
} else {
|
||||||
saveMutation.mutate({
|
saveMutation.mutate({
|
||||||
profileId,
|
profileId,
|
||||||
token: token as string,
|
token: token as string,
|
||||||
@ -51,6 +69,7 @@ export default function OffersProfileSave({
|
|||||||
category: 'engagement',
|
category: 'engagement',
|
||||||
label: 'Save to profile in profile submission',
|
label: 'Save to profile in profile submission',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -99,9 +118,9 @@ export default function OffersProfileSave({
|
|||||||
</p>
|
</p>
|
||||||
<div className="mb-20">
|
<div className="mb-20">
|
||||||
<Button
|
<Button
|
||||||
disabled={isSaved}
|
disabled={isSavedQuery.isLoading || isSaved}
|
||||||
icon={isSaved ? CheckIcon : BookmarkSquareIcon}
|
icon={isSaved ? CheckIcon : BookmarkSquareIcon}
|
||||||
isLoading={saveMutation.isLoading}
|
isLoading={saveMutation.isLoading || isSavedQuery.isLoading}
|
||||||
label={isSaved ? 'Saved to user profile' : 'Save to user profile'}
|
label={isSaved ? 'Saved to user profile' : 'Save to user profile'}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import { signIn, useSession } from 'next-auth/react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
BookmarkIcon as BookmarkIconOutline,
|
BookmarkIcon as BookmarkIconOutline,
|
||||||
@ -10,23 +11,22 @@ import {
|
|||||||
import { BookmarkIcon as BookmarkIconSolid } from '@heroicons/react/24/solid';
|
import { BookmarkIcon as BookmarkIconSolid } from '@heroicons/react/24/solid';
|
||||||
import { Button, Dialog, Spinner, Tabs, useToast } from '@tih/ui';
|
import { Button, Dialog, Spinner, Tabs, useToast } from '@tih/ui';
|
||||||
|
|
||||||
|
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||||
|
import type { ProfileDetailTab } from '~/components/offers/constants';
|
||||||
|
import { profileDetailTabs } from '~/components/offers/constants';
|
||||||
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 Tooltip from '~/components/offers/util/Tooltip';
|
||||||
|
|
||||||
import { getProfileEditPath } from '~/utils/offers/link';
|
import { getProfileEditPath } from '~/utils/offers/link';
|
||||||
import { trpc } from '~/utils/trpc';
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
import type { ProfileDetailTab } 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;
|
||||||
}>;
|
}>;
|
||||||
@ -36,20 +36,41 @@ 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 [saved, setSaved] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const trpcContext = trpc.useContext();
|
const trpcContext = trpc.useContext();
|
||||||
|
|
||||||
const { offerProfileId = '', token = '' } = router.query;
|
const { offerProfileId = '', token = '' } = router.query;
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
const { data: session, status } = useSession();
|
||||||
|
const { event: gaEvent } = useGoogleAnalytics();
|
||||||
|
|
||||||
const handleEditClick = () => {
|
const handleEditClick = () => {
|
||||||
|
gaEvent({
|
||||||
|
action: 'offers.edit_profile',
|
||||||
|
category: 'engagement',
|
||||||
|
label: 'Edit profile',
|
||||||
|
});
|
||||||
router.push(getProfileEditPath(offerProfileId as string, token as string));
|
router.push(getProfileEditPath(offerProfileId as string, token as string));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isSavedQuery = trpc.useQuery(
|
||||||
|
[
|
||||||
|
`offers.profile.isSaved`,
|
||||||
|
{ profileId: offerProfileId as string, userId: session?.user?.id },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
onSuccess: (res) => {
|
||||||
|
setSaved(res);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const saveMutation = trpc.useMutation(
|
const saveMutation = trpc.useMutation(
|
||||||
['offers.user.profile.addToUserProfile'],
|
['offers.user.profile.addToUserProfile'],
|
||||||
{
|
{
|
||||||
@ -61,7 +82,13 @@ export default function ProfileHeader({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setSaved(true);
|
trpcContext.invalidateQueries([
|
||||||
|
'offers.profile.isSaved',
|
||||||
|
{
|
||||||
|
profileId: offerProfileId as string,
|
||||||
|
userId: session?.user?.id,
|
||||||
|
},
|
||||||
|
]);
|
||||||
showToast({
|
showToast({
|
||||||
title: `Saved to dashboard!`,
|
title: `Saved to dashboard!`,
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
@ -80,18 +107,25 @@ export default function ProfileHeader({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setSaved(false);
|
trpcContext.invalidateQueries([
|
||||||
|
'offers.profile.isSaved',
|
||||||
|
{
|
||||||
|
profileId: offerProfileId as string,
|
||||||
|
userId: session?.user?.id,
|
||||||
|
},
|
||||||
|
]);
|
||||||
showToast({
|
showToast({
|
||||||
title: `Removed from dashboard!`,
|
title: `Removed from dashboard!`,
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
});
|
});
|
||||||
trpcContext.invalidateQueries(['offers.profile.listOne']);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleSaved = () => {
|
const toggleSaved = () => {
|
||||||
if (saved) {
|
if (status === 'unauthenticated') {
|
||||||
|
signIn();
|
||||||
|
} else if (saved) {
|
||||||
unsaveMutation.mutate({ profileId: offerProfileId as string });
|
unsaveMutation.mutate({ profileId: offerProfileId as string });
|
||||||
} else {
|
} else {
|
||||||
saveMutation.mutate({
|
saveMutation.mutate({
|
||||||
@ -106,15 +140,22 @@ export default function ProfileHeader({
|
|||||||
<div className="flex justify-center space-x-2">
|
<div className="flex justify-center space-x-2">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
isSaved ? 'Remove from account' : 'Save to your account'
|
saved ? 'Remove from account' : 'Save to your account'
|
||||||
}>
|
}>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
isLoading || saveMutation.isLoading || unsaveMutation.isLoading
|
isLoading ||
|
||||||
|
saveMutation.isLoading ||
|
||||||
|
unsaveMutation.isLoading ||
|
||||||
|
isSavedQuery.isLoading
|
||||||
}
|
}
|
||||||
icon={saved ? BookmarkIconSolid : BookmarkIconOutline}
|
icon={saved ? BookmarkIconSolid : BookmarkIconOutline}
|
||||||
isLabelHidden={true}
|
isLabelHidden={true}
|
||||||
isLoading={saveMutation.isLoading || unsaveMutation.isLoading}
|
isLoading={
|
||||||
|
isSavedQuery.isLoading ||
|
||||||
|
saveMutation.isLoading ||
|
||||||
|
unsaveMutation.isLoading
|
||||||
|
}
|
||||||
label={saved ? 'Remove from account' : 'Save to your account'}
|
label={saved ? 'Remove from account' : 'Save to your account'}
|
||||||
size="md"
|
size="md"
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
|
@ -4,6 +4,7 @@ import { useSession } from 'next-auth/react';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Spinner, useToast } from '@tih/ui';
|
import { Spinner, useToast } from '@tih/ui';
|
||||||
|
|
||||||
|
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||||
import { ProfileDetailTab } from '~/components/offers/constants';
|
import { ProfileDetailTab } from '~/components/offers/constants';
|
||||||
import ProfileComments from '~/components/offers/profile/ProfileComments';
|
import ProfileComments from '~/components/offers/profile/ProfileComments';
|
||||||
import ProfileDetails from '~/components/offers/profile/ProfileDetails';
|
import ProfileDetails from '~/components/offers/profile/ProfileDetails';
|
||||||
@ -36,6 +37,7 @@ export default function OfferProfile() {
|
|||||||
);
|
);
|
||||||
const [analysis, setAnalysis] = useState<ProfileAnalysis>();
|
const [analysis, setAnalysis] = useState<ProfileAnalysis>();
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
const { event: gaEvent } = useGoogleAnalytics();
|
||||||
|
|
||||||
const getProfileQuery = trpc.useQuery(
|
const getProfileQuery = trpc.useQuery(
|
||||||
[
|
[
|
||||||
@ -176,6 +178,11 @@ export default function OfferProfile() {
|
|||||||
profileId: offerProfileId as string,
|
profileId: offerProfileId as string,
|
||||||
token: token as string,
|
token: token as string,
|
||||||
});
|
});
|
||||||
|
gaEvent({
|
||||||
|
action: 'offers.delete_profile',
|
||||||
|
category: 'engagement',
|
||||||
|
label: 'Delete profile',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,7 +209,6 @@ 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}
|
||||||
/>
|
/>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import type { OffersProfile } from '@prisma/client';
|
||||||
import { JobType } from '@prisma/client';
|
import { JobType } from '@prisma/client';
|
||||||
import * as trpc from '@trpc/server';
|
import * as trpc from '@trpc/server';
|
||||||
|
|
||||||
@ -108,14 +109,15 @@ export const offersProfileRouter = createRouter()
|
|||||||
token: z.string(),
|
token: z.string(),
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
const profile = await ctx.prisma.offersProfile.findFirst({
|
const profile: OffersProfile | null =
|
||||||
|
await ctx.prisma.offersProfile.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: input.profileId
|
id: input.profileId,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return profile?.editToken === input.token
|
return profile?.editToken === input.token;
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.query('isSaved', {
|
.query('isSaved', {
|
||||||
input: z.object({
|
input: z.object({
|
||||||
@ -123,36 +125,35 @@ export const offersProfileRouter = createRouter()
|
|||||||
userId: z.string().nullish(),
|
userId: z.string().nullish(),
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
|
|
||||||
if (!input.userId) {
|
if (!input.userId) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = await ctx.prisma.offersProfile.findFirst({
|
const profile = await ctx.prisma.offersProfile.findFirst({
|
||||||
include: {
|
include: {
|
||||||
users: true
|
users: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
id: input.profileId
|
id: input.profileId,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const users = profile?.users
|
const users = profile?.users;
|
||||||
|
|
||||||
if (!users) {
|
if (!users) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let isSaved = false
|
let isSaved = false;
|
||||||
|
|
||||||
for (let i = 0; i < users.length; i++) {
|
for (let i = 0; i < users.length; i++) {
|
||||||
if (users[i].id === input.userId) {
|
if (users[i].id === input.userId) {
|
||||||
isSaved = true
|
isSaved = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return isSaved
|
return isSaved;
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.query('listOne', {
|
.query('listOne', {
|
||||||
input: z.object({
|
input: z.object({
|
||||||
|
Reference in New Issue
Block a user