From 199fc1a8b999524ecca1c69a74ff9527c910420d Mon Sep 17 00:00:00 2001 From: Peirong <35712975+peironggg@users.noreply.github.com> Date: Tue, 25 Oct 2022 12:08:07 +0800 Subject: [PATCH 01/41] [resumes][feat] url search params (#429) * [resumes][feat] adapt useSearchParams * [resumes][feat] clickable button from review info tags --- apps/portal/src/pages/resumes/[resumeId].tsx | 92 +++++++++++++++- apps/portal/src/pages/resumes/browse.tsx | 100 ++++++++++++++---- apps/portal/src/pages/resumes/submit.tsx | 6 +- .../browse => utils/resumes}/resumeFilters.ts | 14 +-- .../src/utils/resumes/useSearchParams.ts | 26 +++++ 5 files changed, 202 insertions(+), 36 deletions(-) rename apps/portal/src/{components/resumes/browse => utils/resumes}/resumeFilters.ts (93%) create mode 100644 apps/portal/src/utils/resumes/useSearchParams.ts diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx index 38c7cf31..0d3ec979 100644 --- a/apps/portal/src/pages/resumes/[resumeId].tsx +++ b/apps/portal/src/pages/resumes/[resumeId].tsx @@ -21,9 +21,25 @@ import ResumeCommentsList from '~/components/resumes/comments/ResumeCommentsList import ResumePdf from '~/components/resumes/ResumePdf'; import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText'; +import type { + FilterOption, + LocationFilter, +} from '~/utils/resumes/resumeFilters'; +import { + BROWSE_TABS_VALUES, + EXPERIENCES, + INITIAL_FILTER_STATE, + LOCATIONS, + ROLES, + SORT_OPTIONS, +} from '~/utils/resumes/resumeFilters'; import { trpc } from '~/utils/trpc'; import SubmitResumeForm from './submit'; +import type { + ExperienceFilter, + RoleFilter, +} from '../../utils/resumes/resumeFilters'; export default function ResumeReviewPage() { const ErrorPage = ( @@ -57,7 +73,8 @@ export default function ResumeReviewPage() { }, }); const userIsOwner = - session?.user?.id != null && session.user.id === detailsQuery.data?.userId; + session?.user?.id !== undefined && + session.user.id === detailsQuery.data?.userId; const [isEditMode, setIsEditMode] = useState(false); const [showCommentsForm, setShowCommentsForm] = useState(false); @@ -79,6 +96,46 @@ export default function ResumeReviewPage() { } }; + const onInfoTagClick = ({ + locationLabel, + experienceLabel, + roleLabel, + }: { + experienceLabel?: string; + locationLabel?: string; + roleLabel?: string; + }) => { + const getFilterValue = ( + label: string, + filterOptions: Array< + FilterOption + >, + ) => filterOptions.find((option) => option.label === label)?.value; + + router.push({ + pathname: '/resumes/browse', + query: { + currentPage: JSON.stringify(1), + searchValue: JSON.stringify(''), + shortcutSelected: JSON.stringify('all'), + sortOrder: JSON.stringify(SORT_OPTIONS.LATEST), + tabsValue: JSON.stringify(BROWSE_TABS_VALUES.ALL), + userFilters: JSON.stringify({ + ...INITIAL_FILTER_STATE, + ...(locationLabel && { + location: [getFilterValue(locationLabel, LOCATIONS)], + }), + ...(roleLabel && { + role: [getFilterValue(roleLabel, ROLES)], + }), + ...(experienceLabel && { + experience: [getFilterValue(experienceLabel, EXPERIENCES)], + }), + }), + }, + }); + }; + const onEditButtonClick = () => { setIsEditMode(true); }; @@ -199,21 +256,48 @@ export default function ResumeReviewPage() { aria-hidden="true" className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400" /> - {detailsQuery.data.role} +
{ + return ( + isTabsValueInit && + isSortOrderInit && + isSearchValueInit && + isShortcutInit && + isCurrentPageInit && + isUserFiltersInit + ); + }, [ + isTabsValueInit, + isSortOrderInit, + isSearchValueInit, + isShortcutInit, + isCurrentPageInit, + isUserFiltersInit, + ]); useEffect(() => { setCurrentPage(1); - }, [userFilters, sortOrder, searchValue]); + }, [userFilters, sortOrder, setCurrentPage, searchValue]); + + useEffect(() => { + // Router.replace used instead of router.replace to avoid + // the page reloading itself since the router.replace + // callback changes on every page load + if (!isSearchOptionsInit) { + return; + } + + Router.replace({ + pathname: router.pathname, + query: { + currentPage: JSON.stringify(currentPage), + searchValue: JSON.stringify(searchValue), + shortcutSelected: JSON.stringify(shortcutSelected), + sortOrder: JSON.stringify(sortOrder), + tabsValue: JSON.stringify(tabsValue), + userFilters: JSON.stringify(userFilters), + }, + }); + }, [ + tabsValue, + sortOrder, + searchValue, + userFilters, + shortcutSelected, + currentPage, + router.pathname, + isSearchOptionsInit, + ]); const allResumesQuery = trpc.useQuery( [ @@ -509,7 +569,7 @@ export default function ResumeHomePage() { key={key} isSelected={sortOrder === key} label={value} - onClick={() => setSortOrder(key)}> + onClick={() => setSortOrder(value)}> ))}
diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx index 7360c9de..182a32bd 100644 --- a/apps/portal/src/pages/resumes/submit.tsx +++ b/apps/portal/src/pages/resumes/submit.tsx @@ -19,14 +19,10 @@ import { TextInput, } from '@tih/ui'; -import { - EXPERIENCES, - LOCATIONS, - ROLES, -} from '~/components/resumes/browse/resumeFilters'; import SubmissionGuidelines from '~/components/resumes/submit-form/SubmissionGuidelines'; import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys'; +import { EXPERIENCES, LOCATIONS, ROLES } from '~/utils/resumes/resumeFilters'; import { trpc } from '~/utils/trpc'; const FILE_SIZE_LIMIT_MB = 3; diff --git a/apps/portal/src/components/resumes/browse/resumeFilters.ts b/apps/portal/src/utils/resumes/resumeFilters.ts similarity index 93% rename from apps/portal/src/components/resumes/browse/resumeFilters.ts rename to apps/portal/src/utils/resumes/resumeFilters.ts index e0c4b0b5..1731647f 100644 --- a/apps/portal/src/components/resumes/browse/resumeFilters.ts +++ b/apps/portal/src/utils/resumes/resumeFilters.ts @@ -4,7 +4,7 @@ export type CustomFilter = { numComments: number; }; -type RoleFilter = +export type RoleFilter = | 'Android Engineer' | 'Backend Engineer' | 'DevOps Engineer' @@ -12,7 +12,7 @@ type RoleFilter = | 'Full-Stack Engineer' | 'iOS Engineer'; -type ExperienceFilter = +export type ExperienceFilter = | 'Entry Level (0 - 2 years)' | 'Freshman' | 'Junior' @@ -21,7 +21,7 @@ type ExperienceFilter = | 'Senior' | 'Sophomore'; -type LocationFilter = 'India' | 'Singapore' | 'United States'; +export type LocationFilter = 'India' | 'Singapore' | 'United States'; export type FilterValue = ExperienceFilter | LocationFilter | RoleFilter; @@ -54,10 +54,10 @@ export const BROWSE_TABS_VALUES = { STARRED: 'starred', }; -export const SORT_OPTIONS: Record = { - latest: 'Latest', - popular: 'Popular', - topComments: 'Most Comments', +export const SORT_OPTIONS: Record = { + LATEST: 'latest', + POPULAR: 'popular', + TOPCOMMENTS: 'topComments', }; export const ROLES: Array> = [ diff --git a/apps/portal/src/utils/resumes/useSearchParams.ts b/apps/portal/src/utils/resumes/useSearchParams.ts new file mode 100644 index 00000000..0bea502c --- /dev/null +++ b/apps/portal/src/utils/resumes/useSearchParams.ts @@ -0,0 +1,26 @@ +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; + +export const useSearchParams = (name: string, defaultValue: T) => { + const [isInitialized, setIsInitialized] = useState(false); + const router = useRouter(); + + const [filters, setFilters] = useState(defaultValue); + + useEffect(() => { + if (router.isReady && !isInitialized) { + // Initialize from url query params + const query = router.query[name]; + if (query) { + const parsedQuery = + typeof query === 'string' ? JSON.parse(query) : query; + setFilters(parsedQuery); + } + setIsInitialized(true); + } + }, [isInitialized, name, router]); + + return [filters, setFilters, isInitialized] as const; +}; + +export default useSearchParams; From 05119f52fa9a8c036ccad0783a5fb6aa9221de3e Mon Sep 17 00:00:00 2001 From: Wu Peirong Date: Tue, 25 Oct 2022 18:46:17 +0800 Subject: [PATCH 02/41] [resumes][fix] browse ui and sort order labels --- apps/portal/src/pages/resumes/[resumeId].tsx | 3 +-- apps/portal/src/pages/resumes/browse.tsx | 23 +++++++++++-------- .../portal/src/utils/resumes/resumeFilters.ts | 16 +++++++++---- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx index 0d3ec979..beb8c793 100644 --- a/apps/portal/src/pages/resumes/[resumeId].tsx +++ b/apps/portal/src/pages/resumes/[resumeId].tsx @@ -31,7 +31,6 @@ import { INITIAL_FILTER_STATE, LOCATIONS, ROLES, - SORT_OPTIONS, } from '~/utils/resumes/resumeFilters'; import { trpc } from '~/utils/trpc'; @@ -118,7 +117,7 @@ export default function ResumeReviewPage() { currentPage: JSON.stringify(1), searchValue: JSON.stringify(''), shortcutSelected: JSON.stringify('all'), - sortOrder: JSON.stringify(SORT_OPTIONS.LATEST), + sortOrder: JSON.stringify('latest'), tabsValue: JSON.stringify(BROWSE_TABS_VALUES.ALL), userFilters: JSON.stringify({ ...INITIAL_FILTER_STATE, diff --git a/apps/portal/src/pages/resumes/browse.tsx b/apps/portal/src/pages/resumes/browse.tsx index c48fb6ed..86e06a78 100644 --- a/apps/portal/src/pages/resumes/browse.tsx +++ b/apps/portal/src/pages/resumes/browse.tsx @@ -38,7 +38,7 @@ import useDebounceValue from '~/utils/resumes/useDebounceValue'; import useSearchParams from '~/utils/resumes/useSearchParams'; import { trpc } from '~/utils/trpc'; -import type { FilterState } from '../../utils/resumes/resumeFilters'; +import type { FilterState, SortOrder } from '../../utils/resumes/resumeFilters'; const STALE_TIME = 5 * 60 * 1000; const DEBOUNCE_DELAY = 800; @@ -102,9 +102,9 @@ export default function ResumeHomePage() { 'tabsValue', BROWSE_TABS_VALUES.ALL, ); - const [sortOrder, setSortOrder, isSortOrderInit] = useSearchParams( + const [sortOrder, setSortOrder, isSortOrderInit] = useSearchParams( 'sortOrder', - SORT_OPTIONS.LATEST, + 'latest', ); const [searchValue, setSearchValue, isSearchValueInit] = useSearchParams( 'searchValue', @@ -491,7 +491,7 @@ export default function ResumeHomePage() { {filter.options.map((option) => (
+ className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 px-1 [&>div>div:nth-child(2)>label]:font-normal">
- - {Object.entries(SORT_OPTIONS).map(([key, value]) => ( + value === sortOrder) + ?.label + }> + {SORT_OPTIONS.map(({ label, value }) => ( setSortOrder(value)}> ))} diff --git a/apps/portal/src/utils/resumes/resumeFilters.ts b/apps/portal/src/utils/resumes/resumeFilters.ts index 1731647f..3799e533 100644 --- a/apps/portal/src/utils/resumes/resumeFilters.ts +++ b/apps/portal/src/utils/resumes/resumeFilters.ts @@ -54,11 +54,17 @@ export const BROWSE_TABS_VALUES = { STARRED: 'starred', }; -export const SORT_OPTIONS: Record = { - LATEST: 'latest', - POPULAR: 'popular', - TOPCOMMENTS: 'topComments', -}; +// Export const SORT_OPTIONS: Record = { +// LATEST: 'latest', +// POPULAR: 'popular', +// TOPCOMMENTS: 'topComments', +// }; + +export const SORT_OPTIONS: Array> = [ + { label: 'Latest', value: 'latest' }, + { label: 'Popular', value: 'popular' }, + { label: 'Top Comments', value: 'topComments' }, +]; export const ROLES: Array> = [ { From fa5cf0c115b0a972c7c0a6c9099ea43252d7cc3e Mon Sep 17 00:00:00 2001 From: Su Yin <53945359+tnsyn@users.noreply.github.com> Date: Wed, 26 Oct 2022 13:18:51 +0800 Subject: [PATCH 03/41] [resumes][fix] Fix profile popup issue (#432) --- apps/portal/src/pages/resumes/browse.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/portal/src/pages/resumes/browse.tsx b/apps/portal/src/pages/resumes/browse.tsx index 86e06a78..f4ed682e 100644 --- a/apps/portal/src/pages/resumes/browse.tsx +++ b/apps/portal/src/pages/resumes/browse.tsx @@ -517,7 +517,7 @@ export default function ResumeHomePage() {
-
+
Date: Wed, 26 Oct 2022 15:38:28 +0800 Subject: [PATCH 04/41] [questions][feat] add encounters transaction for crud (#409) * [questions][chore] refactor question queries * [questions][chore] destructure values from input * [questions][feat] add sorting * [question][fix] fix frontend * [questions][feat] add sorting * [questions][feat] add sorting index * [questions][chore] push migration file * [questions][fix] fix ci issues * [questions][fix] fix import errors * [questions][feat] add encounters transaction for crud * [questions][chore] fix import * [questions][chore] update error handling * [questions][feat] parallelize queries * [questions][fix] update to use corrcet client * Update questions-question-encounter-router.ts * Update questions-question-encounter-router.ts Co-authored-by: Jeff Sieu --- .../migration.sql | 2 + .../migration.sql | 8 ++ .../migrations/20221025014050_/migration.sql | 8 ++ apps/portal/prisma/schema.prisma | 5 +- .../questions-question-encounter-router.ts | 130 +++++++++++++++--- 5 files changed, 135 insertions(+), 18 deletions(-) create mode 100644 apps/portal/prisma/migrations/20221023100925_update_last_seen_val_to_be_optional/migration.sql create mode 100644 apps/portal/prisma/migrations/20221025013857_add_full_text_search/migration.sql create mode 100644 apps/portal/prisma/migrations/20221025014050_/migration.sql diff --git a/apps/portal/prisma/migrations/20221023100925_update_last_seen_val_to_be_optional/migration.sql b/apps/portal/prisma/migrations/20221023100925_update_last_seen_val_to_be_optional/migration.sql new file mode 100644 index 00000000..e4c76fc9 --- /dev/null +++ b/apps/portal/prisma/migrations/20221023100925_update_last_seen_val_to_be_optional/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "QuestionsQuestion" ALTER COLUMN "lastSeenAt" DROP NOT NULL; diff --git a/apps/portal/prisma/migrations/20221025013857_add_full_text_search/migration.sql b/apps/portal/prisma/migrations/20221025013857_add_full_text_search/migration.sql new file mode 100644 index 00000000..8f58d7f9 --- /dev/null +++ b/apps/portal/prisma/migrations/20221025013857_add_full_text_search/migration.sql @@ -0,0 +1,8 @@ +-- AlterTable +ALTER TABLE "QuestionsQuestion" ADD COLUMN "contentSearch" TSVECTOR + GENERATED ALWAYS AS + (to_tsvector('english', coalesce(content, ''))) + STORED; + +-- CreateIndex +CREATE INDEX "QuestionsQuestion_contentSearch_idx" ON "QuestionsQuestion" USING GIN("contentSearch"); diff --git a/apps/portal/prisma/migrations/20221025014050_/migration.sql b/apps/portal/prisma/migrations/20221025014050_/migration.sql new file mode 100644 index 00000000..6d3a3407 --- /dev/null +++ b/apps/portal/prisma/migrations/20221025014050_/migration.sql @@ -0,0 +1,8 @@ +-- DropIndex +DROP INDEX "QuestionsQuestion_contentSearch_idx"; + +-- AlterTable +ALTER TABLE "QuestionsQuestion" ALTER COLUMN "contentSearch" DROP DEFAULT; + +-- CreateIndex +CREATE INDEX "QuestionsQuestion_contentSearch_idx" ON "QuestionsQuestion"("contentSearch"); diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index c3902fe0..bf57a55d 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -1,7 +1,8 @@ // Refer to the Prisma schema docs: https://pris.ly/d/prisma-schema generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" + previewFeatures = ["interactiveTransactions"] } datasource db { @@ -402,7 +403,7 @@ model QuestionsQuestion { userId String? content String @db.Text questionType QuestionsQuestionType - lastSeenAt DateTime + lastSeenAt DateTime? upvotes Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/apps/portal/src/server/router/questions-question-encounter-router.ts b/apps/portal/src/server/router/questions-question-encounter-router.ts index 8fa4a0e4..6198d866 100644 --- a/apps/portal/src/server/router/questions-question-encounter-router.ts +++ b/apps/portal/src/server/router/questions-question-encounter-router.ts @@ -4,6 +4,8 @@ import { TRPCError } from '@trpc/server'; import { createProtectedRouter } from './context'; import type { AggregatedQuestionEncounter } from '~/types/questions'; +import { SortOrder } from '~/types/questions.d'; + export const questionsQuestionEncounterRouter = createProtectedRouter() .query('getAggregatedEncounters', { @@ -68,11 +70,40 @@ export const questionsQuestionEncounterRouter = createProtectedRouter() async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - return await ctx.prisma.questionsQuestionEncounter.create({ - data: { - ...input, - userId, - }, + return await ctx.prisma.$transaction(async (tx) => { + const [questionToUpdate, questionEncounterCreated] = await Promise.all([ + tx.questionsQuestion.findUnique({ + where: { + id: input.questionId, + }, + }), + tx.questionsQuestionEncounter.create({ + data: { + ...input, + userId, + }, + }) + ]); + + + if (questionToUpdate === null) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Question does not exist', + }); + } + + if (!questionToUpdate.lastSeenAt || questionToUpdate.lastSeenAt < input.seenAt) { + await tx.questionsQuestion.update({ + data: { + lastSeenAt : input.seenAt, + }, + where: { + id: input.questionId, + }, + }); + } + return questionEncounterCreated; }); }, }) @@ -101,14 +132,48 @@ export const questionsQuestionEncounterRouter = createProtectedRouter() }); } - return await ctx.prisma.questionsQuestionEncounter.update({ - data: { - ...input, - }, - where: { - id: input.id, - }, + return await ctx.prisma.$transaction(async (tx) => { + const [questionToUpdate, questionEncounterUpdated] = await Promise.all([ + tx.questionsQuestion.findUnique({ + where: { + id: questionEncounterToUpdate.questionId, + }, + }), + tx.questionsQuestionEncounter.update({ + data: { + ...input, + }, + where: { + id: input.id, + }, + }) + ]); + + + if (questionToUpdate!.lastSeenAt === questionEncounterToUpdate.seenAt) { + const latestEncounter = await ctx.prisma.questionsQuestionEncounter.findFirst({ + orderBy: { + seenAt: SortOrder.DESC, + }, + where: { + questionId: questionToUpdate!.id, + }, + }); + + await tx.questionsQuestion.update({ + data: { + lastSeenAt : latestEncounter!.seenAt, + }, + where: { + id: questionToUpdate!.id, + }, + }); + } + + + return questionEncounterUpdated; }); + }, }) .mutation('delete', { @@ -132,10 +197,43 @@ export const questionsQuestionEncounterRouter = createProtectedRouter() }); } - return await ctx.prisma.questionsQuestionEncounter.delete({ - where: { - id: input.id, - }, + return await ctx.prisma.$transaction(async (tx) => { + const [questionToUpdate, questionEncounterDeleted] = await Promise.all([ + tx.questionsQuestion.findUnique({ + where: { + id: questionEncounterToDelete.questionId, + }, + }), + tx.questionsQuestionEncounter.delete({ + where: { + id: input.id, + }, + }) + ]); + + if (questionToUpdate!.lastSeenAt === questionEncounterToDelete.seenAt) { + const latestEncounter = await ctx.prisma.questionsQuestionEncounter.findFirst({ + orderBy: { + seenAt: SortOrder.DESC, + }, + where: { + questionId: questionToUpdate!.id, + }, + }); + + const lastSeenVal = latestEncounter ? latestEncounter!.seenAt : null; + + await tx.questionsQuestion.update({ + data: { + lastSeenAt : lastSeenVal, + }, + where: { + id: questionToUpdate!.id, + }, + }); + } + + return questionEncounterDeleted; }); }, }); From 0ba2815fbdc81d1fe480010e9027975da5c7db5c Mon Sep 17 00:00:00 2001 From: Keane Chan Date: Wed, 26 Oct 2022 16:23:14 +0800 Subject: [PATCH 05/41] [portal] change top nav bar (#433) --- apps/portal/src/components/global/ProductNavigation.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/portal/src/components/global/ProductNavigation.tsx b/apps/portal/src/components/global/ProductNavigation.tsx index 74e6dada..0b172559 100644 --- a/apps/portal/src/components/global/ProductNavigation.tsx +++ b/apps/portal/src/components/global/ProductNavigation.tsx @@ -26,7 +26,9 @@ export default function ProductNavigation({ items, title, titleHref }: Props) { return (
-
+
& Record>; -export type SortOrder = 'latest' | 'popular' | 'topComments'; +export type SortOrder = 'latest' | 'mostComments' | 'popular'; export type Shortcut = { customFilters?: CustomFilter; @@ -63,7 +63,7 @@ export const BROWSE_TABS_VALUES = { export const SORT_OPTIONS: Array> = [ { label: 'Latest', value: 'latest' }, { label: 'Popular', value: 'popular' }, - { label: 'Top Comments', value: 'topComments' }, + { label: 'Most Comments', value: 'mostComments' }, ]; export const ROLES: Array> = [ From e277cda7809effd3e94c434df45c8e3f2c34f4a9 Mon Sep 17 00:00:00 2001 From: Keane Chan Date: Wed, 26 Oct 2022 17:45:20 +0800 Subject: [PATCH 11/41] [resumes][feat] add app icon --- apps/portal/src/components/global/ProductNavigation.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/portal/src/components/global/ProductNavigation.tsx b/apps/portal/src/components/global/ProductNavigation.tsx index 5f162a51..d5ec2218 100644 --- a/apps/portal/src/components/global/ProductNavigation.tsx +++ b/apps/portal/src/components/global/ProductNavigation.tsx @@ -27,8 +27,11 @@ export default function ProductNavigation({ items, title, titleHref }: Props) { return (