[questions][feat] create question encounters

pull/411/head
Jeff Sieu 3 years ago
parent d61b5fab02
commit f4f5b30c5e

@ -7,7 +7,7 @@ import {
import { TextInput } from '@tih/ui'; import { TextInput } from '@tih/ui';
import ContributeQuestionDialog from './ContributeQuestionDialog'; import ContributeQuestionDialog from './ContributeQuestionDialog';
import type { ContributeQuestionFormProps } from './ContributeQuestionForm'; import type { ContributeQuestionFormProps } from './forms/ContributeQuestionForm';
export type ContributeQuestionCardProps = Pick< export type ContributeQuestionCardProps = Pick<
ContributeQuestionFormProps, ContributeQuestionFormProps,

@ -2,9 +2,9 @@ import { Fragment, useState } from 'react';
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from '@headlessui/react';
import { HorizontalDivider } from '@tih/ui'; import { HorizontalDivider } from '@tih/ui';
import type { ContributeQuestionFormProps } from './ContributeQuestionForm';
import ContributeQuestionForm from './ContributeQuestionForm';
import DiscardDraftDialog from './DiscardDraftDialog'; import DiscardDraftDialog from './DiscardDraftDialog';
import type { ContributeQuestionFormProps } from './forms/ContributeQuestionForm';
import ContributeQuestionForm from './forms/ContributeQuestionForm';
export type ContributeQuestionDialogProps = Pick< export type ContributeQuestionDialogProps = Pick<
ContributeQuestionFormProps, ContributeQuestionFormProps,
@ -60,7 +60,7 @@ export default function ContributeQuestionDialog({
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"> leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<Dialog.Panel className="relative max-w-5xl transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 w-full"> <Dialog.Panel className="relative w-full max-w-5xl transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8">
<div className="bg-white p-6 pt-5 sm:pb-4"> <div className="bg-white p-6 pt-5 sm:pb-4">
<div className="flex flex-1 items-stretch"> <div className="flex flex-1 items-stretch">
<div className="mt-3 w-full sm:mt-0 sm:text-left"> <div className="mt-3 w-full sm:mt-0 sm:text-left">

@ -4,13 +4,15 @@ import QuestionCard from './QuestionCard';
export type QuestionOverviewCardProps = Omit< export type QuestionOverviewCardProps = Omit<
QuestionCardProps & { QuestionCardProps & {
showActionButton: false; showActionButton: false;
showUserStatistics: false; showAnswerStatistics: false;
showReceivedStatistics: true;
showVoteButtons: true; showVoteButtons: true;
}, },
| 'actionButtonLabel' | 'actionButtonLabel'
| 'onActionButtonClick' | 'onActionButtonClick'
| 'showActionButton' | 'showActionButton'
| 'showUserStatistics' | 'showAnswerStatistics'
| 'showReceivedStatistics'
| 'showVoteButtons' | 'showVoteButtons'
>; >;
@ -19,7 +21,8 @@ export default function FullQuestionCard(props: QuestionOverviewCardProps) {
<QuestionCard <QuestionCard
{...props} {...props}
showActionButton={false} showActionButton={false}
showUserStatistics={false} showAnswerStatistics={false}
showReceivedStatistics={true}
showVoteButtons={true} showVoteButtons={true}
/> />
); );

@ -1,9 +1,15 @@
import { ChatBubbleBottomCenterTextIcon } from '@heroicons/react/24/outline'; import { useState } from 'react';
import {
ChatBubbleBottomCenterTextIcon,
EyeIcon,
} from '@heroicons/react/24/outline';
import type { QuestionsQuestionType } from '@prisma/client'; import type { QuestionsQuestionType } from '@prisma/client';
import { Badge, Button } from '@tih/ui'; import { Badge, Button } from '@tih/ui';
import { useQuestionVote } from '~/utils/questions/useVote'; import { useQuestionVote } from '~/utils/questions/useVote';
import type { CreateQuestionEncounterData } from '../forms/CreateQuestionEncounterForm';
import CreateQuestionEncounterForm from '../forms/CreateQuestionEncounterForm';
import QuestionTypeBadge from '../QuestionTypeBadge'; import QuestionTypeBadge from '../QuestionTypeBadge';
import VotingButtons from '../VotingButtons'; import VotingButtons from '../VotingButtons';
@ -17,14 +23,14 @@ type UpvoteProps =
upvoteCount?: never; upvoteCount?: never;
}; };
type StatisticsProps = type AnswerStatisticsProps =
| { | {
answerCount: number; answerCount: number;
showUserStatistics: true; showAnswerStatistics: true;
} }
| { | {
answerCount?: never; answerCount?: never;
showUserStatistics?: false; showAnswerStatistics?: false;
}; };
type ActionButtonProps = type ActionButtonProps =
@ -39,14 +45,26 @@ type ActionButtonProps =
showActionButton?: false; showActionButton?: false;
}; };
type ReceivedStatisticsProps =
| {
onReceivedSubmit: (data: CreateQuestionEncounterData) => void;
receivedCount: number;
showReceivedStatistics: true;
}
| {
onReceivedSubmit?: never;
receivedCount?: never;
showReceivedStatistics?: false;
};
export type QuestionCardProps = ActionButtonProps & export type QuestionCardProps = ActionButtonProps &
StatisticsProps & AnswerStatisticsProps &
ReceivedStatisticsProps &
UpvoteProps & { UpvoteProps & {
company: string; company: string;
content: string; content: string;
location: string; location: string;
questionId: string; questionId: string;
receivedCount: number;
role: string; role: string;
showHover?: boolean; showHover?: boolean;
timestamp: string; timestamp: string;
@ -58,10 +76,11 @@ export default function QuestionCard({
company, company,
answerCount, answerCount,
content, content,
// ReceivedCount, receivedCount,
type, type,
showVoteButtons, showVoteButtons,
showUserStatistics, showAnswerStatistics,
showReceivedStatistics,
showActionButton, showActionButton,
actionButtonLabel, actionButtonLabel,
onActionButtonClick, onActionButtonClick,
@ -70,12 +89,24 @@ export default function QuestionCard({
role, role,
location, location,
showHover, showHover,
onReceivedSubmit,
}: QuestionCardProps) { }: QuestionCardProps) {
const [showReceivedForm, setShowReceivedForm] = useState(false);
const { handleDownvote, handleUpvote, vote } = useQuestionVote(questionId); const { handleDownvote, handleUpvote, vote } = useQuestionVote(questionId);
const hoverClass = showHover ? 'hover:bg-slate-50' : ''; const hoverClass = showHover ? 'hover:bg-slate-50' : '';
return (
<article const cardContent = showReceivedForm ? (
className={`flex gap-4 rounded-md border border-slate-300 bg-white p-4 ${hoverClass}`}> <CreateQuestionEncounterForm
onCancel={() => {
setShowReceivedForm(false);
}}
onSubmit={(data) => {
onReceivedSubmit?.(data);
setShowReceivedForm(false);
}}
/>
) : (
<>
{showVoteButtons && ( {showVoteButtons && (
<VotingButtons <VotingButtons
upvoteCount={upvoteCount} upvoteCount={upvoteCount}
@ -105,8 +136,9 @@ export default function QuestionCard({
<div className="ml-2"> <div className="ml-2">
<p className="line-clamp-2 text-ellipsis ">{content}</p> <p className="line-clamp-2 text-ellipsis ">{content}</p>
</div> </div>
{showUserStatistics && ( {(showAnswerStatistics || showReceivedStatistics) && (
<div className="flex gap-2"> <div className="flex gap-2">
{showAnswerStatistics && (
<Button <Button
addonPosition="start" addonPosition="start"
icon={ChatBubbleBottomCenterTextIcon} icon={ChatBubbleBottomCenterTextIcon}
@ -114,16 +146,30 @@ export default function QuestionCard({
size="sm" size="sm"
variant="tertiary" variant="tertiary"
/> />
{/* <Button )}
{showReceivedStatistics && (
<Button
addonPosition="start" addonPosition="start"
icon={EyeIcon} icon={EyeIcon}
label={`${receivedCount} received this`} label={`${receivedCount} received this`}
size="sm" size="sm"
variant="tertiary" variant="tertiary"
/> */} onClick={(event) => {
event.preventDefault();
setShowReceivedForm(true);
}}
/>
)}
</div> </div>
)} )}
</div> </div>
</>
);
return (
<article
className={`flex gap-4 rounded-md border border-slate-300 bg-white p-4 ${hoverClass}`}>
{cardContent}
</article> </article>
); );
} }

@ -6,13 +6,15 @@ import QuestionCard from './QuestionCard';
export type QuestionOverviewCardProps = Omit< export type QuestionOverviewCardProps = Omit<
QuestionCardProps & { QuestionCardProps & {
showActionButton: false; showActionButton: false;
showUserStatistics: true; showAnswerStatistics: true;
showReceivedStatistics: true;
showVoteButtons: true; showVoteButtons: true;
}, },
| 'actionButtonLabel' | 'actionButtonLabel'
| 'onActionButtonClick' | 'onActionButtonClick'
| 'showActionButton' | 'showActionButton'
| 'showUserStatistics' | 'showAnswerStatistics'
| 'showReceivedStatistics'
| 'showVoteButtons' | 'showVoteButtons'
>; >;
@ -21,8 +23,9 @@ function QuestionOverviewCardWithoutHref(props: QuestionOverviewCardProps) {
<QuestionCard <QuestionCard
{...props} {...props}
showActionButton={false} showActionButton={false}
showAnswerStatistics={true}
showHover={true} showHover={true}
showUserStatistics={true} showReceivedStatistics={true}
showVoteButtons={true} showVoteButtons={true}
/> />
); );

@ -17,9 +17,11 @@ import {
useSelectRegister, useSelectRegister,
} from '~/utils/questions/useFormRegister'; } from '~/utils/questions/useFormRegister';
import CompaniesTypeahead from '../shared/CompaniesTypeahead'; import LocationTypeahead from '../typeahead/LocationTypeahead';
import type { Month } from '../shared/MonthYearPicker'; import RoleTypeahead from '../typeahead/RoleTypeahead';
import MonthYearPicker from '../shared/MonthYearPicker'; import CompaniesTypeahead from '../../shared/CompaniesTypeahead';
import type { Month } from '../../shared/MonthYearPicker';
import MonthYearPicker from '../../shared/MonthYearPicker';
export type ContributeQuestionData = { export type ContributeQuestionData = {
company: string; company: string;
@ -86,9 +88,7 @@ export default function ContributeQuestionForm({
control={control} control={control}
name="location" name="location"
render={({ field }) => ( render={({ field }) => (
<Typeahead <LocationTypeahead
label="Location"
options={LOCATIONS}
required={true} required={true}
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
onQueryChange={() => {}} onQueryChange={() => {}}
@ -142,9 +142,7 @@ export default function ContributeQuestionForm({
control={control} control={control}
name="role" name="role"
render={({ field }) => ( render={({ field }) => (
<Typeahead <RoleTypeahead
label="Role"
options={ROLES}
required={true} required={true}
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
onQueryChange={() => {}} onQueryChange={() => {}}

@ -0,0 +1,110 @@
import { startOfMonth } from 'date-fns';
import { useState } from 'react';
import { Button } from '@tih/ui';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import type { Month } from '~/components/shared/MonthYearPicker';
import MonthYearPicker from '~/components/shared/MonthYearPicker';
import LocationTypeahead from '../typeahead/LocationTypeahead';
import RoleTypeahead from '../typeahead/RoleTypeahead';
export type CreateQuestionEncounterData = {
company: string;
location: string;
role: string;
seenAt: Date;
};
export type CreateQuestionEncounterFormProps = {
onCancel: () => void;
onSubmit: (data: CreateQuestionEncounterData) => void;
};
export default function CreateQuestionEncounterForm({
onCancel,
onSubmit,
}: CreateQuestionEncounterFormProps) {
const [selectedCompany, setSelectedCompany] = useState<string | null>(null);
const [selectedLocation, setSelectedLocation] = useState<string | null>(null);
const [selectedRole, setSelectedRole] = useState<string | null>(null);
const [selectedDate, setSelectedDate] = useState<Date>(
startOfMonth(new Date()),
);
return (
<div>
<Button
label="Cancel"
variant="tertiary"
onClick={(event) => {
event.preventDefault();
onCancel();
}}
/>
<div className="flex items-center gap-2">
<p className="font-md text-xl text-slate-600">I saw this question at</p>
<div>
<CompaniesTypeahead
isLabelHidden={true}
onSelect={({ value: company }) => {
setSelectedCompany(company);
}}
/>
</div>
<div>
<LocationTypeahead
isLabelHidden={true}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onQueryChange={() => {}}
onSelect={({ value: location }) => {
setSelectedLocation(location);
}}
/>
</div>
<p className="font-md text-xl text-slate-600">for</p>
<div>
<RoleTypeahead
isLabelHidden={true}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onQueryChange={() => {}}
onSelect={({ value: role }) => {
setSelectedRole(role);
}}
/>
</div>
<p className="font-md text-xl text-slate-600">for</p>
<MonthYearPicker
value={{
month: ((selectedDate?.getMonth() ?? 0) + 1) as Month,
year: selectedDate?.getFullYear() as number,
}}
onChange={(value) => {
setSelectedDate(
startOfMonth(new Date(value.year, value.month - 1)),
);
}}
/>
<Button
label="Submit"
variant="primary"
onClick={() => {
if (
selectedCompany &&
selectedLocation &&
selectedRole &&
selectedDate
) {
onSubmit({
company: selectedCompany,
location: selectedLocation,
role: selectedRole,
seenAt: selectedDate,
});
}
}}
/>
</div>
</div>
);
}

@ -0,0 +1,12 @@
import type { ComponentProps } from 'react';
import { Typeahead } from '@tih/ui';
import { LOCATIONS } from '~/utils/questions/constants';
type TypeaheadProps = ComponentProps<typeof Typeahead>;
export type LocationTypeaheadProps = Omit<TypeaheadProps, 'label' | 'options'>;
export default function LocationTypeahead(props: LocationTypeaheadProps) {
return <Typeahead label="Location" options={LOCATIONS} {...props} />;
}

@ -0,0 +1,12 @@
import type { ComponentProps } from 'react';
import { Typeahead } from '@tih/ui';
import { ROLES } from '~/utils/questions/constants';
type TypeaheadProps = ComponentProps<typeof Typeahead>;
export type RoleTypeaheadProps = Omit<TypeaheadProps, 'label' | 'options'>;
export default function RoleTypeahead(props: RoleTypeaheadProps) {
return <Typeahead label="Role" options={ROLES} {...props} />;
}

@ -45,6 +45,11 @@ export default function QuestionPage() {
{ id: questionId as string }, { id: questionId as string },
]); ]);
const { data: aggregatedEncounters } = trpc.useQuery([
'questions.questions.encounters.getAggregatedEncounters',
{ questionId: questionId as string },
]);
const utils = trpc.useContext(); const utils = trpc.useContext();
const { data: comments } = trpc.useQuery([ const { data: comments } = trpc.useQuery([
@ -74,6 +79,18 @@ export default function QuestionPage() {
}, },
}); });
const { mutate: addEncounter } = trpc.useMutation(
'questions.questions.encounters.create',
{
onSuccess: () => {
utils.invalidateQueries(
'questions.questions.encounters.getAggregatedEncounters',
);
utils.invalidateQueries('questions.questions.getQuestionById');
},
},
);
const handleBackNavigation = () => { const handleBackNavigation = () => {
router.back(); router.back();
}; };
@ -114,12 +131,21 @@ export default function QuestionPage() {
<FullQuestionCard <FullQuestionCard
{...question} {...question}
questionId={question.id} questionId={question.id}
receivedCount={0} receivedCount={question.receivedCount}
timestamp={question.seenAt.toLocaleDateString(undefined, { timestamp={question.seenAt.toLocaleDateString(undefined, {
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',
})} })}
upvoteCount={question.numVotes} upvoteCount={question.numVotes}
onReceivedSubmit={(data) => {
addEncounter({
companyId: data.company,
location: data.location,
questionId: questionId as string,
role: data.role,
seenAt: data.seenAt,
});
}}
/> />
<div className="mx-2"> <div className="mx-2">
<Collapsible label={`${(comments ?? []).length} comment(s)`}> <Collapsible label={`${(comments ?? []).length} comment(s)`}>
@ -180,7 +206,7 @@ export default function QuestionPage() {
authorName={comment.user} authorName={comment.user}
content={comment.content} content={comment.content}
createdAt={comment.createdAt} createdAt={comment.createdAt}
upvoteCount={0} upvoteCount={comment.numVotes}
/> />
))} ))}
</Collapsible> </Collapsible>

@ -381,14 +381,17 @@ export default function QuestionsHomePage() {
)}`} )}`}
location={question.location} location={question.location}
questionId={question.id} questionId={question.id}
receivedCount={0} receivedCount={question.receivedCount}
role={question.role} role={question.role}
timestamp={question.seenAt.toLocaleDateString(undefined, { timestamp={question.seenAt.toLocaleDateString(undefined, {
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',
})} // TODO: Implement received count })}
type={question.type} type={question.type}
upvoteCount={question.numVotes} upvoteCount={question.numVotes}
onReceivedSubmit={(data) => {
console.log(data);
}}
/> />
))} ))}
{questions?.length === 0 && ( {questions?.length === 0 && (

@ -10,6 +10,7 @@ import { protectedExampleRouter } from './protected-example-router';
import { questionsAnswerCommentRouter } from './questions-answer-comment-router'; import { questionsAnswerCommentRouter } from './questions-answer-comment-router';
import { questionsAnswerRouter } from './questions-answer-router'; import { questionsAnswerRouter } from './questions-answer-router';
import { questionsQuestionCommentRouter } from './questions-question-comment-router'; import { questionsQuestionCommentRouter } from './questions-question-comment-router';
import { questionsQuestionEncounterRouter } from './questions-question-encounter-router';
import { questionsQuestionRouter } from './questions-question-router'; import { questionsQuestionRouter } from './questions-question-router';
import { resumeCommentsRouter } from './resumes/resumes-comments-router'; import { resumeCommentsRouter } from './resumes/resumes-comments-router';
import { resumesCommentsUserRouter } from './resumes/resumes-comments-user-router'; import { resumesCommentsUserRouter } from './resumes/resumes-comments-user-router';
@ -40,6 +41,7 @@ export const appRouter = createRouter()
.merge('questions.answers.comments.', questionsAnswerCommentRouter) .merge('questions.answers.comments.', questionsAnswerCommentRouter)
.merge('questions.answers.', questionsAnswerRouter) .merge('questions.answers.', questionsAnswerRouter)
.merge('questions.questions.comments.', questionsQuestionCommentRouter) .merge('questions.questions.comments.', questionsQuestionCommentRouter)
.merge('questions.questions.encounters.', questionsQuestionEncounterRouter)
.merge('questions.questions.', questionsQuestionRouter) .merge('questions.questions.', questionsQuestionRouter)
.merge('offers.', offersRouter) .merge('offers.', offersRouter)
.merge('offers.profile.', offersProfileRouter) .merge('offers.profile.', offersProfileRouter)

@ -11,9 +11,10 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
questionId: z.string(), questionId: z.string(),
}), }),
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const questionEncountersData = await ctx.prisma.questionsQuestionEncounter.findMany({ const questionEncountersData =
await ctx.prisma.questionsQuestionEncounter.findMany({
include: { include: {
company : true, company: true,
}, },
where: { where: {
...input, ...input,
@ -22,7 +23,7 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
const companyCounts: Record<string, number> = {}; const companyCounts: Record<string, number> = {};
const locationCounts: Record<string, number> = {}; const locationCounts: Record<string, number> = {};
const roleCounts:Record<string, number> = {}; const roleCounts: Record<string, number> = {};
for (let i = 0; i < questionEncountersData.length; i++) { for (let i = 0; i < questionEncountersData.length; i++) {
const encounter = questionEncountersData[i]; const encounter = questionEncountersData[i];
@ -41,16 +42,15 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
roleCounts[encounter.role] = 1; roleCounts[encounter.role] = 1;
} }
roleCounts[encounter.role] += 1; roleCounts[encounter.role] += 1;
} }
const questionEncounter:AggregatedQuestionEncounter = { const questionEncounter: AggregatedQuestionEncounter = {
companyCounts, companyCounts,
locationCounts, locationCounts,
roleCounts, roleCounts,
} };
return questionEncounter; return questionEncounter;
} },
}) })
.mutation('create', { .mutation('create', {
input: z.object({ input: z.object({
@ -58,7 +58,7 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
location: z.string(), location: z.string(),
questionId: z.string(), questionId: z.string(),
role: z.string(), role: z.string(),
seenAt: z.date() seenAt: z.date(),
}), }),
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id; const userId = ctx.session?.user?.id;
@ -83,7 +83,8 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id; const userId = ctx.session?.user?.id;
const questionEncounterToUpdate = await ctx.prisma.questionsQuestionEncounter.findUnique({ const questionEncounterToUpdate =
await ctx.prisma.questionsQuestionEncounter.findUnique({
where: { where: {
id: input.id, id: input.id,
}, },
@ -113,7 +114,8 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id; const userId = ctx.session?.user?.id;
const questionEncounterToDelete = await ctx.prisma.questionsQuestionEncounter.findUnique({ const questionEncounterToDelete =
await ctx.prisma.questionsQuestionEncounter.findUnique({
where: { where: {
id: input.id, id: input.id,
}, },

@ -107,6 +107,7 @@ export const questionsQuestionRouter = createProtectedRouter()
numAnswers: data._count.answers, numAnswers: data._count.answers,
numComments: data._count.comments, numComments: data._count.comments,
numVotes: votes, numVotes: votes,
receivedCount: data.encounters.length,
role: data.encounters[0].role ?? 'Unknown role', role: data.encounters[0].role ?? 'Unknown role',
seenAt: data.encounters[0].seenAt, seenAt: data.encounters[0].seenAt,
type: data.questionType, type: data.questionType,
@ -180,6 +181,7 @@ export const questionsQuestionRouter = createProtectedRouter()
numAnswers: questionData._count.answers, numAnswers: questionData._count.answers,
numComments: questionData._count.comments, numComments: questionData._count.comments,
numVotes: votes, numVotes: votes,
receivedCount: questionData.encounters.length,
role: questionData.encounters[0].role ?? 'Unknown role', role: questionData.encounters[0].role ?? 'Unknown role',
seenAt: questionData.encounters[0].seenAt, seenAt: questionData.encounters[0].seenAt,
type: questionData.questionType, type: questionData.questionType,

@ -9,6 +9,7 @@ export type Question = {
numAnswers: number; numAnswers: number;
numComments: number; numComments: number;
numVotes: number; numVotes: number;
receivedCount: number;
role: string; role: string;
seenAt: Date; seenAt: Date;
type: QuestionsQuestionType; type: QuestionsQuestionType;
@ -20,7 +21,7 @@ export type AggregatedQuestionEncounter = {
companyCounts: Record<string, number>; companyCounts: Record<string, number>;
locationCounts: Record<string, number>; locationCounts: Record<string, number>;
roleCounts: Record<string, number>; roleCounts: Record<string, number>;
} };
export type AnswerComment = { export type AnswerComment = {
content: string; content: string;

Loading…
Cancel
Save