From 9741bf83b9dda30c1165475875640140cf945262 Mon Sep 17 00:00:00 2001 From: Wu Peirong Date: Thu, 20 Oct 2022 17:54:36 +0800 Subject: [PATCH 01/23] [resumes][refactor] add staleTime to browse page queries --- apps/portal/src/pages/resumes/browse.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/portal/src/pages/resumes/browse.tsx b/apps/portal/src/pages/resumes/browse.tsx index e8adaf64..3968d8a5 100644 --- a/apps/portal/src/pages/resumes/browse.tsx +++ b/apps/portal/src/pages/resumes/browse.tsx @@ -99,6 +99,7 @@ export default function ResumeHomePage() { setResumes(data.mappedResumeData); setRenderSignInButton(false); }, + staleTime: 5 * 60 * 1000, }, ); const starredResumesQuery = trpc.useQuery( @@ -129,6 +130,7 @@ export default function ResumeHomePage() { setResumes(data.mappedResumeData); }, retry: false, + staleTime: 5 * 60 * 1000, }, ); const myResumesQuery = trpc.useQuery( @@ -159,6 +161,7 @@ export default function ResumeHomePage() { setResumes(data.mappedResumeData); }, retry: false, + staleTime: 5 * 60 * 1000, }, ); From a5c300c9b24cc9778c7332878f85b84546bbc02f Mon Sep 17 00:00:00 2001 From: BryannYeap Date: Thu, 20 Oct 2022 18:12:30 +0800 Subject: [PATCH 02/23] [offers][fix] Align the range of Junior, Mid, and Senior SWE in the backend with the frontend --- apps/portal/src/server/router/offers/offers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts index 539085fd..d75ed270 100644 --- a/apps/portal/src/server/router/offers/offers.ts +++ b/apps/portal/src/server/router/offers/offers.ts @@ -17,11 +17,11 @@ const yoeCategoryMap: Record = { const getYoeRange = (yoeCategory: number) => { return yoeCategoryMap[yoeCategory] === 'Fresh Grad' - ? { maxYoe: 3, minYoe: 0 } + ? { maxYoe: 2, minYoe: 0 } : yoeCategoryMap[yoeCategory] === 'Mid' - ? { maxYoe: 7, minYoe: 4 } + ? { maxYoe: 5, minYoe: 3 } : yoeCategoryMap[yoeCategory] === 'Senior' - ? { maxYoe: 100, minYoe: 8 } + ? { maxYoe: 100, minYoe: 6 } : null; // Internship }; From 41d51702252a6d736d09532858239b9532e1cae0 Mon Sep 17 00:00:00 2001 From: Keane Chan Date: Thu, 20 Oct 2022 18:25:26 +0800 Subject: [PATCH 03/23] [resumes][feat] scaffold for resume badges (#399) * [resumes][fix] reduce font size in comments * [resumes][feat] add queries for resume badges * [resumes][feat] add scaffold for resume badges * [resumes][chore] remove unused query --- .../reviewer/BronzeReviewerBadgeIcon.tsx | 7 ++- .../reviewer/GoldReviewerBadgeIcon.tsx | 7 ++- .../reviewer/SilverReviewerBadgeIcon.tsx | 7 ++- .../resumes/badges/ResumeUserBadge.tsx | 31 ++++++++++ .../resumes/badges/ResumeUserBadges.tsx | 36 ++++++++++++ .../resumes/badges/resumeBadgeConstants.ts | 57 +++++++++++++++++++ .../comments/ResumeCommentListItem.tsx | 11 ++-- .../resumes/comments/ResumeCommentsList.tsx | 6 +- .../router/resumes/resumes-comments-router.ts | 1 - .../router/resumes/resumes-resume-router.ts | 16 ++++++ 10 files changed, 168 insertions(+), 11 deletions(-) create mode 100644 apps/portal/src/components/resumes/badges/ResumeUserBadge.tsx create mode 100644 apps/portal/src/components/resumes/badges/ResumeUserBadges.tsx create mode 100644 apps/portal/src/components/resumes/badges/resumeBadgeConstants.ts diff --git a/apps/portal/src/components/resumes/badgeIcons/reviewer/BronzeReviewerBadgeIcon.tsx b/apps/portal/src/components/resumes/badgeIcons/reviewer/BronzeReviewerBadgeIcon.tsx index 16871299..bd5ae146 100644 --- a/apps/portal/src/components/resumes/badgeIcons/reviewer/BronzeReviewerBadgeIcon.tsx +++ b/apps/portal/src/components/resumes/badgeIcons/reviewer/BronzeReviewerBadgeIcon.tsx @@ -1,7 +1,12 @@ -export default function BronzeReviewerBadgeIcon() { +type Props = Readonly<{ + className: string; +}>; + +export default function BronzeReviewerBadgeIcon({ className }: Props) { return (
+
+ +

{toolTip}

+

{description}

+
+ +
+ ); +} diff --git a/apps/portal/src/components/resumes/badges/ResumeUserBadges.tsx b/apps/portal/src/components/resumes/badges/ResumeUserBadges.tsx new file mode 100644 index 00000000..8c772d8f --- /dev/null +++ b/apps/portal/src/components/resumes/badges/ResumeUserBadges.tsx @@ -0,0 +1,36 @@ +import { trpc } from '~/utils/trpc'; + +import type { BadgePayload } from './resumeBadgeConstants'; +import { RESUME_USER_BADGES } from './resumeBadgeConstants'; +import ResumeUserBadge from './ResumeUserBadge'; + +type Props = Readonly<{ + userId: string; +}>; + +export default function ResumeUserBadges({ userId }: Props) { + const userReviewedResumesCountQuery = trpc.useQuery([ + 'resumes.resume.findUserReviewedResumeCount', + { userId }, + ]); + + // TODO: Add other badges in + const payload: BadgePayload = { + reviewedResumesCount: userReviewedResumesCountQuery.data ?? 0, + }; + + return ( + <> + {RESUME_USER_BADGES.filter((badge) => badge.isValid(payload)).map( + (badge) => ( + + ), + )} + + ); +} diff --git a/apps/portal/src/components/resumes/badges/resumeBadgeConstants.ts b/apps/portal/src/components/resumes/badges/resumeBadgeConstants.ts new file mode 100644 index 00000000..92086204 --- /dev/null +++ b/apps/portal/src/components/resumes/badges/resumeBadgeConstants.ts @@ -0,0 +1,57 @@ +import BronzeReviewerBadgeIcon from '../badgeIcons/reviewer/BronzeReviewerBadgeIcon'; +import GoldReviewerBadgeIcon from '../badgeIcons/reviewer/GoldReviewerBadgeIcon'; +import SilverReviewerBadgeIcon from '../badgeIcons/reviewer/SilverReviewerBadgeIcon'; + +export type BadgeIcon = ( + props: React.ComponentProps< + | typeof BronzeReviewerBadgeIcon + | typeof GoldReviewerBadgeIcon + | typeof SilverReviewerBadgeIcon + >, +) => JSX.Element; + +export type BadgeInfo = { + description: string; + icon: BadgeIcon; + id: string; + isValid: (payload: BadgePayload) => boolean; + toolTip: string; +}; + +// TODO: Add other badges in +export type BadgePayload = { + reviewedResumesCount: number; +}; + +const GOLD_TIER = 20; +const SILVER_TIER = 10; +const BRONZE_TIER = 5; + +export const RESUME_USER_BADGES: Array = [ + { + description: `User has reviewed over ${GOLD_TIER} resumes`, + icon: GoldReviewerBadgeIcon, + id: 'Superhero', + isValid: (payload: BadgePayload) => + payload.reviewedResumesCount >= GOLD_TIER, + toolTip: 'True saviour of the people', + }, + { + description: `User has reviewed over ${SILVER_TIER} resumes`, + icon: SilverReviewerBadgeIcon, + id: 'Detective', + isValid: (payload: BadgePayload) => + payload.reviewedResumesCount >= SILVER_TIER && + payload.reviewedResumesCount < GOLD_TIER, + toolTip: 'Keen eye for details like a private eye', + }, + { + description: `User has reviewed over ${BRONZE_TIER} resumes`, + icon: BronzeReviewerBadgeIcon, + id: 'Eagle', + isValid: (payload: BadgePayload) => + payload.reviewedResumesCount >= BRONZE_TIER && + payload.reviewedResumesCount < SILVER_TIER, + toolTip: 'As sharp as an eagle', + }, +]; diff --git a/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx b/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx index 28f6a224..833c53c2 100644 --- a/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx +++ b/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx @@ -13,6 +13,7 @@ import { Button, TextArea } from '@tih/ui'; import { trpc } from '~/utils/trpc'; +import ResumeUserBadges from '../badges/ResumeUserBadges'; import ResumeExpandableText from '../shared/ResumeExpandableText'; import type { ResumeComment } from '~/types/resume-comments'; @@ -152,13 +153,15 @@ export default function ResumeCommentListItem({ {/* Name and creation time */}
-
+

{comment.user.name ?? 'Reviewer ABC'} -

+

-
+

{isCommentOwner ? '(Me)' : ''} -

+

+ +
diff --git a/apps/portal/src/components/resumes/comments/ResumeCommentsList.tsx b/apps/portal/src/components/resumes/comments/ResumeCommentsList.tsx index 23f8ad02..677678e6 100644 --- a/apps/portal/src/components/resumes/comments/ResumeCommentsList.tsx +++ b/apps/portal/src/components/resumes/comments/ResumeCommentsList.tsx @@ -33,7 +33,7 @@ export default function ResumeCommentsList({ const commentsQuery = trpc.useQuery(['resumes.comments.list', { resumeId }]); const renderIcon = (section: ResumesSection) => { - const className = 'h-8 w-8'; + const className = 'h-7 w-7'; switch (section) { case ResumesSection.GENERAL: return ; @@ -83,11 +83,11 @@ export default function ResumeCommentsList({ const commentCount = comments.length; return ( -
+
{renderIcon(value)} -
{label}
+
{label}
{commentCount > 0 ? ( diff --git a/apps/portal/src/server/router/resumes/resumes-comments-router.ts b/apps/portal/src/server/router/resumes/resumes-comments-router.ts index 33d6256a..096e8323 100644 --- a/apps/portal/src/server/router/resumes/resumes-comments-router.ts +++ b/apps/portal/src/server/router/resumes/resumes-comments-router.ts @@ -13,7 +13,6 @@ export const resumeCommentsRouter = createRouter().query('list', { // For this resume, we retrieve every comment's information, along with: // The user's name and image to render - // Number of votes, and whether the user (if-any) has voted const comments = await ctx.prisma.resumesComment.findMany({ include: { user: { diff --git a/apps/portal/src/server/router/resumes/resumes-resume-router.ts b/apps/portal/src/server/router/resumes/resumes-resume-router.ts index 00c9f13b..00f20b20 100644 --- a/apps/portal/src/server/router/resumes/resumes-resume-router.ts +++ b/apps/portal/src/server/router/resumes/resumes-resume-router.ts @@ -138,4 +138,20 @@ export const resumesRouter = createRouter() }, }); }, + }) + .query('findUserReviewedResumeCount', { + input: z.object({ + userId: z.string(), + }), + async resolve({ ctx, input }) { + return await ctx.prisma.resumesResume.count({ + where: { + comments: { + some: { + userId: input.userId, + }, + }, + }, + }); + }, }); From 283333e1ee197c470473046989203a46d70211d9 Mon Sep 17 00:00:00 2001 From: Wu Peirong Date: Thu, 20 Oct 2022 19:26:26 +0800 Subject: [PATCH 04/23] [resumes][fix] browse tabs updates on tab shift --- .../resumes/browse/ResumeListItem.tsx | 4 +- .../resumes/comments/ResumeCommentsForm.tsx | 3 + apps/portal/src/pages/resumes/[resumeId].tsx | 9 +++ apps/portal/src/pages/resumes/browse.tsx | 62 +++++++++---------- 4 files changed, 44 insertions(+), 34 deletions(-) diff --git a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx index 10689062..b0ef8b4d 100644 --- a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx +++ b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx @@ -41,7 +41,9 @@ export default function ResumeListItem({ href, resumeInfo }: Props) {
- {resumeInfo.numComments} comments + {`${resumeInfo.numComments} comment${ + resumeInfo.numComments > 0 ? 's' : '' + }`}
{resumeInfo.isStarredByUser ? ( diff --git a/apps/portal/src/components/resumes/comments/ResumeCommentsForm.tsx b/apps/portal/src/components/resumes/comments/ResumeCommentsForm.tsx index 31bade2f..584f88d0 100644 --- a/apps/portal/src/components/resumes/comments/ResumeCommentsForm.tsx +++ b/apps/portal/src/components/resumes/comments/ResumeCommentsForm.tsx @@ -47,6 +47,9 @@ export default function ResumeCommentsForm({ onSuccess: () => { // New Comment added, invalidate query to trigger refetch trpcContext.invalidateQueries(['resumes.comments.list']); + trpcContext.invalidateQueries(['resumes.resume.findAll']); + trpcContext.invalidateQueries(['resumes.resume.user.findUserStarred']); + trpcContext.invalidateQueries(['resumes.resume.user.findUserCreated']); }, }, ); diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx index 002be9b6..4c66576e 100644 --- a/apps/portal/src/pages/resumes/[resumeId].tsx +++ b/apps/portal/src/pages/resumes/[resumeId].tsx @@ -42,11 +42,17 @@ export default function ResumeReviewPage() { const starMutation = trpc.useMutation('resumes.resume.star', { onSuccess() { utils.invalidateQueries(['resumes.resume.findOne']); + utils.invalidateQueries(['resumes.resume.findAll']); + utils.invalidateQueries(['resumes.resume.user.findUserStarred']); + utils.invalidateQueries(['resumes.resume.user.findUserCreated']); }, }); const unstarMutation = trpc.useMutation('resumes.resume.unstar', { onSuccess() { utils.invalidateQueries(['resumes.resume.findOne']); + utils.invalidateQueries(['resumes.resume.findAll']); + utils.invalidateQueries(['resumes.resume.user.findUserStarred']); + utils.invalidateQueries(['resumes.resume.user.findUserCreated']); }, }); const userIsOwner = @@ -89,6 +95,9 @@ export default function ResumeReviewPage() { }} onClose={() => { utils.invalidateQueries(['resumes.resume.findOne']); + utils.invalidateQueries(['resumes.resume.findAll']); + utils.invalidateQueries(['resumes.resume.user.findUserStarred']); + utils.invalidateQueries(['resumes.resume.user.findUserCreated']); setIsEditMode(false); }} /> diff --git a/apps/portal/src/pages/resumes/browse.tsx b/apps/portal/src/pages/resumes/browse.tsx index 3968d8a5..81948432 100644 --- a/apps/portal/src/pages/resumes/browse.tsx +++ b/apps/portal/src/pages/resumes/browse.tsx @@ -35,8 +35,6 @@ import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton'; import { trpc } from '~/utils/trpc'; -import type { Resume } from '~/types/resume'; - const filters: Array = [ { id: 'role', @@ -63,11 +61,9 @@ export default function ResumeHomePage() { const [searchValue, setSearchValue] = useState(''); const [userFilters, setUserFilters] = useState(INITIAL_FILTER_STATE); const [shortcutSelected, setShortcutSelected] = useState('All'); - const [resumes, setResumes] = useState>([]); const [renderSignInButton, setRenderSignInButton] = useState(false); const [signInButtonText, setSignInButtonText] = useState(''); const [currentPage, setCurrentPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); const PAGE_LIMIT = 10; const skip = (currentPage - 1) * PAGE_LIMIT; @@ -90,13 +86,7 @@ export default function ResumeHomePage() { ], { enabled: tabsValue === BROWSE_TABS_VALUES.ALL, - onSuccess: (data) => { - setTotalPages( - data.totalRecords % PAGE_LIMIT === 0 - ? data.totalRecords / PAGE_LIMIT - : Math.floor(data.totalRecords / PAGE_LIMIT) + 1, - ); - setResumes(data.mappedResumeData); + onSuccess: () => { setRenderSignInButton(false); }, staleTime: 5 * 60 * 1000, @@ -117,18 +107,9 @@ export default function ResumeHomePage() { { enabled: tabsValue === BROWSE_TABS_VALUES.STARRED, onError: () => { - setResumes([]); setRenderSignInButton(true); setSignInButtonText('to view starred resumes'); }, - onSuccess: (data) => { - setTotalPages( - data.totalRecords % PAGE_LIMIT === 0 - ? data.totalRecords / PAGE_LIMIT - : Math.floor(data.totalRecords / PAGE_LIMIT) + 1, - ); - setResumes(data.mappedResumeData); - }, retry: false, staleTime: 5 * 60 * 1000, }, @@ -148,18 +129,9 @@ export default function ResumeHomePage() { { enabled: tabsValue === BROWSE_TABS_VALUES.MY, onError: () => { - setResumes([]); setRenderSignInButton(true); setSignInButtonText('to view your submitted resumes'); }, - onSuccess: (data) => { - setTotalPages( - data.totalRecords % PAGE_LIMIT === 0 - ? data.totalRecords / PAGE_LIMIT - : Math.floor(data.totalRecords / PAGE_LIMIT) + 1, - ); - setResumes(data.mappedResumeData); - }, retry: false, staleTime: 5 * 60 * 1000, }, @@ -208,6 +180,30 @@ export default function ResumeHomePage() { setCurrentPage(1); }; + const getTabQueryData = () => { + switch (tabsValue) { + case BROWSE_TABS_VALUES.ALL: + return allResumesQuery.data; + case BROWSE_TABS_VALUES.STARRED: + return starredResumesQuery.data; + case BROWSE_TABS_VALUES.MY: + return myResumesQuery.data; + default: + return null; + } + }; + + const getTabResumes = () => { + return getTabQueryData()?.mappedResumeData ?? []; + }; + + const getTabTotalPages = () => { + const numRecords = getTabQueryData()?.totalRecords ?? 0; + return numRecords % PAGE_LIMIT === 0 + ? numRecords / PAGE_LIMIT + : Math.floor(numRecords / PAGE_LIMIT) + 1; + }; + return ( <> @@ -277,7 +273,7 @@ export default function ResumeHomePage() {