From 87aa16929bf9b539256cea17e560a9d28f96f0be Mon Sep 17 00:00:00 2001 From: Jeff Sieu Date: Wed, 26 Oct 2022 18:53:09 +0800 Subject: [PATCH] [questions][feat] Add question lists (#438) Co-authored-by: wlren --- .../questions/AddToListDropdown.tsx | 160 +++++++++++ .../questions/ContributeQuestionDialog.tsx | 2 +- .../components/questions/CreateListDialog.tsx | 75 +++++ .../components/questions/DeleteListDialog.tsx | 29 ++ .../components/questions/LandingComponent.tsx | 2 +- .../questions/QuestionsNavigation.ts | 4 +- .../card/question/BaseQuestionCard.tsx | 65 ++++- .../card/question/FullQuestionCard.tsx | 6 + .../card/question/QuestionListCard.tsx | 3 + .../card/question/QuestionOverviewCard.tsx | 3 + .../card/question/SimilarQuestionCard.tsx | 11 +- .../forms/ContributeQuestionForm.tsx | 264 ++++++++---------- apps/portal/src/pages/questions/browse.tsx | 83 +++--- apps/portal/src/pages/questions/lists.tsx | 185 +++++++----- apps/portal/src/server/router/index.ts | 2 + ...-list-crud.ts => questions-list-router.ts} | 102 ++++++- .../questions-question-encounter-router.ts | 8 +- .../router/questions-question-router.ts | 134 +-------- .../server/createQuestionWithAggregateData.ts | 92 ++++++ .../src/utils/questions/useSearchParam.ts | 22 +- 20 files changed, 813 insertions(+), 439 deletions(-) create mode 100644 apps/portal/src/components/questions/AddToListDropdown.tsx create mode 100644 apps/portal/src/components/questions/CreateListDialog.tsx create mode 100644 apps/portal/src/components/questions/DeleteListDialog.tsx rename apps/portal/src/server/router/{questions-list-crud.ts => questions-list-router.ts} (59%) create mode 100644 apps/portal/src/utils/questions/server/createQuestionWithAggregateData.ts diff --git a/apps/portal/src/components/questions/AddToListDropdown.tsx b/apps/portal/src/components/questions/AddToListDropdown.tsx new file mode 100644 index 00000000..b68ef77b --- /dev/null +++ b/apps/portal/src/components/questions/AddToListDropdown.tsx @@ -0,0 +1,160 @@ +import clsx from 'clsx'; +import type { PropsWithChildren } from 'react'; +import { useMemo } from 'react'; +import { Fragment, useRef, useState } from 'react'; +import { Menu, Transition } from '@headlessui/react'; +import { CheckIcon, HeartIcon } from '@heroicons/react/20/solid'; + +import { trpc } from '~/utils/trpc'; + +export type AddToListDropdownProps = { + questionId: string; +}; + +export default function AddToListDropdown({ + questionId, +}: AddToListDropdownProps) { + const [menuOpened, setMenuOpened] = useState(false); + const ref = useRef(null); + + const utils = trpc.useContext(); + const { data: lists } = trpc.useQuery(['questions.lists.getListsByUser']); + + const listsWithQuestionData = useMemo(() => { + return lists?.map((list) => ({ + ...list, + hasQuestion: list.questionEntries.some( + (entry) => entry.question.id === questionId, + ), + })); + }, [lists, questionId]); + + const { mutateAsync: addQuestionToList } = trpc.useMutation( + 'questions.lists.createQuestionEntry', + { + // TODO: Add optimistic update + onSuccess: () => { + utils.invalidateQueries(['questions.lists.getListsByUser']); + }, + }, + ); + + const { mutateAsync: removeQuestionFromList } = trpc.useMutation( + 'questions.lists.deleteQuestionEntry', + { + // TODO: Add optimistic update + onSuccess: () => { + utils.invalidateQueries(['questions.lists.getListsByUser']); + }, + }, + ); + + const addClickOutsideListener = () => { + document.addEventListener('click', handleClickOutside, true); + }; + + const handleClickOutside = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + setMenuOpened(false); + document.removeEventListener('click', handleClickOutside, true); + } + }; + + const handleAddToList = async (listId: string) => { + await addQuestionToList({ + listId, + questionId, + }); + }; + + const handleDeleteFromList = async (listId: string) => { + const list = listsWithQuestionData?.find( + (listWithQuestion) => listWithQuestion.id === listId, + ); + if (!list) { + return; + } + const entry = list.questionEntries.find( + (questionEntry) => questionEntry.question.id === questionId, + ); + if (!entry) { + return; + } + await removeQuestionFromList({ + id: entry.id, + }); + }; + + const CustomMenuButton = ({ children }: PropsWithChildren) => ( + + ); + + return ( + +
+ + +
+ + + + {menuOpened && ( + <> + {(listsWithQuestionData ?? []).map((list) => ( +
+ + {({ active }) => ( + + )} + +
+ ))} + + )} +
+
+
+ ); +} diff --git a/apps/portal/src/components/questions/ContributeQuestionDialog.tsx b/apps/portal/src/components/questions/ContributeQuestionDialog.tsx index 9a2d971f..4115a2db 100644 --- a/apps/portal/src/components/questions/ContributeQuestionDialog.tsx +++ b/apps/portal/src/components/questions/ContributeQuestionDialog.tsx @@ -61,7 +61,7 @@ export default function ContributeQuestionDialog({ leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"> -
+
void; + onSubmit: (data: CreateListFormData) => Promise; + show: boolean; +}; + +export default function CreateListDialog({ + show, + onCancel, + onSubmit, +}: CreateListDialogProps) { + const { + register: formRegister, + handleSubmit, + formState: { isSubmitting }, + reset, + } = useForm(); + const register = useFormRegister(formRegister); + + const handleDialogCancel = () => { + onCancel(); + reset(); + }; + + return ( + +
{ + await onSubmit(data); + reset(); + })}> +
+ +
+
+ ); +} diff --git a/apps/portal/src/components/questions/DeleteListDialog.tsx b/apps/portal/src/components/questions/DeleteListDialog.tsx new file mode 100644 index 00000000..3da568d2 --- /dev/null +++ b/apps/portal/src/components/questions/DeleteListDialog.tsx @@ -0,0 +1,29 @@ +import { Button, Dialog } from '@tih/ui'; + +export type DeleteListDialogProps = { + onCancel: () => void; + onDelete: () => void; + show: boolean; +}; +export default function DeleteListDialog({ + show, + onCancel, + onDelete, +}: DeleteListDialogProps) { + return ( + + } + secondaryButton={ + + ); +} diff --git a/apps/portal/src/components/questions/LandingComponent.tsx b/apps/portal/src/components/questions/LandingComponent.tsx index 1007f1e4..cd2ace15 100644 --- a/apps/portal/src/components/questions/LandingComponent.tsx +++ b/apps/portal/src/components/questions/LandingComponent.tsx @@ -118,7 +118,7 @@ export default function LandingComponent({ onClick={() => { if (company !== undefined && location !== undefined) { return handleLandingQuery({ - company: company.value, + company: company.label, location: location.value, questionType, }); diff --git a/apps/portal/src/components/questions/QuestionsNavigation.ts b/apps/portal/src/components/questions/QuestionsNavigation.ts index f46d2388..52fc62e4 100644 --- a/apps/portal/src/components/questions/QuestionsNavigation.ts +++ b/apps/portal/src/components/questions/QuestionsNavigation.ts @@ -3,8 +3,8 @@ import type { ProductNavigationItems } from '~/components/global/ProductNavigati const navigation: ProductNavigationItems = [ { href: '/questions/browse', name: 'Browse' }, { href: '/questions/lists', name: 'My Lists' }, - { href: '/questions/my-questions', name: 'My Questions' }, - { href: '/questions/history', name: 'History' }, + // { href: '/questions/my-questions', name: 'My Questions' }, + // { href: '/questions/history', name: 'History' }, ]; const config = { diff --git a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx index ca9f676e..d4c534e4 100644 --- a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx +++ b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx @@ -11,6 +11,7 @@ import { Button } from '@tih/ui'; import { useQuestionVote } from '~/utils/questions/useVote'; +import AddToListDropdown from '../../AddToListDropdown'; import type { CreateQuestionEncounterData } from '../../forms/CreateQuestionEncounterForm'; import CreateQuestionEncounterForm from '../../forms/CreateQuestionEncounterForm'; import QuestionAggregateBadge from '../../QuestionAggregateBadge'; @@ -47,6 +48,20 @@ type AnswerStatisticsProps = showAnswerStatistics?: false; }; +type AggregateStatisticsProps = + | { + companies: Record; + locations: Record; + roles: Record; + showAggregateStatistics: true; + } + | { + companies?: never; + locations?: never; + roles?: never; + showAggregateStatistics?: false; + }; + type ActionButtonProps = | { actionButtonLabel: string; @@ -79,19 +94,26 @@ type CreateEncounterProps = showCreateEncounterButton?: false; }; +type AddToListProps = + | { + showAddToList: true; + } + | { + showAddToList?: false; + }; + export type BaseQuestionCardProps = ActionButtonProps & + AddToListProps & + AggregateStatisticsProps & AnswerStatisticsProps & CreateEncounterProps & DeleteProps & ReceivedStatisticsProps & UpvoteProps & { - companies: Record; content: string; - locations: Record; questionId: string; - roles: Record; showHover?: boolean; - timestamp: string; + timestamp: string | null; truncateContent?: boolean; type: QuestionsQuestionType; }; @@ -104,6 +126,7 @@ export default function BaseQuestionCard({ receivedCount, type, showVoteButtons, + showAggregateStatistics, showAnswerStatistics, showReceivedStatistics, showCreateEncounterButton, @@ -117,6 +140,7 @@ export default function BaseQuestionCard({ showHover, onReceivedSubmit, showDeleteButton, + showAddToList, onDelete, truncateContent = true, }: BaseQuestionCardProps) { @@ -133,20 +157,35 @@ export default function BaseQuestionCard({ onUpvote={handleUpvote} /> )} -
-
-
- - - - -

{timestamp}

+
+
+
+ {showAggregateStatistics && ( + <> + + + + + + )} + {timestamp !== null &&

{timestamp}

} + {showAddToList && ( +
+ +
+ )}
{showActionButton && (