From 27095e37470a040b9154200206eeb25aa37b70b2 Mon Sep 17 00:00:00 2001 From: Wu Peirong Date: Mon, 31 Oct 2022 12:16:29 +0800 Subject: [PATCH 01/19] [resumes][chore] add GA for resumes review page --- .../resumes/browse/ResumeListItem.tsx | 13 +++++++++- apps/portal/src/pages/resumes/index.tsx | 24 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx index 59ed277d..a9d1731c 100644 --- a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx +++ b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx @@ -10,6 +10,8 @@ import { } from '@heroicons/react/20/solid'; import { ChatBubbleLeftIcon, StarIcon } from '@heroicons/react/24/outline'; +import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; + import type { ExperienceFilter, LocationFilter, @@ -30,8 +32,17 @@ type Props = Readonly<{ }>; export default function ResumeListItem({ href, resumeInfo }: Props) { + const { event: gaEvent } = useGoogleAnalytics(); return ( - + + gaEvent({ + action: 'resumes.listitem_click', + category: 'engagement', + label: 'Select Resume', + }) + }>
diff --git a/apps/portal/src/pages/resumes/index.tsx b/apps/portal/src/pages/resumes/index.tsx index db14977f..9a0ca902 100644 --- a/apps/portal/src/pages/resumes/index.tsx +++ b/apps/portal/src/pages/resumes/index.tsx @@ -20,6 +20,7 @@ import { TextInput, } from '@tih/ui'; +import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill'; import ResumeListItems from '~/components/resumes/browse/ResumeListItems'; import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton'; @@ -135,6 +136,7 @@ export default function ResumeHomePage() { role: false, }); const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false); + const { event: gaEvent } = useGoogleAnalytics(); const skip = (currentPage - 1) * PAGE_LIMIT; const isSearchOptionsInit = useMemo(() => { @@ -279,6 +281,11 @@ export default function ResumeHomePage() { ), }); } + gaEvent({ + action: 'resumes.filter_checkbox_click', + category: 'engagement', + label: 'Select Filter', + }); }; const onClearFilterClick = (filterSection: FilterId) => { @@ -296,11 +303,21 @@ export default function ResumeHomePage() { setShortcutSelected(shortcutName); setSortOrder(shortcutSortOrder); setUserFilters(shortcutFilters); + gaEvent({ + action: 'resumes.shortcut_button_click', + category: 'engagement', + label: `Select Shortcut: ${shortcutName}`, + }); }; const onTabChange = (tab: string) => { setTabsValue(tab); setCurrentPage(1); + gaEvent({ + action: 'resumes.tab_click', + category: 'engagement', + label: `Select Tab: ${tab}`, + }); }; const getTabQueryData = () => { @@ -631,6 +648,13 @@ export default function ResumeHomePage() { type="text" value={searchValue} onChange={setSearchValue} + onFocus={() => + gaEvent({ + action: 'resumes.search_input_focus', + category: 'engagement', + label: 'Click Search', + }) + } />
Date: Mon, 31 Oct 2022 12:56:45 +0800 Subject: [PATCH 02/19] [questions][feat] add encounters sorting (#458) Co-authored-by: Bryann Yeap Kok Keong Co-authored-by: Ai Ling Co-authored-by: Zhang Ziqing Co-authored-by: Bryann Yeap Kok Keong <77266823+BryannYeap@users.noreply.github.com> --- .../migration.sql | 5 ++ apps/portal/prisma/schema.prisma | 18 ++++--- ...uestions-question-encounter-user-router.ts | 37 +++++++------ .../questions/questions-question-router.ts | 52 ++++++++++++------- apps/portal/src/types/questions.d.ts | 1 + 5 files changed, 72 insertions(+), 41 deletions(-) create mode 100644 apps/portal/prisma/migrations/20221029180052_add_ecnounter_sort_support/migration.sql diff --git a/apps/portal/prisma/migrations/20221029180052_add_ecnounter_sort_support/migration.sql b/apps/portal/prisma/migrations/20221029180052_add_ecnounter_sort_support/migration.sql new file mode 100644 index 00000000..6744fc0c --- /dev/null +++ b/apps/portal/prisma/migrations/20221029180052_add_ecnounter_sort_support/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "QuestionsQuestion" ADD COLUMN "numEncounters" INTEGER NOT NULL DEFAULT 0; + +-- CreateIndex +CREATE INDEX "QuestionsQuestion_numEncounters_id_idx" ON "QuestionsQuestion"("numEncounters", "id"); diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index 3a9277f0..b2820c3c 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -438,14 +438,15 @@ enum QuestionsQuestionType { } model QuestionsQuestion { - id String @id @default(cuid()) - userId String? - content String @db.Text - questionType QuestionsQuestionType - lastSeenAt DateTime? - upvotes Int @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + userId String? + content String @db.Text + questionType QuestionsQuestionType + lastSeenAt DateTime? + upvotes Int @default(0) + numEncounters Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt user User? @relation(fields: [userId], references: [id], onDelete: SetNull) encounters QuestionsQuestionEncounter[] @@ -455,6 +456,7 @@ model QuestionsQuestion { questionsListQuestionEntries QuestionsListQuestionEntry[] @@index([lastSeenAt, id]) + @@index([numEncounters, id]) @@index([upvotes, id]) } diff --git a/apps/portal/src/server/router/questions/questions-question-encounter-user-router.ts b/apps/portal/src/server/router/questions/questions-question-encounter-user-router.ts index 54a34cfa..6a78dc14 100644 --- a/apps/portal/src/server/router/questions/questions-question-encounter-user-router.ts +++ b/apps/portal/src/server/router/questions/questions-question-encounter-user-router.ts @@ -41,19 +41,21 @@ export const questionsQuestionEncounterUserRouter = createProtectedRouter() }); } - if ( - questionToUpdate.lastSeenAt === null || - questionToUpdate.lastSeenAt < input.seenAt - ) { - await tx.questionsQuestion.update({ - data: { - lastSeenAt: input.seenAt, - }, - where: { - id: input.questionId, + + await tx.questionsQuestion.update({ + data: { + lastSeenAt: (questionToUpdate.lastSeenAt === null || + questionToUpdate.lastSeenAt < input.seenAt) + ? input.seenAt : undefined, + numEncounters: { + increment: 1, }, - }); - } + }, + where: { + id: input.questionId, + }, + }); + return questionEncounterCreated; }); }, @@ -160,6 +162,8 @@ export const questionsQuestionEncounterUserRouter = createProtectedRouter() }), ]); + let lastSeenVal = undefined; + if (questionToUpdate!.lastSeenAt === questionEncounterToDelete.seenAt) { const latestEncounter = await ctx.prisma.questionsQuestionEncounter.findFirst({ @@ -171,17 +175,20 @@ export const questionsQuestionEncounterUserRouter = createProtectedRouter() }, }); - const lastSeenVal = latestEncounter ? latestEncounter!.seenAt : null; + lastSeenVal = latestEncounter ? latestEncounter!.seenAt : null; + } - await tx.questionsQuestion.update({ + await tx.questionsQuestion.update({ data: { lastSeenAt: lastSeenVal, + numEncounters: { + increment: -1, + }, }, where: { id: questionToUpdate!.id, }, }); - } return questionEncounterDeleted; }); diff --git a/apps/portal/src/server/router/questions/questions-question-router.ts b/apps/portal/src/server/router/questions/questions-question-router.ts index 0a2d58a4..a5c06f2d 100644 --- a/apps/portal/src/server/router/questions/questions-question-router.ts +++ b/apps/portal/src/server/router/questions/questions-question-router.ts @@ -27,24 +27,40 @@ export const questionsQuestionRouter = createRouter() async resolve({ ctx, input }) { const { cursor } = input; - const sortCondition = - input.sortType === SortType.TOP - ? [ - { - upvotes: input.sortOrder, - }, - { - id: input.sortOrder, - }, - ] - : [ - { - lastSeenAt: input.sortOrder, - }, - { - id: input.sortOrder, - }, - ]; + let sortCondition = undefined; + + switch (input.sortType) { + case SortType.TOP: + sortCondition = [ + { + upvotes: input.sortOrder, + }, + { + id: input.sortOrder, + }, + ] + break; + case SortType.NEW: + sortCondition = [ + { + lastSeenAt: input.sortOrder, + }, + { + id: input.sortOrder, + }, + ]; + break; + case SortType.ENCOUNTERS: + sortCondition = [ + { + numEncounters: input.sortOrder, + }, + { + id: input.sortOrder, + }, + ]; + break; + } const questionsData = await ctx.prisma.questionsQuestion.findMany({ cursor: cursor ? { id: cursor } : undefined, diff --git a/apps/portal/src/types/questions.d.ts b/apps/portal/src/types/questions.d.ts index 8391e6e1..0a4a9339 100644 --- a/apps/portal/src/types/questions.d.ts +++ b/apps/portal/src/types/questions.d.ts @@ -88,4 +88,5 @@ export enum SortOrder { export enum SortType { TOP, NEW, + ENCOUNTERS, } From ade6d1d88d13a566ec536ed94509812fc22ef49b Mon Sep 17 00:00:00 2001 From: Ai Ling <50992674+ailing35@users.noreply.github.com> Date: Mon, 31 Oct 2022 14:16:09 +0800 Subject: [PATCH 03/19] [offers][fix] Fix save failure message (#474) --- .../components/offers/offersSubmission/OffersProfileSave.tsx | 5 ++--- apps/portal/src/components/offers/profile/ProfileHeader.tsx | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx b/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx index efc0db79..a2f9f089 100644 --- a/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx +++ b/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx @@ -1,5 +1,3 @@ -// Import { useState } from 'react'; -// import { setTimeout } from 'timers'; import { useState } from 'react'; import { DocumentDuplicateIcon } from '@heroicons/react/20/solid'; import { BookmarkSquareIcon, CheckIcon } from '@heroicons/react/24/outline'; @@ -28,11 +26,13 @@ export default function OffersProfileSave({ { onError: () => { showToast({ + subtitle: 'Please check that you are logged in.', title: `Failed to saved to dashboard!`, variant: 'failure', }); }, onSuccess: () => { + setSaved(true); showToast({ title: `Saved to your dashboard!`, variant: 'success', @@ -46,7 +46,6 @@ export default function OffersProfileSave({ profileId, token: token as string, }); - setSaved(true); gaEvent({ action: 'offers.profile_submission_save_to_profile', category: 'engagement', diff --git a/apps/portal/src/components/offers/profile/ProfileHeader.tsx b/apps/portal/src/components/offers/profile/ProfileHeader.tsx index ec2da0ff..f258d0b7 100644 --- a/apps/portal/src/components/offers/profile/ProfileHeader.tsx +++ b/apps/portal/src/components/offers/profile/ProfileHeader.tsx @@ -55,6 +55,7 @@ export default function ProfileHeader({ { onError: () => { showToast({ + subtitle: 'Please check that you are logged in.', title: `Failed to saved to dashboard!`, variant: 'failure', }); From 538fa5ccf7f8ca63a24df5064e5b811e13c60cef Mon Sep 17 00:00:00 2001 From: Jeff Sieu Date: Mon, 31 Oct 2022 14:31:56 +0800 Subject: [PATCH 04/19] [questions][feat] add useProtectedCallback hook (#472) --- .../questions/AddToListDropdown.tsx | 11 +++-- .../questions/ContributeQuestionCard.tsx | 6 ++- .../components/questions/VotingButtons.tsx | 15 ++++++- .../card/question/BaseQuestionCard.tsx | 10 +++-- .../protected/ProtectedContextProvider.tsx | 40 +++++++++++++++++++ .../questions/protected/ProtectedDialog.tsx | 36 +++++++++++++++++ apps/portal/src/pages/_app.tsx | 9 +++-- .../answer/[answerId]/[answerSlug]/index.tsx | 17 ++++---- .../[questionId]/[questionSlug]/index.tsx | 38 ++++++++++-------- apps/portal/src/pages/questions/lists.tsx | 19 ++++++--- .../utils/questions/useProtectedCallback.ts | 22 ++++++++++ 11 files changed, 179 insertions(+), 44 deletions(-) create mode 100644 apps/portal/src/components/questions/protected/ProtectedContextProvider.tsx create mode 100644 apps/portal/src/components/questions/protected/ProtectedDialog.tsx create mode 100644 apps/portal/src/utils/questions/useProtectedCallback.ts diff --git a/apps/portal/src/components/questions/AddToListDropdown.tsx b/apps/portal/src/components/questions/AddToListDropdown.tsx index b68ef77b..4ae9da46 100644 --- a/apps/portal/src/components/questions/AddToListDropdown.tsx +++ b/apps/portal/src/components/questions/AddToListDropdown.tsx @@ -5,6 +5,7 @@ import { Fragment, useRef, useState } from 'react'; import { Menu, Transition } from '@headlessui/react'; import { CheckIcon, HeartIcon } from '@heroicons/react/20/solid'; +import { useProtectedCallback } from '~/utils/questions/useProtectedCallback'; import { trpc } from '~/utils/trpc'; export type AddToListDropdownProps = { @@ -85,14 +86,16 @@ export default function AddToListDropdown({ }); }; + const handleMenuButtonClick = useProtectedCallback(() => { + addClickOutsideListener(); + setMenuOpened(!menuOpened); + }); + const CustomMenuButton = ({ children }: PropsWithChildren) => ( ); diff --git a/apps/portal/src/components/questions/ContributeQuestionCard.tsx b/apps/portal/src/components/questions/ContributeQuestionCard.tsx index f4256e8d..9313896a 100644 --- a/apps/portal/src/components/questions/ContributeQuestionCard.tsx +++ b/apps/portal/src/components/questions/ContributeQuestionCard.tsx @@ -6,6 +6,8 @@ import { } from '@heroicons/react/24/outline'; import { TextInput } from '@tih/ui'; +import { useProtectedCallback } from '~/utils/questions/useProtectedCallback'; + import ContributeQuestionDialog from './ContributeQuestionDialog'; import type { ContributeQuestionFormProps } from './forms/ContributeQuestionForm'; @@ -23,9 +25,9 @@ export default function ContributeQuestionCard({ setShowDraftDialog(false); }; - const handleOpenContribute = () => { + const handleOpenContribute = useProtectedCallback(() => { setShowDraftDialog(true); - }; + }); return (
diff --git a/apps/portal/src/components/questions/VotingButtons.tsx b/apps/portal/src/components/questions/VotingButtons.tsx index cd404dca..28824715 100644 --- a/apps/portal/src/components/questions/VotingButtons.tsx +++ b/apps/portal/src/components/questions/VotingButtons.tsx @@ -4,6 +4,8 @@ import type { Vote } from '@prisma/client'; import type { ButtonSize } from '@tih/ui'; import { Button } from '@tih/ui'; +import { useProtectedCallback } from '~/utils/questions/useProtectedCallback'; + export type BackendVote = { id: string; vote: Vote; @@ -31,6 +33,15 @@ export default function VotingButtons({ vote?.vote === 'UPVOTE' ? 'secondary' : 'tertiary'; const downvoteButtonVariant = vote?.vote === 'DOWNVOTE' ? 'secondary' : 'tertiary'; + + const handleUpvoteClick = useProtectedCallback(() => { + onUpvote(); + }); + + const handleDownvoteClick = useProtectedCallback(() => { + onDownvote(); + }); + return (
diff --git a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx index b183b012..0c8f1460 100644 --- a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx +++ b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx @@ -9,6 +9,7 @@ import { import type { QuestionsQuestionType } from '@prisma/client'; import { Button } from '@tih/ui'; +import { useProtectedCallback } from '~/utils/questions/useProtectedCallback'; import { useQuestionVote } from '~/utils/questions/useVote'; import AddToListDropdown from '../../AddToListDropdown'; @@ -168,6 +169,10 @@ export default function BaseQuestionCard({ return countryCount; }, [countries]); + const handleCreateEncounterClick = useProtectedCallback(() => { + setShowReceivedForm(true); + }); + const cardContent = ( <> {showVoteButtons && ( @@ -244,10 +249,7 @@ export default function BaseQuestionCard({ label={createEncounterButtonText} size="sm" variant="tertiary" - onClick={(event) => { - event.preventDefault(); - setShowReceivedForm(true); - }} + onClick={handleCreateEncounterClick} /> )}
diff --git a/apps/portal/src/components/questions/protected/ProtectedContextProvider.tsx b/apps/portal/src/components/questions/protected/ProtectedContextProvider.tsx new file mode 100644 index 00000000..8dee8681 --- /dev/null +++ b/apps/portal/src/components/questions/protected/ProtectedContextProvider.tsx @@ -0,0 +1,40 @@ +import type { PropsWithChildren } from 'react'; +import { createContext, useState } from 'react'; + +import ProtectedDialog from './ProtectedDialog'; + +export type ProtectedContextData = { + showDialog: () => void; +}; + +export const ProtectedContext = createContext({ + // eslint-disable-next-line @typescript-eslint/no-empty-function + showDialog: () => {}, +}); + +export type ProtectedContextProviderProps = PropsWithChildren< + Record +>; + +export default function ProtectedContextProvider({ + children, +}: ProtectedContextProviderProps) { + const [show, setShow] = useState(false); + + return ( + { + setShow(true); + }, + }}> + {children} + { + setShow(false); + }} + /> + + ); +} diff --git a/apps/portal/src/components/questions/protected/ProtectedDialog.tsx b/apps/portal/src/components/questions/protected/ProtectedDialog.tsx new file mode 100644 index 00000000..98e5ed62 --- /dev/null +++ b/apps/portal/src/components/questions/protected/ProtectedDialog.tsx @@ -0,0 +1,36 @@ +import { signIn } from 'next-auth/react'; +import { Button, Dialog } from '@tih/ui'; + +export type ProtectedDialogProps = { + onClose: () => void; + show: boolean; +}; + +export default function ProtectedDialog({ + show, + onClose, +}: ProtectedDialogProps) { + const handlePrimaryClick = () => { + signIn(); + onClose(); + }; + + return ( + + } + secondaryButton={ + + ); +} diff --git a/apps/portal/src/pages/_app.tsx b/apps/portal/src/pages/_app.tsx index 9914942e..565a48f6 100644 --- a/apps/portal/src/pages/_app.tsx +++ b/apps/portal/src/pages/_app.tsx @@ -9,6 +9,7 @@ import { loggerLink } from '@trpc/client/links/loggerLink'; import { withTRPC } from '@trpc/next'; import AppShell from '~/components/global/AppShell'; +import ProtectedContextProvider from '~/components/questions/protected/ProtectedContextProvider'; import type { AppRouter } from '~/server/router'; @@ -21,9 +22,11 @@ const MyApp: AppType<{ session: Session | null }> = ({ return ( - - - + + + + + ); diff --git a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/answer/[answerId]/[answerSlug]/index.tsx b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/answer/[answerId]/[answerSlug]/index.tsx index 5481240a..73fd6065 100644 --- a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/answer/[answerId]/[answerSlug]/index.tsx +++ b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/answer/[answerId]/[answerSlug]/index.tsx @@ -13,6 +13,7 @@ import SortOptionsSelect from '~/components/questions/SortOptionsSelect'; import { APP_TITLE } from '~/utils/questions/constants'; import { useFormRegister } from '~/utils/questions/useFormRegister'; +import { useProtectedCallback } from '~/utils/questions/useProtectedCallback'; import { trpc } from '~/utils/trpc'; import { SortOrder, SortType } from '~/types/questions.d'; @@ -82,13 +83,15 @@ export default function QuestionPage() { }, ); - const handleSubmitComment = (data: AnswerCommentData) => { - resetComment(); - addComment({ - answerId: answerId as string, - content: data.commentContent, - }); - }; + const handleSubmitComment = useProtectedCallback( + (data: AnswerCommentData) => { + resetComment(); + addComment({ + answerId: answerId as string, + content: data.commentContent, + }); + }, + ); if (!answer) { return ; diff --git a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx index 2de0521c..c383a550 100644 --- a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx +++ b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx @@ -16,6 +16,7 @@ import { APP_TITLE } from '~/utils/questions/constants'; import createSlug from '~/utils/questions/createSlug'; import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates'; import { useFormRegister } from '~/utils/questions/useFormRegister'; +import { useProtectedCallback } from '~/utils/questions/useProtectedCallback'; import { trpc } from '~/utils/trpc'; import { SortOrder, SortType } from '~/types/questions.d'; @@ -53,10 +54,11 @@ export default function QuestionPage() { const { register: comRegister, - handleSubmit: handleCommentSubmit, + handleSubmit: handleCommentSubmitClick, reset: resetComment, formState: { isDirty: isCommentDirty, isValid: isCommentValid }, } = useForm({ mode: 'onChange' }); + const commentRegister = useFormRegister(comRegister); const { questionId } = router.query; @@ -149,21 +151,25 @@ export default function QuestionPage() { }, ); - const handleSubmitAnswer = (data: AnswerQuestionData) => { - addAnswer({ - content: data.answerContent, - questionId: questionId as string, - }); - resetAnswer(); - }; + const handleSubmitAnswer = useProtectedCallback( + (data: AnswerQuestionData) => { + addAnswer({ + content: data.answerContent, + questionId: questionId as string, + }); + resetAnswer(); + }, + ); - const handleSubmitComment = (data: QuestionCommentData) => { - addComment({ - content: data.commentContent, - questionId: questionId as string, - }); - resetComment(); - }; + const handleSubmitComment = useProtectedCallback( + (data: QuestionCommentData) => { + addComment({ + content: data.commentContent, + questionId: questionId as string, + }); + resetComment(); + }, + ); if (!question) { return ; @@ -219,7 +225,7 @@ export default function QuestionPage() {
+ onSubmit={handleCommentSubmitClick(handleSubmitComment)}>