Merge branch 'hongpo/sorting-pagination' of https://github.com/yangshun/tech-interview-handbook into hongpo/sorting-pagination

pull/457/head
hpkoh 3 years ago
commit 06840d10da

@ -9,7 +9,8 @@
"lint": "next lint", "lint": "next lint",
"tsc": "tsc", "tsc": "tsc",
"postinstall": "prisma generate", "postinstall": "prisma generate",
"seed": "ts-node prisma/seed.ts" "seed": "ts-node prisma/seed.ts",
"seed-questions": "ts-node prisma/seed-questions.ts"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.7.3", "@headlessui/react": "^1.7.3",

@ -0,0 +1,207 @@
import { PrismaClient } from '@prisma/client';
import { JobTitleLabels } from '../src/components/shared/JobTitles';
const prisma = new PrismaClient();
type QuestionCreateData = Parameters<
typeof prisma.questionsQuestion.create
>[0]['data'];
function selectRandomRole() {
const roles = Object.keys(JobTitleLabels);
const randomIndex = Math.floor(Math.random() * roles.length);
return roles[randomIndex];
}
function generateRandomDate() {
// Return a date between 2020 and 2022.
const start = new Date(2020, 0, 1);
const end = new Date(2022, 0, 1);
return new Date(
start.getTime() + Math.random() * (end.getTime() - start.getTime()),
);
}
function generateRandomCodingAnswer() {
return CODING_ANSWER_CONTENT[
Math.floor(Math.random() * CODING_ANSWER_CONTENT.length)
];
}
function generateRandomBehavioralAnswer() {
return BEHAVIORAL_ANSWER_CONTENT[
Math.floor(Math.random() * BEHAVIORAL_ANSWER_CONTENT.length)
];
}
const CODING_QUESTION_CONTENT = [
'Given a string, find the length of the longest substring without repeating characters.',
'Given an array of integers, return indices of the two numbers such that they add up to a specific target.',
'Given a contiguous sequence of numbers in which each number repeats thrice, there is exactly one missing number. Find the missing number.',
'Find the contiguous subarray within an array (containing at least one number) which has the largest product.',
'Find a contiguous subarray which has the largest sum.',
];
const BEHAVIORAL_QUESTION_CONTENT = [
'Tell me about a time you had to work with a difficult person.',
'Rate your communication skills on a scale of 1 to 10.',
'Are you a team player?',
'What is your greatest weakness?',
'What is your greatest strength?',
'What is your biggest accomplishment?',
'What is your biggest failure?',
'Be honest, how would your friends describe you?',
'How do you handle stress?',
'Lets say you have a deadline to meet. How do you prioritize your work?',
];
const CODING_ANSWER_CONTENT = [
'This question is easy. Just use a hash map.',
'This question is hard. I have no idea how to solve it.',
'This question is medium. I can solve it in 30 minutes.',
'Can be done with a simple for loop.',
'Simple recursion can solve this.',
'Please explain the question again.',
'Question is not clear.',
'Brute force solution is the best.',
];
const BEHAVIORAL_ANSWER_CONTENT = [
'This is a very common question. I have a lot of experience with this.',
"I don't think this is a good question to ask. However, I can answer it.",
'Most companies ask this question. I think you should ask something else.',
'I try to take a step back and assess the situation. I figure out what is the most important thing to do and what can wait. I also try to delegate or ask for help when needed.',
'I try to have a discussion with my manager or the person who I feel is not valuing my work. I try to explain how I feel and what I would like to see change.',
'I try to have a discussion with the coworker. I try to understand their perspective and see if there is a way to resolve the issue.',
];
const CODING_QUESTIONS: Array<QuestionCreateData> = CODING_QUESTION_CONTENT.map(
(content) => ({
content,
questionType: 'CODING',
userId: null,
encounters: {
create: {
location: 'Singapore',
role: selectRandomRole(),
seenAt: generateRandomDate(),
},
},
}),
);
const BEHAVIORAL_QUESTIONS: Array<QuestionCreateData> =
BEHAVIORAL_QUESTION_CONTENT.map((content) => ({
content,
questionType: 'BEHAVIORAL',
userId: null,
encounters: {
create: {
location: 'Singapore',
role: selectRandomRole(),
seenAt: generateRandomDate(),
},
},
}));
const QUESTIONS: Array<QuestionCreateData> = [
...CODING_QUESTIONS,
...BEHAVIORAL_QUESTIONS,
];
async function main() {
console.log('Performing preliminary checks...');
const firstCompany = await prisma.company.findFirst();
if (!firstCompany) {
throw new Error(
'No company found. Please seed db with some companies first.',
);
}
// Generate random answers to the questions
const users = await prisma.user.findMany();
if (users.length === 0) {
throw new Error('No users found. Please seed db with some users first.');
}
console.log('Seeding started...');
console.log('Creating coding and behavioral questions...');
await Promise.all([
QUESTIONS.map(async (question) => {
await prisma.questionsQuestion.create({
data: {
...question,
encounters: {
create: {
...question.encounters!.create,
companyId: firstCompany.id,
} as any,
},
},
});
}),
]);
console.log('Creating answers to coding questions...');
const codingQuestions = await prisma.questionsQuestion.findMany({
where: {
questionType: 'CODING',
},
});
await Promise.all(
codingQuestions.map(async (question) => {
const answers = Array.from(
{ length: Math.floor(Math.random() * 5) },
() => ({
content: generateRandomCodingAnswer(),
userId: users[Math.floor(Math.random() * users.length)].id,
questionId: question.id,
}),
);
await prisma.questionsAnswer.createMany({
data: answers,
});
}),
);
console.log('Creating answers to behavioral questions...');
const behavioralQuestions = await prisma.questionsQuestion.findMany({
where: {
questionType: 'BEHAVIORAL',
},
});
await Promise.all(
behavioralQuestions.map(async (question) => {
const answers = Array.from(
{ length: Math.floor(Math.random() * 5) },
() => ({
content: generateRandomBehavioralAnswer(),
userId: users[Math.floor(Math.random() * users.length)].id,
questionId: question.id,
}),
);
await prisma.questionsAnswer.createMany({
data: answers,
});
}),
);
console.log('Seeding completed.');
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});

