[questions][feat] add similar questions check (#468)

Co-authored-by: wlren <weilinwork99@gmail.com>
pull/470/head
Jeff Sieu 2 years ago committed by GitHub
parent 568b674322
commit 8fdea6b5bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,6 @@
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from '@headlessui/react';
import { HorizontalDivider } from '@tih/ui'; import { HorizontalDivider, useToast } from '@tih/ui';
import DiscardDraftDialog from './DiscardDraftDialog'; import DiscardDraftDialog from './DiscardDraftDialog';
import type { ContributeQuestionFormProps } from './forms/ContributeQuestionForm'; import type { ContributeQuestionFormProps } from './forms/ContributeQuestionForm';
@ -21,6 +21,8 @@ export default function ContributeQuestionDialog({
}: ContributeQuestionDialogProps) { }: ContributeQuestionDialogProps) {
const [showDiscardDialog, setShowDiscardDialog] = useState(false); const [showDiscardDialog, setShowDiscardDialog] = useState(false);
const { showToast } = useToast();
const handleDraftDiscard = () => { const handleDraftDiscard = () => {
setShowDiscardDialog(false); setShowDiscardDialog(false);
onCancel(); onCancel();
@ -75,6 +77,14 @@ export default function ContributeQuestionDialog({
<div className="mt-2"> <div className="mt-2">
<ContributeQuestionForm <ContributeQuestionForm
onDiscard={() => setShowDiscardDialog(true)} onDiscard={() => setShowDiscardDialog(true)}
onSimilarQuestionFound={() => {
onCancel();
showToast({
title:
'Your response has been recorded. Draft discarded.',
variant: 'success',
});
}}
onSubmit={(data) => { onSubmit={(data) => {
onSubmit(data); onSubmit(data);
onCancel(); onCancel();

@ -88,10 +88,12 @@ type ReceivedStatisticsProps =
type CreateEncounterProps = type CreateEncounterProps =
| { | {
createEncounterButtonText: string;
onReceivedSubmit: (data: CreateQuestionEncounterData) => void; onReceivedSubmit: (data: CreateQuestionEncounterData) => void;
showCreateEncounterButton: true; showCreateEncounterButton: true;
} }
| { | {
createEncounterButtonText?: never;
onReceivedSubmit?: never; onReceivedSubmit?: never;
showCreateEncounterButton?: false; showCreateEncounterButton?: false;
}; };
@ -132,6 +134,7 @@ export default function BaseQuestionCard({
showAnswerStatistics, showAnswerStatistics,
showReceivedStatistics, showReceivedStatistics,
showCreateEncounterButton, showCreateEncounterButton,
createEncounterButtonText,
showActionButton, showActionButton,
actionButtonLabel, actionButtonLabel,
onActionButtonClick, onActionButtonClick,
@ -238,7 +241,7 @@ export default function BaseQuestionCard({
<Button <Button
addonPosition="start" addonPosition="start"
icon={CheckIcon} icon={CheckIcon}
label="I received this too" label={createEncounterButtonText}
size="sm" size="sm"
variant="tertiary" variant="tertiary"
onClick={(event) => { onClick={(event) => {

@ -3,10 +3,10 @@ import BaseQuestionCard from './BaseQuestionCard';
export type SimilarQuestionCardProps = Omit< export type SimilarQuestionCardProps = Omit<
BaseQuestionCardProps & { BaseQuestionCardProps & {
showActionButton: true; showActionButton: false;
showAggregateStatistics: false; showAggregateStatistics: true;
showAnswerStatistics: false; showAnswerStatistics: false;
showCreateEncounterButton: false; showCreateEncounterButton: true;
showDeleteButton: false; showDeleteButton: false;
showHover: true; showHover: true;
showReceivedStatistics: false; showReceivedStatistics: false;
@ -22,26 +22,20 @@ export type SimilarQuestionCardProps = Omit<
| 'showHover' | 'showHover'
| 'showReceivedStatistics' | 'showReceivedStatistics'
| 'showVoteButtons' | 'showVoteButtons'
> & { >;
onSimilarQuestionClick: () => void;
};
export default function SimilarQuestionCard(props: SimilarQuestionCardProps) { export default function SimilarQuestionCard(props: SimilarQuestionCardProps) {
const { onSimilarQuestionClick, ...rest } = props;
return ( return (
<BaseQuestionCard <BaseQuestionCard
actionButtonLabel="Yes, this is my question" showActionButton={false}
showActionButton={true} showAggregateStatistics={true}
showAggregateStatistics={false}
showAnswerStatistics={false} showAnswerStatistics={false}
showCreateEncounterButton={false} showCreateEncounterButton={true}
showDeleteButton={false} showDeleteButton={false}
showHover={true} showHover={true}
showReceivedStatistics={false} showReceivedStatistics={false}
showVoteButtons={false} showVoteButtons={false}
onActionButtonClick={onSimilarQuestionClick} {...props}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{...(rest as any)}
/> />
); );
} }

@ -1,15 +1,21 @@
import { startOfMonth } from 'date-fns'; import { startOfMonth } from 'date-fns';
import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { ArrowPathIcon } from '@heroicons/react/20/solid';
import type { QuestionsQuestionType } from '@prisma/client'; import type { QuestionsQuestionType } from '@prisma/client';
import type { TypeaheadOption } from '@tih/ui'; import type { TypeaheadOption } from '@tih/ui';
import { CheckboxInput } from '@tih/ui';
import { Button, HorizontalDivider, Select, TextArea } from '@tih/ui'; import { Button, HorizontalDivider, Select, TextArea } from '@tih/ui';
import { QUESTION_TYPES } from '~/utils/questions/constants'; import { QUESTION_TYPES } from '~/utils/questions/constants';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { import {
useFormRegister, useFormRegister,
useSelectRegister, useSelectRegister,
} from '~/utils/questions/useFormRegister'; } from '~/utils/questions/useFormRegister';
import { trpc } from '~/utils/trpc';
import SimilarQuestionCard from '../card/question/SimilarQuestionCard';
import CompanyTypeahead from '../typeahead/CompanyTypeahead'; import CompanyTypeahead from '../typeahead/CompanyTypeahead';
import LocationTypeahead from '../typeahead/LocationTypeahead'; import LocationTypeahead from '../typeahead/LocationTypeahead';
import RoleTypeahead from '../typeahead/RoleTypeahead'; import RoleTypeahead from '../typeahead/RoleTypeahead';
@ -30,26 +36,64 @@ export type ContributeQuestionData = {
export type ContributeQuestionFormProps = { export type ContributeQuestionFormProps = {
onDiscard: () => void; onDiscard: () => void;
onSimilarQuestionFound: () => void;
onSubmit: (data: ContributeQuestionData) => void; onSubmit: (data: ContributeQuestionData) => void;
}; };
export default function ContributeQuestionForm({ export default function ContributeQuestionForm({
onSubmit,
onDiscard, onDiscard,
onSimilarQuestionFound,
onSubmit,
}: ContributeQuestionFormProps) { }: ContributeQuestionFormProps) {
const { const {
control, control,
register: formRegister, register: formRegister,
handleSubmit, handleSubmit,
watch,
} = useForm<ContributeQuestionData>({ } = useForm<ContributeQuestionData>({
defaultValues: { defaultValues: {
date: startOfMonth(new Date()), date: startOfMonth(new Date()),
}, },
}); });
const [contentToCheck, setContentToCheck] = useState('');
const { data: similarQuestions } = trpc.useQuery(
['questions.questions.getRelatedQuestions', { content: contentToCheck }],
{
keepPreviousData: true,
},
);
const utils = trpc.useContext();
const { mutateAsync: addEncounterAsync } = trpc.useMutation(
'questions.questions.encounters.user.create',
{
onSuccess: () => {
utils.invalidateQueries(
'questions.questions.encounters.getAggregatedEncounters',
);
utils.invalidateQueries('questions.questions.getQuestionById');
},
},
);
const questionContent = watch('questionContent');
const register = useFormRegister(formRegister); const register = useFormRegister(formRegister);
const selectRegister = useSelectRegister(formRegister); const selectRegister = useSelectRegister(formRegister);
const [checkedSimilar, setCheckedSimilar] = useState<boolean>(false);
const handleCheckSimilarQuestions = (checked: boolean) => {
setCheckedSimilar(checked);
};
useEffect(() => {
if (questionContent !== contentToCheck) {
setCheckedSimilar(false);
}
}, [questionContent, contentToCheck]);
return ( return (
<div className="flex flex-col justify-between gap-4"> <div className="flex flex-col justify-between gap-4">
<form <form
@ -149,12 +193,83 @@ export default function ContributeQuestionForm({
<div className="w-full"> <div className="w-full">
<HorizontalDivider /> <HorizontalDivider />
</div> </div>
<h2
className="text-primary-900 mb-3
text-lg font-semibold
">
Are these questions the same as yours?
</h2>
<Button
addonPosition="start"
disabled={questionContent === contentToCheck}
icon={ArrowPathIcon}
label="Refresh similar questions"
variant="primary"
onClick={() => {
setContentToCheck(questionContent);
}}
/>
<div className="flex flex-col gap-y-2">
{similarQuestions?.map((question) => {
const { companyCounts, countryCounts, roleCounts } =
relabelQuestionAggregates(question.aggregatedQuestionEncounters);
return (
<SimilarQuestionCard
key={question.id}
companies={companyCounts}
content={question.content}
countries={countryCounts}
createEncounterButtonText="Yes, this is my question"
questionId={question.id}
roles={roleCounts}
timestamp={
question.seenAt.toLocaleDateString(undefined, {
month: 'short',
year: 'numeric',
}) ?? null
}
type={question.type}
onReceivedSubmit={async (data) => {
await addEncounterAsync({
cityId: data.cityId,
companyId: data.company,
countryId: data.countryId,
questionId: question.id,
role: data.role,
seenAt: data.seenAt,
stateId: data.stateId,
});
onSimilarQuestionFound();
}}
/>
);
})}
{similarQuestions?.length === 0 && (
<p className="font-semibold text-slate-900">
No similar questions found.
</p>
)}
</div>
<div <div
className="bg-primary-50 flex w-full justify-end gap-y-2 py-3 shadow-[0_0_0_100vmax_theme(colors.primary.50)]" className="bg-primary-50 flex w-full justify-between gap-y-2 py-3 shadow-[0_0_0_100vmax_theme(colors.primary.50)]"
style={{ style={{
// Hack to make the background bleed outside the container // Hack to make the background bleed outside the container
clipPath: 'inset(0 -100vmax)', clipPath: 'inset(0 -100vmax)',
}}> }}>
<div className="my-2 flex items-center sm:my-0">
<CheckboxInput
disabled={questionContent !== contentToCheck}
label={
questionContent !== contentToCheck
? 'I have checked that my question is new (Refresh similar questions to proceed)'
: 'I have checked that my question is new'
}
value={checkedSimilar}
onChange={handleCheckSimilarQuestions}
/>
</div>
<div className="flex gap-x-2"> <div className="flex gap-x-2">
<button <button
className="focus:ring-primary-500 inline-flex w-full justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-base font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" className="focus:ring-primary-500 inline-flex w-full justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-base font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"

@ -193,6 +193,7 @@ export default function QuestionPage() {
{...question} {...question}
companies={relabeledAggregatedEncounters?.companyCounts ?? {}} companies={relabeledAggregatedEncounters?.companyCounts ?? {}}
countries={relabeledAggregatedEncounters?.countryCounts ?? {}} countries={relabeledAggregatedEncounters?.countryCounts ?? {}}
createEncounterButtonText="I received this too"
questionId={question.id} questionId={question.id}
receivedCount={undefined} receivedCount={undefined}
roles={relabeledAggregatedEncounters?.roleCounts ?? {}} roles={relabeledAggregatedEncounters?.roleCounts ?? {}}

@ -1,8 +1,6 @@
import { z } from 'zod'; import { z } from 'zod';
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
import { JobTitleLabels } from '~/components/shared/JobTitles';
import { createProtectedRouter } from '../context'; import { createProtectedRouter } from '../context';
import { SortOrder } from '~/types/questions.d'; import { SortOrder } from '~/types/questions.d';
@ -14,7 +12,7 @@ export const questionsQuestionEncounterUserRouter = createProtectedRouter()
companyId: z.string(), companyId: z.string(),
countryId: z.string(), countryId: z.string(),
questionId: z.string(), questionId: z.string(),
role: z.nativeEnum(JobTitleLabels), role: z.string(),
seenAt: z.date(), seenAt: z.date(),
stateId: z.string().nullish(), stateId: z.string().nullish(),
}), }),

6907
tatus

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save