mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-14 01:33:01 +08:00
[questions][feat] add similar questions check (#468)
Co-authored-by: wlren <weilinwork99@gmail.com>
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { HorizontalDivider } from '@tih/ui';
|
||||
import { HorizontalDivider, useToast } from '@tih/ui';
|
||||
|
||||
import DiscardDraftDialog from './DiscardDraftDialog';
|
||||
import type { ContributeQuestionFormProps } from './forms/ContributeQuestionForm';
|
||||
@ -21,6 +21,8 @@ export default function ContributeQuestionDialog({
|
||||
}: ContributeQuestionDialogProps) {
|
||||
const [showDiscardDialog, setShowDiscardDialog] = useState(false);
|
||||
|
||||
const { showToast } = useToast();
|
||||
|
||||
const handleDraftDiscard = () => {
|
||||
setShowDiscardDialog(false);
|
||||
onCancel();
|
||||
@ -75,6 +77,14 @@ export default function ContributeQuestionDialog({
|
||||
<div className="mt-2">
|
||||
<ContributeQuestionForm
|
||||
onDiscard={() => setShowDiscardDialog(true)}
|
||||
onSimilarQuestionFound={() => {
|
||||
onCancel();
|
||||
showToast({
|
||||
title:
|
||||
'Your response has been recorded. Draft discarded.',
|
||||
variant: 'success',
|
||||
});
|
||||
}}
|
||||
onSubmit={(data) => {
|
||||
onSubmit(data);
|
||||
onCancel();
|
||||
|
@ -88,10 +88,12 @@ type ReceivedStatisticsProps =
|
||||
|
||||
type CreateEncounterProps =
|
||||
| {
|
||||
createEncounterButtonText: string;
|
||||
onReceivedSubmit: (data: CreateQuestionEncounterData) => void;
|
||||
showCreateEncounterButton: true;
|
||||
}
|
||||
| {
|
||||
createEncounterButtonText?: never;
|
||||
onReceivedSubmit?: never;
|
||||
showCreateEncounterButton?: false;
|
||||
};
|
||||
@ -132,6 +134,7 @@ export default function BaseQuestionCard({
|
||||
showAnswerStatistics,
|
||||
showReceivedStatistics,
|
||||
showCreateEncounterButton,
|
||||
createEncounterButtonText,
|
||||
showActionButton,
|
||||
actionButtonLabel,
|
||||
onActionButtonClick,
|
||||
@ -238,7 +241,7 @@ export default function BaseQuestionCard({
|
||||
<Button
|
||||
addonPosition="start"
|
||||
icon={CheckIcon}
|
||||
label="I received this too"
|
||||
label={createEncounterButtonText}
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onClick={(event) => {
|
||||
|
@ -3,10 +3,10 @@ import BaseQuestionCard from './BaseQuestionCard';
|
||||
|
||||
export type SimilarQuestionCardProps = Omit<
|
||||
BaseQuestionCardProps & {
|
||||
showActionButton: true;
|
||||
showAggregateStatistics: false;
|
||||
showActionButton: false;
|
||||
showAggregateStatistics: true;
|
||||
showAnswerStatistics: false;
|
||||
showCreateEncounterButton: false;
|
||||
showCreateEncounterButton: true;
|
||||
showDeleteButton: false;
|
||||
showHover: true;
|
||||
showReceivedStatistics: false;
|
||||
@ -22,26 +22,20 @@ export type SimilarQuestionCardProps = Omit<
|
||||
| 'showHover'
|
||||
| 'showReceivedStatistics'
|
||||
| 'showVoteButtons'
|
||||
> & {
|
||||
onSimilarQuestionClick: () => void;
|
||||
};
|
||||
>;
|
||||
|
||||
export default function SimilarQuestionCard(props: SimilarQuestionCardProps) {
|
||||
const { onSimilarQuestionClick, ...rest } = props;
|
||||
return (
|
||||
<BaseQuestionCard
|
||||
actionButtonLabel="Yes, this is my question"
|
||||
showActionButton={true}
|
||||
showAggregateStatistics={false}
|
||||
showActionButton={false}
|
||||
showAggregateStatistics={true}
|
||||
showAnswerStatistics={false}
|
||||
showCreateEncounterButton={false}
|
||||
showCreateEncounterButton={true}
|
||||
showDeleteButton={false}
|
||||
showHover={true}
|
||||
showReceivedStatistics={false}
|
||||
showVoteButtons={false}
|
||||
onActionButtonClick={onSimilarQuestionClick}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{...(rest as any)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,15 +1,21 @@
|
||||
import { startOfMonth } from 'date-fns';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { ArrowPathIcon } from '@heroicons/react/20/solid';
|
||||
import type { QuestionsQuestionType } from '@prisma/client';
|
||||
import type { TypeaheadOption } from '@tih/ui';
|
||||
import { CheckboxInput } from '@tih/ui';
|
||||
import { Button, HorizontalDivider, Select, TextArea } from '@tih/ui';
|
||||
|
||||
import { QUESTION_TYPES } from '~/utils/questions/constants';
|
||||
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
|
||||
import {
|
||||
useFormRegister,
|
||||
useSelectRegister,
|
||||
} from '~/utils/questions/useFormRegister';
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import SimilarQuestionCard from '../card/question/SimilarQuestionCard';
|
||||
import CompanyTypeahead from '../typeahead/CompanyTypeahead';
|
||||
import LocationTypeahead from '../typeahead/LocationTypeahead';
|
||||
import RoleTypeahead from '../typeahead/RoleTypeahead';
|
||||
@ -30,26 +36,64 @@ export type ContributeQuestionData = {
|
||||
|
||||
export type ContributeQuestionFormProps = {
|
||||
onDiscard: () => void;
|
||||
onSimilarQuestionFound: () => void;
|
||||
onSubmit: (data: ContributeQuestionData) => void;
|
||||
};
|
||||
|
||||
export default function ContributeQuestionForm({
|
||||
onSubmit,
|
||||
onDiscard,
|
||||
onSimilarQuestionFound,
|
||||
onSubmit,
|
||||
}: ContributeQuestionFormProps) {
|
||||
const {
|
||||
control,
|
||||
register: formRegister,
|
||||
handleSubmit,
|
||||
watch,
|
||||
} = useForm<ContributeQuestionData>({
|
||||
defaultValues: {
|
||||
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 selectRegister = useSelectRegister(formRegister);
|
||||
|
||||
const [checkedSimilar, setCheckedSimilar] = useState<boolean>(false);
|
||||
const handleCheckSimilarQuestions = (checked: boolean) => {
|
||||
setCheckedSimilar(checked);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (questionContent !== contentToCheck) {
|
||||
setCheckedSimilar(false);
|
||||
}
|
||||
}, [questionContent, contentToCheck]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-between gap-4">
|
||||
<form
|
||||
@ -149,12 +193,83 @@ export default function ContributeQuestionForm({
|
||||
<div className="w-full">
|
||||
<HorizontalDivider />
|
||||
</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
|
||||
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={{
|
||||
// Hack to make the background bleed outside the container
|
||||
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">
|
||||
<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"
|
||||
|
@ -193,6 +193,7 @@ export default function QuestionPage() {
|
||||
{...question}
|
||||
companies={relabeledAggregatedEncounters?.companyCounts ?? {}}
|
||||
countries={relabeledAggregatedEncounters?.countryCounts ?? {}}
|
||||
createEncounterButtonText="I received this too"
|
||||
questionId={question.id}
|
||||
receivedCount={undefined}
|
||||
roles={relabeledAggregatedEncounters?.roleCounts ?? {}}
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { JobTitleLabels } from '~/components/shared/JobTitles';
|
||||
|
||||
import { createProtectedRouter } from '../context';
|
||||
|
||||
import { SortOrder } from '~/types/questions.d';
|
||||
@ -14,7 +12,7 @@ export const questionsQuestionEncounterUserRouter = createProtectedRouter()
|
||||
companyId: z.string(),
|
||||
countryId: z.string(),
|
||||
questionId: z.string(),
|
||||
role: z.nativeEnum(JobTitleLabels),
|
||||
role: z.string(),
|
||||
seenAt: z.date(),
|
||||
stateId: z.string().nullish(),
|
||||
}),
|
||||
|
Reference in New Issue
Block a user