@ -30,7 +30,7 @@ export default function ContributeQuestionCard({
return ( return (
<div className="w-full"> <div className="w-full">
<button <button
className="w-full flex flex-col items-stretch justify-center gap-2 rounded-md border border-slate-300 bg-white p-4 text-left hover:bg-slate-100" className="flex flex-col items-stretch justify-center gap-2 rounded-md border border-slate-300 bg-white p-4 text-left hover:bg-slate-100"
type="button" type="button"
onClick={handleOpenContribute}> onClick={handleOpenContribute}>
<TextInput <TextInput
@ -72,12 +72,12 @@ export default function ContributeQuestionCard({
Contribute Contribute
</h1> </h1>
</div> </div>
<ContributeQuestionDialog
show={showDraftDialog}
onCancel={handleDraftDialogCancel}
onSubmit={onSubmit}
/>
</button> </button>
<ContributeQuestionDialog
show={showDraftDialog}
onCancel={handleDraftDialogCancel}
onSubmit={onSubmit}
/>
</div> </div>
); );
} }

@ -1,13 +1,21 @@
import { ROLES } from '~/utils/questions/constants'; import { JobTitleLabels } from '~/components/shared/JobTitles';
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead'; import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
import ExpandedTypeahead from './ExpandedTypeahead'; import ExpandedTypeahead from './ExpandedTypeahead';
import type { FilterChoices } from '../filter/FilterSection';
export type RoleTypeaheadProps = Omit< export type RoleTypeaheadProps = Omit<
ExpandedTypeaheadProps, ExpandedTypeaheadProps,
'label' | 'onQueryChange' | 'options' 'label' | 'onQueryChange' | 'options'
>; >;
const ROLES: FilterChoices = Object.entries(JobTitleLabels).map(
([slug, label]) => ({
id: slug,
label,
value: slug,
}),
);
export default function RoleTypeahead(props: RoleTypeaheadProps) { export default function RoleTypeahead(props: RoleTypeaheadProps) {
return ( return (
<ExpandedTypeahead <ExpandedTypeahead

@ -1,6 +1,6 @@
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useMemo, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline'; import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
import { Button, Collapsible, HorizontalDivider, TextArea } from '@tih/ui'; import { Button, Collapsible, HorizontalDivider, TextArea } from '@tih/ui';
@ -13,6 +13,7 @@ import SortOptionsSelect from '~/components/questions/SortOptionsSelect';
import { APP_TITLE } from '~/utils/questions/constants'; import { APP_TITLE } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug'; import createSlug from '~/utils/questions/createSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { useFormRegister } from '~/utils/questions/useFormRegister'; import { useFormRegister } from '~/utils/questions/useFormRegister';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
@ -69,6 +70,14 @@ export default function QuestionPage() {
{ questionId: questionId as string }, { questionId: questionId as string },
]); ]);
const relabeledAggregatedEncounters = useMemo(() => {
if (!aggregatedEncounters) {
return aggregatedEncounters;
}
return relabelQuestionAggregates(aggregatedEncounters);
}, [aggregatedEncounters]);
const utils = trpc.useContext(); const utils = trpc.useContext();
const { data: commentData } = trpc.useInfiniteQuery( const { data: commentData } = trpc.useInfiniteQuery(
@ -175,11 +184,11 @@ export default function QuestionPage() {
<div className="flex max-w-7xl flex-1 flex-col gap-2"> <div className="flex max-w-7xl flex-1 flex-col gap-2">
<FullQuestionCard <FullQuestionCard
{...question} {...question}
companies={aggregatedEncounters?.companyCounts ?? {}} companies={relabeledAggregatedEncounters?.companyCounts ?? {}}
locations={aggregatedEncounters?.locationCounts ?? {}} locations={relabeledAggregatedEncounters?.locationCounts ?? {}}
questionId={question.id} questionId={question.id}
receivedCount={undefined} receivedCount={undefined}
roles={aggregatedEncounters?.roleCounts ?? {}} roles={relabeledAggregatedEncounters?.roleCounts ?? {}}
timestamp={question.seenAt.toLocaleDateString(undefined, { timestamp={question.seenAt.toLocaleDateString(undefined, {
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',

@ -14,11 +14,13 @@ import QuestionSearchBar from '~/components/questions/QuestionSearchBar';
import CompanyTypeahead from '~/components/questions/typeahead/CompanyTypeahead'; import CompanyTypeahead from '~/components/questions/typeahead/CompanyTypeahead';
import LocationTypeahead from '~/components/questions/typeahead/LocationTypeahead'; import LocationTypeahead from '~/components/questions/typeahead/LocationTypeahead';
import RoleTypeahead from '~/components/questions/typeahead/RoleTypeahead'; import RoleTypeahead from '~/components/questions/typeahead/RoleTypeahead';
import { JobTitleLabels } from '~/components/shared/JobTitles';
import type { QuestionAge } from '~/utils/questions/constants'; import type { QuestionAge } from '~/utils/questions/constants';
import { APP_TITLE } from '~/utils/questions/constants'; import { APP_TITLE } from '~/utils/questions/constants';
import { QUESTION_AGES, QUESTION_TYPES } from '~/utils/questions/constants'; import { QUESTION_AGES, QUESTION_TYPES } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug'; import createSlug from '~/utils/questions/createSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { import {
useSearchParam, useSearchParam,
useSearchParamSingle, useSearchParamSingle,
@ -174,7 +176,7 @@ export default function QuestionsBrowsePage() {
return undefined; return undefined;
} }
return questionsQueryData.pages.reduce( return questionsQueryData.pages.reduce(
(acc, page) => acc + page.data.length, (acc, page) => acc + (page.data.length as number),
0, 0,
); );
}, [questionsQueryData]); }, [questionsQueryData]);
@ -273,7 +275,7 @@ export default function QuestionsBrowsePage() {
return selectedRoles.map((role) => ({ return selectedRoles.map((role) => ({
checked: true, checked: true,
id: role, id: role,
label: role, label: JobTitleLabels[role as keyof typeof JobTitleLabels],
value: role, value: role,
})); }));
}, [selectedRoles]); }, [selectedRoles]);
@ -369,7 +371,7 @@ export default function QuestionsBrowsePage() {
setSelectedRoles([...selectedRoles, option.value]); setSelectedRoles([...selectedRoles, option.value]);
} else { } else {
setSelectedRoles( setSelectedRoles(
selectedCompanies.filter((role) => role !== option.value), selectedRoles.filter((role) => role !== option.value),
); );
} }
}} }}
@ -443,20 +445,20 @@ export default function QuestionsBrowsePage() {
<main className="flex flex-1 flex-col items-stretch"> <main className="flex flex-1 flex-col items-stretch">
<div className="flex h-full flex-1"> <div className="flex h-full flex-1">
<section className="flex min-h-0 flex-1 flex-col items-center overflow-auto"> <section className="flex min-h-0 flex-1 flex-col items-center overflow-auto">
<div className="flex min-h-0 max-w-3xl flex-1 p-4"> <div className="m-4 flex max-w-3xl flex-1 flex-col items-stretch justify-start gap-8">
<div className="flex flex-1 flex-col items-stretch justify-start gap-8"> <ContributeQuestionCard
<ContributeQuestionCard onSubmit={(data) => {
onSubmit={(data) => { createQuestion({
createQuestion({ companyId: data.company,
companyId: data.company, content: data.questionContent,
content: data.questionContent, location: data.location,
location: data.location, questionType: data.questionType,
questionType: data.questionType, role: data.role,
role: data.role, seenAt: data.date,
seenAt: data.date, });
}); }}
}} />
/> <div className="sticky top-0 border-b border-slate-300 bg-slate-50 py-4">
<QuestionSearchBar <QuestionSearchBar
sortOrderValue={sortOrder} sortOrderValue={sortOrder}
sortTypeValue={sortType} sortTypeValue={sortType}
@ -466,28 +468,29 @@ export default function QuestionsBrowsePage() {
onSortOrderChange={setSortOrder} onSortOrderChange={setSortOrder}
onSortTypeChange={setSortType} onSortTypeChange={setSortType}
/> />
<div className="flex flex-col gap-2 pb-4"> </div>
{(questionsQueryData?.pages ?? []).flatMap( <div className="flex flex-col gap-2 pb-4">
({ data: questions }) => {(questionsQueryData?.pages ?? []).flatMap(
questions.map((question) => ( ({ data: questions }) =>
questions.map((question) => {
const { companyCounts, locationCounts, roleCounts } =
relabelQuestionAggregates(
question.aggregatedQuestionEncounters,
);
return (
<QuestionOverviewCard <QuestionOverviewCard
key={question.id} key={question.id}
answerCount={question.numAnswers} answerCount={question.numAnswers}
companies={ companies={companyCounts}
question.aggregatedQuestionEncounters.companyCounts
}
content={question.content} content={question.content}
href={`/questions/${question.id}/${createSlug( href={`/questions/${question.id}/${createSlug(
question.content, question.content,
)}`} )}`}
locations={ locations={locationCounts}
question.aggregatedQuestionEncounters.locationCounts
}
questionId={question.id} questionId={question.id}
receivedCount={question.receivedCount} receivedCount={question.receivedCount}
roles={ roles={roleCounts}
question.aggregatedQuestionEncounters.roleCounts
}
timestamp={question.seenAt.toLocaleDateString( timestamp={question.seenAt.toLocaleDateString(
undefined, undefined,
{ {
@ -498,25 +501,25 @@ export default function QuestionsBrowsePage() {
type={question.type} type={question.type}
upvoteCount={question.numVotes} upvoteCount={question.numVotes}
/> />
)), );
)} }),
<Button )}
disabled={!hasNextPage || isFetchingNextPage} <Button
isLoading={isFetchingNextPage} disabled={!hasNextPage || isFetchingNextPage}
label={hasNextPage ? 'Load more' : 'Nothing more to load'} isLoading={isFetchingNextPage}
variant="tertiary" label={hasNextPage ? 'Load more' : 'Nothing more to load'}
onClick={() => { variant="tertiary"
fetchNextPage(); onClick={() => {
}} fetchNextPage();
/> }}
{questionCount === 0 && ( />
<div className="flex w-full items-center justify-center gap-2 rounded-md border border-slate-300 bg-slate-200 p-4 text-slate-600"> {questionCount === 0 && (
<NoSymbolIcon className="h-6 w-6" /> <div className="flex w-full items-center justify-center gap-2 rounded-md border border-slate-300 bg-slate-200 p-4 text-slate-600">
<p>Nothing found.</p> <NoSymbolIcon className="h-6 w-6" />
{hasFilters && <p>Try changing your search criteria.</p>} <p>Nothing found.</p>
</div> {hasFilters && <p>Try changing your search criteria.</p>}
)} </div>
</div> )}
</div> </div>
</div> </div>
</section> </section>

@ -15,6 +15,7 @@ import DeleteListDialog from '~/components/questions/DeleteListDialog';
import { Button } from '~/../../../packages/ui/dist'; import { Button } from '~/../../../packages/ui/dist';
import { APP_TITLE } from '~/utils/questions/constants'; import { APP_TITLE } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug'; import createSlug from '~/utils/questions/createSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
export default function ListPage() { export default function ListPage() {
@ -172,37 +173,38 @@ export default function ListPage() {
{lists?.[selectedListIndex] && ( {lists?.[selectedListIndex] && (
<div className="flex flex-col gap-4 pb-4"> <div className="flex flex-col gap-4 pb-4">
{lists[selectedListIndex].questionEntries.map( {lists[selectedListIndex].questionEntries.map(
({ question, id: entryId }) => ( ({ question, id: entryId }) => {
<QuestionListCard const { companyCounts, locationCounts, roleCounts } =
key={question.id} relabelQuestionAggregates(
companies={ question.aggregatedQuestionEncounters,
question.aggregatedQuestionEncounters.companyCounts );
}
content={question.content} return (
href={`/questions/${question.id}/${createSlug( <QuestionListCard
question.content, key={question.id}
)}`} companies={companyCounts}
locations={ content={question.content}
question.aggregatedQuestionEncounters.locationCounts href={`/questions/${question.id}/${createSlug(
} question.content,
questionId={question.id} )}`}
receivedCount={question.receivedCount} locations={locationCounts}
roles={ questionId={question.id}
question.aggregatedQuestionEncounters.roleCounts receivedCount={question.receivedCount}
} roles={roleCounts}
timestamp={question.seenAt.toLocaleDateString( timestamp={question.seenAt.toLocaleDateString(
undefined, undefined,
{ {
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',
}, },
)} )}
type={question.type} type={question.type}
onDelete={() => { onDelete={() => {
deleteQuestionEntry({ id: entryId }); deleteQuestionEntry({ id: entryId });
}} }}
/> />
), );
},
)} )}
{lists[selectedListIndex].questionEntries?.length === 0 && ( {lists[selectedListIndex].questionEntries?.length === 0 && (
<div className="flex w-full items-center justify-center gap-2 rounded-md border border-slate-300 bg-slate-200 p-4 text-slate-600"> <div className="flex w-full items-center justify-center gap-2 rounded-md border border-slate-300 bg-slate-200 p-4 text-slate-600">

@ -1,275 +0,0 @@
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import { createQuestionWithAggregateData } from '~/utils/questions/server/aggregate-encounters';
import { createProtectedRouter } from './context';
export const questionsListRouter = createProtectedRouter()
.query('getListsByUser', {
async resolve({ ctx }) {
const userId = ctx.session?.user?.id;
// TODO: Optimize by not returning question entries
const questionsLists = await ctx.prisma.questionsList.findMany({
include: {
questionEntries: {
include: {
question: {
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
company: true,
location: true,
role: true,
seenAt: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
},
},
},
},
orderBy: {
createdAt: 'asc',
},
where: {
userId,
},
});
const lists = questionsLists.map((list) => ({
...list,
questionEntries: list.questionEntries.map((entry) => ({
...entry,
question: createQuestionWithAggregateData(entry.question),
})),
}));
return lists;
},
})
.query('getListById', {
input: z.object({
listId: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { listId } = input;
const questionList = await ctx.prisma.questionsList.findFirst({
include: {
questionEntries: {
include: {
question: {
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
company: true,
location: true,
role: true,
seenAt: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
},
},
},
},
orderBy: {
createdAt: 'asc',
},
where: {
id: listId,
userId,
},
});
if (!questionList) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Question list not found',
});
}
return {
...questionList,
questionEntries: questionList.questionEntries.map((questionEntry) => ({
...questionEntry,
question: createQuestionWithAggregateData(questionEntry.question),
})),
};
},
})
.mutation('create', {
input: z.object({
name: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { name } = input;
return await ctx.prisma.questionsList.create({
data: {
name,
userId,
},
});
},
})
.mutation('update', {
input: z.object({
id: z.string(),
name: z.string().optional(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { name, id } = input;
const listToUpdate = await ctx.prisma.questionsList.findUnique({
where: {
id: input.id,
},
});
if (listToUpdate?.id !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
});
}
return await ctx.prisma.questionsList.update({
data: {
name,
},
where: {
id,
},
});
},
})
.mutation('delete', {
input: z.object({
id: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const listToDelete = await ctx.prisma.questionsList.findUnique({
where: {
id: input.id,
},
});
if (listToDelete?.userId !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
});
}
return await ctx.prisma.questionsList.delete({
where: {
id: input.id,
},
});
},
})
.mutation('createQuestionEntry', {
input: z.object({
listId: z.string(),
questionId: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const listToAugment = await ctx.prisma.questionsList.findUnique({
where: {
id: input.listId,
},
});
if (listToAugment?.userId !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
});
}
const { questionId, listId } = input;
return await ctx.prisma.questionsListQuestionEntry.create({
data: {
listId,
questionId,
},
});
},
})
.mutation('deleteQuestionEntry', {
input: z.object({
id: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const entryToDelete =
await ctx.prisma.questionsListQuestionEntry.findUnique({
where: {
id: input.id,
},
});
if (entryToDelete === null) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Entry not found.',
});
}
const listToAugment = await ctx.prisma.questionsList.findUnique({
where: {
id: entryToDelete.listId,
},
});
if (listToAugment?.userId !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
});
}
return await ctx.prisma.questionsListQuestionEntry.delete({
where: {
id: input.id,
},
});
},
});

@ -1,437 +0,0 @@
import { z } from 'zod';
import { QuestionsQuestionType, Vote } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import { createQuestionWithAggregateData } from '~/utils/questions/server/aggregate-encounters';
import { createProtectedRouter } from './context';
import { SortOrder, SortType } from '~/types/questions.d';
export const questionsQuestionRouter = createProtectedRouter()
.query('getQuestionsByFilter', {
input: z.object({
companyNames: z.string().array(),
cursor: z
.object({
idCursor: z.string().optional(),
lastSeenCursor: z.date().nullish().optional(),
upvoteCursor: z.number().optional(),
})
.nullish(),
endDate: z.date().default(new Date()),
limit: z.number().min(1).default(50),
locations: z.string().array(),
questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
roles: z.string().array(),
sortOrder: z.nativeEnum(SortOrder),
sortType: z.nativeEnum(SortType),
startDate: z.date().optional(),
}),
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,
},
];
const questionsData = await ctx.prisma.questionsQuestion.findMany({
cursor:
cursor !== undefined
? {
id: cursor ? cursor!.idCursor : undefined,
}
: undefined,
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
company: true,
location: true,
role: true,
seenAt: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
orderBy: sortCondition,
take: input.limit + 1,
where: {
...(input.questionTypes.length > 0
? {
questionType: {
in: input.questionTypes,
},
}
: {}),
encounters: {
some: {
seenAt: {
gte: input.startDate,
lte: input.endDate,
},
...(input.companyNames.length > 0
? {
company: {
name: {
in: input.companyNames,
},
},
}
: {}),
...(input.locations.length > 0
? {
location: {
in: input.locations,
},
}
: {}),
...(input.roles.length > 0
? {
role: {
in: input.roles,
},
}
: {}),
},
},
},
});
const processedQuestionsData = questionsData.map(
createQuestionWithAggregateData,
);
let nextCursor: typeof cursor | undefined = undefined;
if (questionsData.length > input.limit) {
const nextItem = questionsData.pop()!;
processedQuestionsData.pop();
const nextIdCursor: string | undefined = nextItem.id;
const nextLastSeenCursor =
input.sortType === SortType.NEW ? nextItem.lastSeenAt : undefined;
const nextUpvoteCursor =
input.sortType === SortType.TOP ? nextItem.upvotes : undefined;
nextCursor = {
idCursor: nextIdCursor,
lastSeenCursor: nextLastSeenCursor,
upvoteCursor: nextUpvoteCursor,
};
}
return {
data: processedQuestionsData,
nextCursor,
};
},
})
.query('getQuestionById', {
input: z.object({
id: z.string(),
}),
async resolve({ ctx, input }) {
const questionData = await ctx.prisma.questionsQuestion.findUnique({
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
company: true,
location: true,
role: true,
seenAt: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
where: {
id: input.id,
},
});
if (!questionData) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Question not found',
});
}
return createQuestionWithAggregateData(questionData);
},
})
.mutation('create', {
input: z.object({
companyId: z.string(),
content: z.string(),
location: z.string(),
questionType: z.nativeEnum(QuestionsQuestionType),
role: z.string(),
seenAt: z.date(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
return await ctx.prisma.questionsQuestion.create({
data: {
content: input.content,
encounters: {
create: {
company: {
connect: {
id: input.companyId,
},
},
location: input.location,
role: input.role,
seenAt: input.seenAt,
user: {
connect: {
id: userId,
},
},
},
},
lastSeenAt: input.seenAt,
questionType: input.questionType,
userId,
},
});
},
})
.mutation('update', {
input: z.object({
content: z.string().optional(),
id: z.string(),
questionType: z.nativeEnum(QuestionsQuestionType).optional(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const questionToUpdate = await ctx.prisma.questionsQuestion.findUnique({
where: {
id: input.id,
},
});
if (questionToUpdate?.id !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
// Optional: pass the original error to retain stack trace
});
}
const { content, questionType } = input;
return await ctx.prisma.questionsQuestion.update({
data: {
content,
questionType,
},
where: {
id: input.id,
},
});
},
})
.mutation('delete', {
input: z.object({
id: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const questionToDelete = await ctx.prisma.questionsQuestion.findUnique({
where: {
id: input.id,
},
});
if (questionToDelete?.id !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
// Optional: pass the original error to retain stack trace
});
}
return await ctx.prisma.questionsQuestion.delete({
where: {
id: input.id,
},
});
},
})
.query('getVote', {
input: z.object({
questionId: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { questionId } = input;
return await ctx.prisma.questionsQuestionVote.findUnique({
where: {
questionId_userId: { questionId, userId },
},
});
},
})
.mutation('createVote', {
input: z.object({
questionId: z.string(),
vote: z.nativeEnum(Vote),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { questionId, vote } = input;
const incrementValue = vote === Vote.UPVOTE ? 1 : -1;
const [questionVote] = await ctx.prisma.$transaction([
ctx.prisma.questionsQuestionVote.create({
data: {
questionId,
userId,
vote,
},
}),
ctx.prisma.questionsQuestion.update({
data: {
upvotes: {
increment: incrementValue,
},
},
where: {
id: questionId,
},
}),
]);
return questionVote;
},
})
.mutation('updateVote', {
input: z.object({
id: z.string(),
vote: z.nativeEnum(Vote),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { id, vote } = input;
const voteToUpdate = await ctx.prisma.questionsQuestionVote.findUnique({
where: {
id: input.id,
},
});
if (voteToUpdate?.userId !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
});
}
const incrementValue = vote === Vote.UPVOTE ? 2 : -2;
const [questionVote] = await ctx.prisma.$transaction([
ctx.prisma.questionsQuestionVote.update({
data: {
vote,
},
where: {
id,
},
}),
ctx.prisma.questionsQuestion.update({
data: {
upvotes: {
increment: incrementValue,
},
},
where: {
id: voteToUpdate.questionId,
},
}),
]);
return questionVote;
},
})
.mutation('deleteVote', {
input: z.object({
id: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const voteToDelete = await ctx.prisma.questionsQuestionVote.findUnique({
where: {
id: input.id,
},
});
if (voteToDelete?.userId !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
});
}
const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1;
const [questionVote] = await ctx.prisma.$transaction([
ctx.prisma.questionsQuestionVote.delete({
where: {
id: input.id,
},
}),
ctx.prisma.questionsQuestion.update({
data: {
upvotes: {
increment: incrementValue,
},
},
where: {
id: voteToDelete.questionId,
},
}),
]);
return questionVote;
},
});

@ -127,7 +127,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
where: { where: {
answerCommentId_userId: { answerCommentId, userId }, answerCommentId_userId: { answerCommentId, userId },
}, },
}) });
if (vote === null) { if (vote === null) {
const createdVote = await tx.questionsAnswerCommentVote.create({ const createdVote = await tx.questionsAnswerCommentVote.create({
@ -149,7 +149,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return createdVote;
} }
if (vote!.userId !== userId) { if (vote!.userId !== userId) {
@ -164,18 +164,15 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
} }
if (vote.vote === Vote.DOWNVOTE) { if (vote.vote === Vote.DOWNVOTE) {
tx.questionsAnswerCommentVote.delete({ const updatedVote = await tx.questionsAnswerCommentVote.update({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsAnswerCommentVote.create({
data: { data: {
answerCommentId, answerCommentId,
userId, userId,
vote: Vote.UPVOTE, vote: Vote.UPVOTE,
}, },
where: {
id: vote.id,
},
}); });
await tx.questionsAnswerComment.update({ await tx.questionsAnswerComment.update({
@ -189,7 +186,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return updatedVote;
} }
}); });
}, },
@ -221,7 +218,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
where: { where: {
answerCommentId_userId: { answerCommentId, userId }, answerCommentId_userId: { answerCommentId, userId },
}, },
}) });
if (vote === null) { if (vote === null) {
const createdVote = await tx.questionsAnswerCommentVote.create({ const createdVote = await tx.questionsAnswerCommentVote.create({
@ -243,7 +240,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return createdVote;
} }
if (vote!.userId !== userId) { if (vote!.userId !== userId) {
@ -258,18 +255,15 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
} }
if (vote.vote === Vote.UPVOTE) { if (vote.vote === Vote.UPVOTE) {
tx.questionsAnswerCommentVote.delete({ const updatedVote = await tx.questionsAnswerCommentVote.update({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsAnswerCommentVote.create({
data: { data: {
answerCommentId, answerCommentId,
userId, userId,
vote: Vote.DOWNVOTE, vote: Vote.DOWNVOTE,
}, },
where: {
id: vote.id,
},
}); });
await tx.questionsAnswerComment.update({ await tx.questionsAnswerComment.update({
@ -283,7 +277,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return updatedVote;
} }
}); });
}, },
@ -315,7 +309,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
where: { where: {
answerCommentId_userId: { answerCommentId, userId }, answerCommentId_userId: { answerCommentId, userId },
}, },
}) });
if (voteToDelete === null) { if (voteToDelete === null) {
return null; return null;
@ -330,7 +324,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1; const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1;
tx.questionsAnswerCommentVote.delete({ await tx.questionsAnswerCommentVote.delete({
where: { where: {
id: voteToDelete.id, id: voteToDelete.id,
}, },
@ -350,4 +344,4 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
return voteToDelete; return voteToDelete;
}); });
}, },
}); });

@ -107,12 +107,11 @@ export const questionsAnswerUserRouter = createProtectedRouter()
const { answerId } = input; const { answerId } = input;
return await ctx.prisma.$transaction(async (tx) => { return await ctx.prisma.$transaction(async (tx) => {
const answerToUpdate = const answerToUpdate = await tx.questionsAnswer.findUnique({
await tx.questionsAnswer.findUnique({ where: {
where: { id: answerId,
id: answerId, },
}, });
});
if (answerToUpdate === null) { if (answerToUpdate === null) {
throw new TRPCError({ throw new TRPCError({
@ -125,7 +124,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
where: { where: {
answerId_userId: { answerId, userId }, answerId_userId: { answerId, userId },
}, },
}) });
if (vote === null) { if (vote === null) {
const createdVote = await tx.questionsAnswerVote.create({ const createdVote = await tx.questionsAnswerVote.create({
@ -147,7 +146,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return createdVote;
} }
if (vote!.userId !== userId) { if (vote!.userId !== userId) {
@ -162,18 +161,15 @@ export const questionsAnswerUserRouter = createProtectedRouter()
} }
if (vote.vote === Vote.DOWNVOTE) { if (vote.vote === Vote.DOWNVOTE) {
tx.questionsAnswerVote.delete({ const updatedVote = await tx.questionsAnswerVote.update({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsAnswerVote.create({
data: { data: {
answerId, answerId,
userId, userId,
vote: Vote.UPVOTE, vote: Vote.UPVOTE,
}, },
where: {
id: vote.id,
},
}); });
await tx.questionsAnswer.update({ await tx.questionsAnswer.update({
@ -187,7 +183,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return updatedVote;
} }
}); });
}, },
@ -201,12 +197,11 @@ export const questionsAnswerUserRouter = createProtectedRouter()
const { answerId } = input; const { answerId } = input;
return await ctx.prisma.$transaction(async (tx) => { return await ctx.prisma.$transaction(async (tx) => {
const answerToUpdate = const answerToUpdate = await tx.questionsAnswer.findUnique({
await tx.questionsAnswer.findUnique({ where: {
where: { id: answerId,
id: answerId, },
}, });
});
if (answerToUpdate === null) { if (answerToUpdate === null) {
throw new TRPCError({ throw new TRPCError({
@ -219,7 +214,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
where: { where: {
answerId_userId: { answerId, userId }, answerId_userId: { answerId, userId },
}, },
}) });
if (vote === null) { if (vote === null) {
const createdVote = await tx.questionsAnswerVote.create({ const createdVote = await tx.questionsAnswerVote.create({
@ -241,7 +236,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return createdVote;
} }
if (vote!.userId !== userId) { if (vote!.userId !== userId) {
@ -256,18 +251,15 @@ export const questionsAnswerUserRouter = createProtectedRouter()
} }
if (vote.vote === Vote.UPVOTE) { if (vote.vote === Vote.UPVOTE) {
tx.questionsAnswerVote.delete({ const updatedVote = await tx.questionsAnswerVote.update({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsAnswerVote.create({
data: { data: {
answerId, answerId,
userId, userId,
vote: Vote.DOWNVOTE, vote: Vote.DOWNVOTE,
}, },
where: {
id: vote.id,
},
}); });
await tx.questionsAnswer.update({ await tx.questionsAnswer.update({
@ -281,7 +273,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return updatedVote;
} }
}); });
}, },
@ -295,12 +287,11 @@ export const questionsAnswerUserRouter = createProtectedRouter()
const { answerId } = input; const { answerId } = input;
return await ctx.prisma.$transaction(async (tx) => { return await ctx.prisma.$transaction(async (tx) => {
const answerToUpdate = const answerToUpdate = await tx.questionsAnswer.findUnique({
await tx.questionsAnswer.findUnique({ where: {
where: { id: answerId,
id: answerId, },
}, });
});
if (answerToUpdate === null) { if (answerToUpdate === null) {
throw new TRPCError({ throw new TRPCError({
@ -313,7 +304,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
where: { where: {
answerId_userId: { answerId, userId }, answerId_userId: { answerId, userId },
}, },
}) });
if (voteToDelete === null) { if (voteToDelete === null) {
return null; return null;
@ -328,7 +319,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1; const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1;
tx.questionsAnswerVote.delete({ await tx.questionsAnswerVote.delete({
where: { where: {
id: voteToDelete.id, id: voteToDelete.id,
}, },

@ -128,7 +128,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
where: { where: {
questionCommentId_userId: { questionCommentId, userId }, questionCommentId_userId: { questionCommentId, userId },
}, },
}) });
if (vote === null) { if (vote === null) {
const createdVote = await tx.questionsQuestionCommentVote.create({ const createdVote = await tx.questionsQuestionCommentVote.create({
@ -150,7 +150,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return createdVote;
} }
if (vote!.userId !== userId) { if (vote!.userId !== userId) {
@ -165,18 +165,15 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
} }
if (vote.vote === Vote.DOWNVOTE) { if (vote.vote === Vote.DOWNVOTE) {
tx.questionsQuestionCommentVote.delete({ const updatedVote = await tx.questionsQuestionCommentVote.update({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsQuestionCommentVote.create({
data: { data: {
questionCommentId, questionCommentId,
userId, userId,
vote: Vote.UPVOTE, vote: Vote.UPVOTE,
}, },
where: {
id: vote.id,
},
}); });
await tx.questionsQuestionComment.update({ await tx.questionsQuestionComment.update({
@ -190,7 +187,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return updatedVote;
} }
}); });
}, },
@ -222,7 +219,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
where: { where: {
questionCommentId_userId: { questionCommentId, userId }, questionCommentId_userId: { questionCommentId, userId },
}, },
}) });
if (vote === null) { if (vote === null) {
const createdVote = await tx.questionsQuestionCommentVote.create({ const createdVote = await tx.questionsQuestionCommentVote.create({
@ -244,7 +241,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return createdVote;
} }
if (vote!.userId !== userId) { if (vote!.userId !== userId) {
@ -284,7 +281,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return createdVote;
} }
}); });
}, },
@ -316,7 +313,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
where: { where: {
questionCommentId_userId: { questionCommentId, userId }, questionCommentId_userId: { questionCommentId, userId },
}, },
}) });
if (voteToDelete === null) { if (voteToDelete === null) {
return null; return null;
@ -331,7 +328,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1; const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1;
tx.questionsQuestionCommentVote.delete({ await tx.questionsQuestionCommentVote.delete({
where: { where: {
id: voteToDelete.id, id: voteToDelete.id,
}, },

@ -182,4 +182,64 @@ export const questionsQuestionRouter = createRouter()
return createQuestionWithAggregateData(questionData); return createQuestionWithAggregateData(questionData);
}, },
})
.query('getRelatedQuestions', {
input: z.object({
content: z.string(),
}),
async resolve({ ctx, input }) {
const escapeChars = /[()|&:*!]/g;
const query =
input.content
.replace(escapeChars, " ")
.trim()
.split(/\s+/)
.join(" | ");
const relatedQuestionsId : Array<{id:string}> = await ctx.prisma.$queryRaw`
SELECT id FROM "QuestionsQuestion"
WHERE
to_tsvector("content") @@ to_tsquery('english', ${query})
ORDER BY ts_rank_cd(to_tsvector("content"), to_tsquery('english', ${query}), 4) DESC;
`;
const relatedQuestionsIdArray = relatedQuestionsId.map(current => current.id);
const relatedQuestionsData = await ctx.prisma.questionsQuestion.findMany({
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
company: true,
location: true,
role: true,
seenAt: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
where: {
id : {
in : relatedQuestionsIdArray,
}
},
});
const processedQuestionsData = relatedQuestionsData.map(
createQuestionWithAggregateData,
);
return processedQuestionsData;
}
}); });

@ -132,12 +132,11 @@ export const questionsQuestionUserRouter = createProtectedRouter()
const { questionId } = input; const { questionId } = input;
return await ctx.prisma.$transaction(async (tx) => { return await ctx.prisma.$transaction(async (tx) => {
const questionToUpdate = const questionToUpdate = await tx.questionsQuestion.findUnique({
await tx.questionsQuestion.findUnique({ where: {
where: { id: questionId,
id: questionId, },
}, });
});
if (questionToUpdate === null) { if (questionToUpdate === null) {
throw new TRPCError({ throw new TRPCError({
@ -150,7 +149,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
where: { where: {
questionId_userId: { questionId, userId }, questionId_userId: { questionId, userId },
}, },
}) });
if (vote === null) { if (vote === null) {
const createdVote = await tx.questionsQuestionVote.create({ const createdVote = await tx.questionsQuestionVote.create({
@ -172,7 +171,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return createdVote;
} }
if (vote!.userId !== userId) { if (vote!.userId !== userId) {
@ -187,18 +186,15 @@ export const questionsQuestionUserRouter = createProtectedRouter()
} }
if (vote.vote === Vote.DOWNVOTE) { if (vote.vote === Vote.DOWNVOTE) {
tx.questionsQuestionVote.delete({ const updatedVote = await tx.questionsQuestionVote.update({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsQuestionVote.create({
data: { data: {
questionId, questionId,
userId, userId,
vote: Vote.UPVOTE, vote: Vote.UPVOTE,
}, },
where: {
id: vote.id,
},
}); });
await tx.questionsQuestion.update({ await tx.questionsQuestion.update({
@ -212,7 +208,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return updatedVote;
} }
}); });
}, },
@ -226,12 +222,11 @@ export const questionsQuestionUserRouter = createProtectedRouter()
const { questionId } = input; const { questionId } = input;
return await ctx.prisma.$transaction(async (tx) => { return await ctx.prisma.$transaction(async (tx) => {
const questionToUpdate = const questionToUpdate = await tx.questionsQuestion.findUnique({
await tx.questionsQuestion.findUnique({ where: {
where: { id: questionId,
id: questionId, },
}, });
});
if (questionToUpdate === null) { if (questionToUpdate === null) {
throw new TRPCError({ throw new TRPCError({
@ -244,7 +239,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
where: { where: {
questionId_userId: { questionId, userId }, questionId_userId: { questionId, userId },
}, },
}) });
if (vote === null) { if (vote === null) {
const createdVote = await tx.questionsQuestionVote.create({ const createdVote = await tx.questionsQuestionVote.create({
@ -266,7 +261,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return createdVote;
} }
if (vote!.userId !== userId) { if (vote!.userId !== userId) {
@ -276,23 +271,20 @@ export const questionsQuestionUserRouter = createProtectedRouter()
}); });
} }
if (vote!.vote === Vote.DOWNVOTE) { if (vote.vote === Vote.DOWNVOTE) {
return vote; return vote;
} }
if (vote.vote === Vote.UPVOTE) { if (vote.vote === Vote.UPVOTE) {
tx.questionsQuestionVote.delete({ const updatedVote = await tx.questionsQuestionVote.update({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsQuestionVote.create({
data: { data: {
questionId, questionId,
userId, userId,
vote: Vote.DOWNVOTE, vote: Vote.DOWNVOTE,
}, },
where: {
id: vote.id,
},
}); });
await tx.questionsQuestion.update({ await tx.questionsQuestion.update({
@ -306,7 +298,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
}, },
}); });
return createdVote return updatedVote;
} }
}); });
}, },
@ -320,12 +312,11 @@ export const questionsQuestionUserRouter = createProtectedRouter()
const { questionId } = input; const { questionId } = input;
return await ctx.prisma.$transaction(async (tx) => { return await ctx.prisma.$transaction(async (tx) => {
const questionToUpdate = const questionToUpdate = await tx.questionsQuestion.findUnique({
await tx.questionsQuestion.findUnique({ where: {
where: { id: questionId,
id: questionId, },
}, });
});
if (questionToUpdate === null) { if (questionToUpdate === null) {
throw new TRPCError({ throw new TRPCError({
@ -338,7 +329,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
where: { where: {
questionId_userId: { questionId, userId }, questionId_userId: { questionId, userId },
}, },
}) });
if (voteToDelete === null) { if (voteToDelete === null) {
return null; return null;
@ -353,7 +344,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1; const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1;
tx.questionsQuestionVote.delete({ await tx.questionsQuestionVote.delete({
where: { where: {
id: voteToDelete.id, id: voteToDelete.id,
}, },

@ -0,0 +1,26 @@
import { JobTitleLabels } from '~/components/shared/JobTitles';
import type { AggregatedQuestionEncounter } from '~/types/questions';
export default function relabelQuestionAggregates({
locationCounts,
companyCounts,
roleCounts,
latestSeenAt,
}: AggregatedQuestionEncounter) {
const newRoleCounts = Object.fromEntries(
Object.entries(roleCounts).map(([roleId, count]) => [
JobTitleLabels[roleId as keyof typeof JobTitleLabels],
count,
]),
);
const relabeledAggregate: AggregatedQuestionEncounter = {
companyCounts,
latestSeenAt,
locationCounts,
roleCounts: newRoleCounts,
};
return relabeledAggregate;
}

@ -5,9 +5,9 @@ import type { Vote } from '@prisma/client';
import { trpc } from '../trpc'; import { trpc } from '../trpc';
type UseVoteOptions = { type UseVoteOptions = {
createVote: (opts: { vote: Vote }) => void; setDownVote: () => void;
deleteVote: (opts: { id: string }) => void; setNoVote: () => void;
updateVote: (opts: BackendVote) => void; setUpVote: () => void;
}; };
type BackendVote = { type BackendVote = {
@ -19,47 +19,23 @@ const createVoteCallbacks = (
vote: BackendVote | null, vote: BackendVote | null,
opts: UseVoteOptions, opts: UseVoteOptions,
) => { ) => {
const { createVote, updateVote, deleteVote } = opts; const { setDownVote, setNoVote, setUpVote } = opts;
const handleUpvote = () => { const handleUpvote = () => {
// Either upvote or remove upvote // Either upvote or remove upvote
if (vote) { if (vote && vote.vote === 'UPVOTE') {
if (vote.vote === 'DOWNVOTE') { setNoVote();
updateVote({
id: vote.id,
vote: 'UPVOTE',
});
} else {
deleteVote({
id: vote.id,
});
}
// Update vote to an upvote
} else { } else {
createVote({ setUpVote();
vote: 'UPVOTE',
});
} }
}; };
const handleDownvote = () => { const handleDownvote = () => {
// Either downvote or remove downvote // Either downvote or remove downvote
if (vote) { if (vote && vote.vote === 'DOWNVOTE') {
if (vote.vote === 'UPVOTE') { setNoVote();
updateVote({
id: vote.id,
vote: 'DOWNVOTE',
});
} else {
deleteVote({
id: vote.id,
});
}
// Update vote to an upvote
} else { } else {
createVote({ setDownVote();
vote: 'DOWNVOTE',
});
} }
}; };
@ -71,61 +47,61 @@ type QueryKey = Parameters<typeof trpc.useQuery>[0][0];
export const useQuestionVote = (id: string) => { export const useQuestionVote = (id: string) => {
return useVote(id, { return useVote(id, {
create: 'questions.questions.user.createVote',
deleteKey: 'questions.questions.user.deleteVote',
idKey: 'questionId', idKey: 'questionId',
invalidateKeys: [ invalidateKeys: [
'questions.questions.getQuestionsByFilter', 'questions.questions.getQuestionsByFilter',
'questions.questions.getQuestionById', 'questions.questions.getQuestionById',
], ],
query: 'questions.questions.user.getVote', query: 'questions.questions.user.getVote',
update: 'questions.questions.user.updateVote', setDownVoteKey: 'questions.questions.user.setDownVote',
setNoVoteKey: 'questions.questions.user.setNoVote',
setUpVoteKey: 'questions.questions.user.setUpVote',
}); });
}; };
export const useAnswerVote = (id: string) => { export const useAnswerVote = (id: string) => {
return useVote(id, { return useVote(id, {
create: 'questions.answers.user.createVote',
deleteKey: 'questions.answers.user.deleteVote',
idKey: 'answerId', idKey: 'answerId',
invalidateKeys: [ invalidateKeys: [
'questions.answers.getAnswers', 'questions.answers.getAnswers',
'questions.answers.getAnswerById', 'questions.answers.getAnswerById',
], ],
query: 'questions.answers.user.getVote', query: 'questions.answers.user.getVote',
update: 'questions.answers.user.updateVote', setDownVoteKey: 'questions.answers.user.setDownVote',
setNoVoteKey: 'questions.answers.user.setNoVote',
setUpVoteKey: 'questions.answers.user.setUpVote',
}); });
}; };
export const useQuestionCommentVote = (id: string) => { export const useQuestionCommentVote = (id: string) => {
return useVote(id, { return useVote(id, {
create: 'questions.questions.comments.user.createVote',
deleteKey: 'questions.questions.comments.user.deleteVote',
idKey: 'questionCommentId', idKey: 'questionCommentId',
invalidateKeys: ['questions.questions.comments.getQuestionComments'], invalidateKeys: ['questions.questions.comments.getQuestionComments'],
query: 'questions.questions.comments.user.getVote', query: 'questions.questions.comments.user.getVote',
update: 'questions.questions.comments.user.updateVote', setDownVoteKey: 'questions.questions.comments.user.setDownVote',
setNoVoteKey: 'questions.questions.comments.user.setNoVote',
setUpVoteKey: 'questions.questions.comments.user.setUpVote',
}); });
}; };
export const useAnswerCommentVote = (id: string) => { export const useAnswerCommentVote = (id: string) => {
return useVote(id, { return useVote(id, {
create: 'questions.answers.comments.user.createVote',
deleteKey: 'questions.answers.comments.user.deleteVote',
idKey: 'answerCommentId', idKey: 'answerCommentId',
invalidateKeys: ['questions.answers.comments.getAnswerComments'], invalidateKeys: ['questions.answers.comments.getAnswerComments'],
query: 'questions.answers.comments.user.getVote', query: 'questions.answers.comments.user.getVote',
update: 'questions.answers.comments.user.updateVote', setDownVoteKey: 'questions.answers.comments.user.setDownVote',
setNoVoteKey: 'questions.answers.comments.user.setNoVote',
setUpVoteKey: 'questions.answers.comments.user.setUpVote',
}); });
}; };
type VoteProps<VoteQueryKey extends QueryKey = QueryKey> = { type VoteProps<VoteQueryKey extends QueryKey = QueryKey> = {
create: MutationKey;
deleteKey: MutationKey;
idKey: string; idKey: string;
invalidateKeys: Array<VoteQueryKey>; invalidateKeys: Array<VoteQueryKey>;
query: VoteQueryKey; query: VoteQueryKey;
update: MutationKey; setDownVoteKey: MutationKey;
setNoVoteKey: MutationKey;
setUpVoteKey: MutationKey;
}; };
type UseVoteMutationContext = { type UseVoteMutationContext = {
@ -137,7 +113,14 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
id: string, id: string,
opts: VoteProps<VoteQueryKey>, opts: VoteProps<VoteQueryKey>,
) => { ) => {
const { create, deleteKey, query, update, idKey, invalidateKeys } = opts; const {
idKey,
invalidateKeys,
query,
setDownVoteKey,
setNoVoteKey,
setUpVoteKey,
} = opts;
const utils = trpc.useContext(); const utils = trpc.useContext();
const onVoteUpdate = useCallback(() => { const onVoteUpdate = useCallback(() => {
@ -157,8 +140,8 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
const backendVote = data as BackendVote; const backendVote = data as BackendVote;
const { mutate: createVote } = trpc.useMutation<any, UseVoteMutationContext>( const { mutate: setUpVote } = trpc.useMutation<any, UseVoteMutationContext>(
create, setUpVoteKey,
{ {
onError: (err, variables, context) => { onError: (err, variables, context) => {
if (context !== undefined) { if (context !== undefined) {
@ -185,8 +168,8 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
onSettled: onVoteUpdate, onSettled: onVoteUpdate,
}, },
); );
const { mutate: updateVote } = trpc.useMutation<any, UseVoteMutationContext>( const { mutate: setDownVote } = trpc.useMutation<any, UseVoteMutationContext>(
update, setDownVoteKey,
{ {
onError: (error, variables, context) => { onError: (error, variables, context) => {
if (context !== undefined) { if (context !== undefined) {
@ -214,8 +197,8 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
}, },
); );
const { mutate: deleteVote } = trpc.useMutation<any, UseVoteMutationContext>( const { mutate: setNoVote } = trpc.useMutation<any, UseVoteMutationContext>(
deleteKey, setNoVoteKey,
{ {
onError: (err, variables, context) => { onError: (err, variables, context) => {
if (context !== undefined) { if (context !== undefined) {
@ -242,14 +225,21 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
const { handleDownvote, handleUpvote } = createVoteCallbacks( const { handleDownvote, handleUpvote } = createVoteCallbacks(
backendVote ?? null, backendVote ?? null,
{ {
createVote: ({ vote }) => { setDownVote: () => {
createVote({ setDownVote({
[idKey]: id, [idKey]: id,
vote, });
} as any); },
setNoVote: () => {
setNoVote({
[idKey]: id,
});
},
setUpVote: () => {
setUpVote({
[idKey]: id,
});
}, },
deleteVote,
updateVote,
}, },
); );

Loading…
Cancel
Save