[questions][feat] integrate backend (#347)

pull/350/head
Jeff Sieu 2 years ago committed by GitHub
parent fecb470c99
commit e78160d654
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -204,8 +204,8 @@ model QuestionsQuestionEncounter {
userId String?
// TODO: sync with models
company String @db.Text
location String @db.Text
role String @db.Text
location String? @db.Text
role String? @db.Text
seenAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@ -7,8 +7,16 @@ import {
import { TextInput } from '@tih/ui';
import ContributeQuestionDialog from './ContributeQuestionDialog';
import type { ContributeQuestionFormProps } from './ContributeQuestionForm';
export default function ContributeQuestionCard() {
export type ContributeQuestionCardProps = Pick<
ContributeQuestionFormProps,
'onSubmit'
>;
export default function ContributeQuestionCard({
onSubmit,
}: ContributeQuestionCardProps) {
const [showDraftDialog, setShowDraftDialog] = useState(false);
const handleDraftDialogCancel = () => {
@ -68,6 +76,7 @@ export default function ContributeQuestionCard() {
<ContributeQuestionDialog
show={showDraftDialog}
onCancel={handleDraftDialogCancel}
onSubmit={onSubmit}
/>
</div>
);

@ -3,16 +3,21 @@ import { Dialog, Transition } from '@headlessui/react';
import { HorizontalDivider } from '~/../../../packages/ui/dist';
import type { ContributeQuestionFormProps } from './ContributeQuestionForm';
import ContributeQuestionForm from './ContributeQuestionForm';
import DiscardDraftDialog from './DiscardDraftDialog';
export type ContributeQuestionDialogProps = {
export type ContributeQuestionDialogProps = Pick<
ContributeQuestionFormProps,
'onSubmit'
> & {
onCancel: () => void;
show: boolean;
};
export default function ContributeQuestionDialog({
show,
onSubmit,
onCancel,
}: ContributeQuestionDialogProps) {
const [showDiscardDialog, setShowDiscardDialog] = useState(false);
@ -72,8 +77,7 @@ export default function ContributeQuestionDialog({
<ContributeQuestionForm
onDiscard={() => setShowDiscardDialog(true)}
onSubmit={(data) => {
// eslint-disable-next-line no-console
console.log(data);
onSubmit(data);
onCancel();
}}
/>

@ -5,6 +5,7 @@ import {
CalendarDaysIcon,
// UserIcon,
} from '@heroicons/react/24/outline';
import type { QuestionsQuestionType } from '@prisma/client';
import {
Button,
Collapsible,
@ -29,7 +30,7 @@ export type ContributeQuestionData = {
location: string;
position: string;
questionContent: string;
questionType: string;
questionType: QuestionsQuestionType;
};
export type ContributeQuestionFormProps = {
@ -87,7 +88,9 @@ export default function ContributeQuestionForm({
required={true}
startAddOn={CalendarDaysIcon}
startAddOnType="icon"
{...register('date')}
{...register('date', {
valueAsDate: true,
})}
/>
</div>
</div>

@ -0,0 +1,9 @@
import { Spinner } from '@tih/ui';
export default function FullScreenSpinner() {
return (
<div className="flex h-full w-full items-center justify-center">
<Spinner size="lg" />
</div>
);
}

@ -19,6 +19,7 @@ export type FullQuestionCardProps = UpvoteProps & {
receivedCount: number;
role: string;
timestamp: string;
type: string;
};
export default function FullQuestionCard({
@ -29,6 +30,7 @@ export default function FullQuestionCard({
timestamp,
role,
location,
type,
}: FullQuestionCardProps) {
const altText = company + ' logo';
return (
@ -41,7 +43,7 @@ export default function FullQuestionCard({
</div>
<div className="flex items-baseline justify-between">
<div className="flex items-center gap-2 text-slate-500">
<Badge label="Technical" variant="primary" />
<Badge label={type} variant="primary" />
<p className="text-xs">
{timestamp} · {location} · {role}
</p>
@ -53,14 +55,4 @@ export default function FullQuestionCard({
</div>
</article>
);
// Return href ? (
// <a
// className="ring-primary-500 rounded-md hover:bg-slate-50 focus:ring-2 focus-visible:outline-none active:bg-slate-100"
// href={href}>
// {mainCard}
// </a>
// ) : (
// mainCard
// );
}

@ -47,12 +47,14 @@ export type QuestionCardProps = ActionButtonProps &
receivedCount: number;
role: string;
timestamp: string;
type: string;
};
export default function QuestionCard({
answerCount,
content,
// ReceivedCount,
type,
showVoteButtons,
showUserStatistics,
showActionButton,
@ -69,7 +71,7 @@ export default function QuestionCard({
<div className="flex flex-col gap-2">
<div className="flex items-baseline justify-between">
<div className="flex items-center gap-2 text-slate-500">
<Badge label="Technical" variant="primary" />
<Badge label={type} variant="primary" />
<p className="text-xs">
{timestamp} · {location} · {role}
</p>

@ -4,13 +4,15 @@ import { Collapsible, TextInput } from '@tih/ui';
import Checkbox from '../ui-patch/Checkbox';
import RadioGroup from '../ui-patch/RadioGroup';
export type FilterOption = {
export type FilterOption<V extends string = string> = {
checked: boolean;
label: string;
value: string;
value: V;
};
export type FilterChoices = Array<Omit<FilterOption, 'checked'>>;
export type FilterChoices<V extends string = string> = Array<
Omit<FilterOption<V>, 'checked'>
>;
type FilterSectionType<FilterOptions extends Array<FilterOption>> =
| {

@ -5,13 +5,14 @@ import { Button, Select, TextArea } from '@tih/ui';
import FullAnswerCard from '~/components/questions/card/FullAnswerCard';
import CommentListItem from '~/components/questions/CommentListItem';
import FullScreenSpinner from '~/components/questions/FullScreenSpinner';
import {
SAMPLE_ANSWER,
SAMPLE_ANSWER_COMMENT,
SAMPLE_QUESTION,
} from '~/utils/questions/constants';
import { useFormRegister } from '~/utils/questions/useFormRegister';
import { trpc } from '~/utils/trpc';
export type AnswerCommentData = {
commentContent: string;
@ -22,22 +23,54 @@ export default function QuestionPage() {
const {
register: comRegister,
reset: resetComment,
handleSubmit: handleCommentSubmit,
formState: { isDirty: isCommentDirty, isValid: isCommentValid },
} = useForm<AnswerCommentData>({ mode: 'onChange' });
const commentRegister = useFormRegister(comRegister);
const question = SAMPLE_QUESTION;
const comment = SAMPLE_ANSWER_COMMENT;
const { answerId } = router.query;
const utils = trpc.useContext();
const { data: answer } = trpc.useQuery([
'questions.answers.getAnswerById',
{ answerId: answerId as string },
]);
const { data: comments } = trpc.useQuery([
'questions.answers.comments.getAnswerComments',
{ answerId: answerId as string },
]);
const { mutate: addComment } = trpc.useMutation(
'questions.answers.comments.create',
{
onSuccess: () => {
utils.invalidateQuery([
'questions.answers.comments.getAnswerComments',
{ answerId: answerId as string },
]);
},
},
);
const handleBackNavigation = () => {
router.back();
};
const handleSubmitComment = (data: AnswerCommentData) => {
// eslint-disable-next-line no-console
console.log(data);
resetComment();
addComment({
answerId: answerId as string,
content: data.commentContent,
});
};
if (!answer) {
return <FullScreenSpinner />;
}
return (
<div className="flex w-full flex-1 items-stretch pb-4">
<div className="flex items-baseline gap-2 py-4 pl-4">
@ -51,7 +84,13 @@ export default function QuestionPage() {
</div>
<div className="flex w-full justify-center overflow-y-auto py-4 px-5">
<div className="flex max-w-7xl flex-1 flex-col gap-2">
<FullAnswerCard {...SAMPLE_ANSWER} />
<FullAnswerCard
authorImageUrl={SAMPLE_ANSWER.authorImageUrl}
authorName={answer.user}
content={answer.content}
createdAt={answer.createdAt}
upvoteCount={0}
/>
<div className="mx-2">
<form
className="mb-2"
@ -89,7 +128,8 @@ export default function QuestionPage() {
onChange={(value) => {
// eslint-disable-next-line no-console
console.log(value);
}}></Select>
}}
/>
</div>
<Button
@ -101,11 +141,14 @@ export default function QuestionPage() {
</div>
</form>
{Array.from({ length: question.commentCount }).map((_, index) => (
{(comments ?? []).map((comment) => (
<CommentListItem
// eslint-disable-next-line react/no-array-index-key
key={index}
{...comment}
key={comment.id}
authorImageUrl={SAMPLE_ANSWER_COMMENT.authorImageUrl}
authorName={comment.user}
content={comment.content}
createdAt={comment.createdAt}
upvoteCount={comment.numVotes}
/>
))}
</div>

@ -6,13 +6,15 @@ import { Button, Collapsible, Select, TextArea } from '@tih/ui';
import AnswerCard from '~/components/questions/card/AnswerCard';
import FullQuestionCard from '~/components/questions/card/FullQuestionCard';
import CommentListItem from '~/components/questions/CommentListItem';
import FullScreenSpinner from '~/components/questions/FullScreenSpinner';
import {
SAMPLE_ANSWER,
SAMPLE_QUESTION,
SAMPLE_QUESTION_COMMENT,
} from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug';
import { useFormRegister } from '~/utils/questions/useFormRegister';
import { trpc } from '~/utils/trpc';
export type AnswerQuestionData = {
answerContent: string;
@ -34,26 +36,70 @@ export default function QuestionPage() {
const {
register: comRegister,
handleSubmit: handleCommentSubmit,
reset: resetComment,
formState: { isDirty: isCommentDirty, isValid: isCommentValid },
} = useForm<QuestionCommentData>({ mode: 'onChange' });
const commentRegister = useFormRegister(comRegister);
const question = SAMPLE_QUESTION;
const comment = SAMPLE_QUESTION_COMMENT;
const { questionId } = router.query;
const { data: question } = trpc.useQuery([
'questions.questions.getQuestionById',
{ id: questionId as string },
]);
const utils = trpc.useContext();
const { data: comments } = trpc.useQuery([
'questions.questions.comments.getQuestionComments',
{ questionId: questionId as string },
]);
const { mutate: addComment } = trpc.useMutation(
'questions.questions.comments.create',
{
onSuccess: () => {
utils.invalidateQueries(
'questions.questions.comments.getQuestionComments',
);
},
},
);
const { data: answers } = trpc.useQuery([
'questions.answers.getAnswers',
{ questionId: questionId as string },
]);
const { mutate: addAnswer } = trpc.useMutation('questions.answers.create', {
onSuccess: () => {
utils.invalidateQueries('questions.answers.getAnswers');
},
});
const handleBackNavigation = () => {
router.back();
};
const handleSubmitAnswer = (data: AnswerQuestionData) => {
// eslint-disable-next-line no-console
console.log(data);
addAnswer({
content: data.answerContent,
questionId: questionId as string,
});
};
const handleSubmitComment = (data: QuestionCommentData) => {
// eslint-disable-next-line no-console
console.log(data);
addComment({
content: data.commentContent,
questionId: questionId as string,
});
resetComment();
};
if (!question) {
return <FullScreenSpinner />;
}
return (
<div className="flex w-full flex-1 items-stretch pb-4">
<div className="flex items-baseline gap-2 py-4 pl-4">
@ -67,9 +113,15 @@ export default function QuestionPage() {
</div>
<div className="flex w-full justify-center overflow-y-auto py-4 px-5">
<div className="flex max-w-7xl flex-1 flex-col gap-2">
<FullQuestionCard {...question} showVoteButtons={true} />
<FullQuestionCard
{...question}
receivedCount={0} // TODO: Change to actual value
showVoteButtons={true}
timestamp={question.seenAt.toLocaleDateString()}
upvoteCount={question.numVotes}
/>
<div className="mx-2">
<Collapsible label={`${question.commentCount} comment(s)`}>
<Collapsible label={`${question.numComments} comment(s)`}>
<form
className="mb-2"
onSubmit={handleCommentSubmit(handleSubmitComment)}>
@ -106,7 +158,8 @@ export default function QuestionPage() {
onChange={(value) => {
// eslint-disable-next-line no-console
console.log(value);
}}></Select>
}}
/>
</div>
<Button
@ -118,11 +171,14 @@ export default function QuestionPage() {
</div>
</form>
{Array.from({ length: question.commentCount }).map((_, index) => (
{(comments ?? []).map((comment) => (
<CommentListItem
// eslint-disable-next-line react/no-array-index-key
key={index}
{...comment}
key={comment.id}
authorImageUrl={SAMPLE_QUESTION_COMMENT.authorImageUrl}
authorName={comment.user}
content={comment.content}
createdAt={comment.createdAt}
upvoteCount={0}
/>
))}
</Collapsible>
@ -140,7 +196,7 @@ export default function QuestionPage() {
/>
<div className="mt-3 mb-1 flex justify-between">
<div className="flex items-baseline justify-start gap-2">
<p>{question.answerCount} answers</p>
<p>{question.numAnswers} answers</p>
<div className="flex items-baseline gap-2">
<span aria-hidden={true} className="text-sm">
Sort by:
@ -163,7 +219,8 @@ export default function QuestionPage() {
onChange={(value) => {
// eslint-disable-next-line no-console
console.log(value);
}}></Select>
}}
/>
</div>
</div>
<Button
@ -174,13 +231,18 @@ export default function QuestionPage() {
/>
</div>
</form>
{Array.from({ length: question.answerCount }).map((_, index) => (
{(answers ?? []).map((answer) => (
<AnswerCard
// eslint-disable-next-line react/no-array-index-key
key={index}
{...SAMPLE_ANSWER}
href={`${router.asPath}/answer/1/1`}
key={answer.id}
authorImageUrl={SAMPLE_ANSWER.authorImageUrl}
authorName={answer.user}
commentCount={answer.numComments}
content={answer.content}
createdAt={answer.createdAt}
href={`${router.asPath}/answer/${answer.id}/${createSlug(
answer.content,
)}`}
upvoteCount={answer.numVotes}
/>
))}
</div>

@ -1,5 +1,6 @@
import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react';
import type { QuestionsQuestionType } from '@prisma/client';
import QuestionOverviewCard from '~/components/questions/card/QuestionOverviewCard';
import ContributeQuestionCard from '~/components/questions/ContributeQuestionCard';
@ -13,32 +14,54 @@ import {
LOCATIONS,
QUESTION_AGES,
QUESTION_TYPES,
SAMPLE_QUESTION,
} from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug';
import {
useSearchFilter,
useSearchFilterSingle,
} from '~/utils/questions/useSearchFilter';
import { trpc } from '~/utils/trpc';
export default function QuestionsHomePage() {
const router = useRouter();
const [selectedCompanies, setSelectedCompanies, areCompaniesInitialized] =
useSearchFilter('companies');
const [
selectedQuestionTypes,
setSelectedQuestionTypes,
areQuestionTypesInitialized,
] = useSearchFilter('questionTypes');
] = useSearchFilter<QuestionsQuestionType>('questionTypes');
const [
selectedQuestionAge,
setSelectedQuestionAge,
isQuestionAgeInitialized,
] = useSearchFilterSingle('questionAge', 'all');
] = useSearchFilterSingle<string>('questionAge', 'all');
const [selectedLocations, setSelectedLocations, areLocationsInitialized] =
useSearchFilter('locations');
// TODO: Implement filtering
const { data: questions } = trpc.useQuery([
'questions.questions.getQuestionsByFilter',
{
// TODO: Update when query accepts multiple question types
questionType:
selectedQuestionTypes.length > 0
? (selectedQuestionTypes[0].toUpperCase() as QuestionsQuestionType)
: 'CODING',
},
]);
const utils = trpc.useContext();
const { mutate: createQuestion } = trpc.useMutation(
'questions.questions.create',
{
onSuccess: () => {
utils.invalidateQueries('questions.questions.getQuestionsByFilter');
},
},
);
const [hasLanded, setHasLanded] = useState(false);
const [loaded, setLoaded] = useState(false);
@ -74,7 +97,7 @@ export default function QuestionsHomePage() {
const { company, location, questionType } = data;
setSelectedCompanies([company]);
setSelectedLocations([location]);
setSelectedQuestionTypes([questionType]);
setSelectedQuestionTypes([questionType as QuestionsQuestionType]);
setHasLanded(true);
};
@ -184,7 +207,17 @@ export default function QuestionsHomePage() {
<section className="flex min-h-0 flex-1 flex-col items-center overflow-auto pt-4">
<div className="flex min-h-0 max-w-3xl flex-1">
<div className="flex flex-1 flex-col items-stretch justify-start gap-4 pb-4">
<ContributeQuestionCard />
<ContributeQuestionCard
onSubmit={(data) => {
createQuestion({
company: data.company,
content: data.questionContent,
location: data.location,
questionType: data.questionType,
seenAt: data.date,
});
}}
/>
<QuestionSearchBar
sortOptions={[
{
@ -202,12 +235,21 @@ export default function QuestionsHomePage() {
console.log(value);
}}
/>
{Array.from({ length: 10 }).map((_, index) => (
{(questions ?? []).map((question) => (
<QuestionOverviewCard
// eslint-disable-next-line react/no-array-index-key
key={index}
href="/questions/1/1"
{...SAMPLE_QUESTION}
key={question.id}
answerCount={question.numAnswers}
content={question.content}
href={`/questions/${question.id}/${createSlug(
question.content,
)}`}
location={question.location}
receivedCount={0} // TODO: Implement received count
role={question.role}
timestamp={question.seenAt.toLocaleDateString()}
type={question.type}
upvoteCount={question.numVotes}
/>
))}
</div>

@ -12,7 +12,8 @@ export const questionsAnswerCommentRouter = createProtectedRouter()
answerId: z.string(),
}),
async resolve({ ctx, input }) {
const questionAnswerCommentsData = await ctx.prisma.questionsAnswerComment.findMany({
const questionAnswerCommentsData =
await ctx.prisma.questionsAnswerComment.findMany({
include: {
user: {
select: {
@ -35,34 +36,28 @@ export const questionsAnswerCommentRouter = createProtectedRouter()
switch (currentValue.vote) {
case Vote.UPVOTE:
result += 1
result += 1;
break;
case Vote.DOWNVOTE:
result -= 1
result -= 1;
break;
}
return result;
},
0
0,
);
let userName = "";
if (data.user) {
userName = data.user.name!;
}
const answerComment: AnswerComment = {
content: data.content,
createdAt: data.createdAt,
id: data.id,
numVotes: votes,
updatedAt: data.updatedAt,
user: userName,
user: data.user?.name ?? '',
};
return answerComment;
});
}
},
})
.mutation('create', {
input: z.object({
@ -88,7 +83,8 @@ export const questionsAnswerCommentRouter = createProtectedRouter()
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const answerCommentToUpdate = await ctx.prisma.questionsAnswerComment.findUnique({
const answerCommentToUpdate =
await ctx.prisma.questionsAnswerComment.findUnique({
where: {
id: input.id,
},
@ -118,7 +114,8 @@ export const questionsAnswerCommentRouter = createProtectedRouter()
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const answerCommentToDelete = await ctx.prisma.questionsAnswerComment.findUnique({
const answerCommentToDelete =
await ctx.prisma.questionsAnswerComment.findUnique({
where: {
id: input.id,
},
@ -144,7 +141,7 @@ export const questionsAnswerCommentRouter = createProtectedRouter()
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const {answerCommentId} = input
const { answerCommentId } = input;
return await ctx.prisma.questionsAnswerCommentVote.findUnique({
where: {
@ -176,9 +173,10 @@ export const questionsAnswerCommentRouter = createProtectedRouter()
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const {id, vote} = input
const { id, vote } = input;
const voteToUpdate = await ctx.prisma.questionsAnswerCommentVote.findUnique({
const voteToUpdate =
await ctx.prisma.questionsAnswerCommentVote.findUnique({
where: {
id: input.id,
},
@ -208,10 +206,12 @@ export const questionsAnswerCommentRouter = createProtectedRouter()
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const voteToDelete = await ctx.prisma.questionsAnswerCommentVote.findUnique({
const voteToDelete =
await ctx.prisma.questionsAnswerCommentVote.findUnique({
where: {
id: input.id,
},});
},
});
if (voteToDelete?.id !== userId) {
throw new TRPCError({

@ -40,35 +40,85 @@ export const questionsAnswerRouter = createProtectedRouter()
switch (currentValue.vote) {
case Vote.UPVOTE:
result += 1
result += 1;
break;
case Vote.DOWNVOTE:
result -= 1
result -= 1;
break;
}
return result;
},
0
0,
);
let userName = "";
if (data.user) {
userName = data.user.name!;
}
const answer: Answer = {
content: data.content,
createdAt: data.createdAt,
id: data.id,
numComments: data._count.comments,
numVotes: votes,
user: userName,
user: data.user?.name ?? '',
};
return answer;
});
},
})
.query('getAnswerById', {
input: z.object({
answerId: z.string(),
}),
async resolve({ ctx, input }) {
const answerData = await ctx.prisma.questionsAnswer.findUnique({
include: {
_count: {
select: {
comments: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
where: {
id: input.answerId,
},
});
if (!answerData) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Answer not found',
});
}
const votes: number = answerData.votes.reduce(
(previousValue: number, currentValue) => {
let result: number = previousValue;
switch (currentValue.vote) {
case Vote.UPVOTE:
result += 1;
break;
case Vote.DOWNVOTE:
result -= 1;
break;
}
return result;
},
0,
);
const answer: Answer = {
content: answerData.content,
createdAt: answerData.createdAt,
id: answerData.id,
numComments: answerData._count.comments,
numVotes: votes,
user: answerData.user?.name ?? '',
};
return answer;
},
})
.mutation('create', {
input: z.object({
@ -93,7 +143,7 @@ export const questionsAnswerRouter = createProtectedRouter()
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const {content, id} = input
const { content, id } = input;
const answerToUpdate = await ctx.prisma.questionsAnswer.findUnique({
where: {
@ -128,7 +178,8 @@ export const questionsAnswerRouter = createProtectedRouter()
const answerToDelete = await ctx.prisma.questionsAnswer.findUnique({
where: {
id: input.id,
},});
},
});
if (answerToDelete?.id !== userId) {
throw new TRPCError({
@ -150,7 +201,7 @@ export const questionsAnswerRouter = createProtectedRouter()
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const {answerId} = input
const { answerId } = input;
return await ctx.prisma.questionsAnswerVote.findUnique({
where: {
@ -182,7 +233,7 @@ export const questionsAnswerRouter = createProtectedRouter()
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const {id, vote} = input
const { id, vote } = input;
const voteToUpdate = await ctx.prisma.questionsAnswerVote.findUnique({
where: {
@ -217,7 +268,8 @@ export const questionsAnswerRouter = createProtectedRouter()
const voteToDelete = await ctx.prisma.questionsAnswerVote.findUnique({
where: {
id: input.id,
},});
},
});
if (voteToDelete?.id !== userId) {
throw new TRPCError({

@ -12,7 +12,8 @@ export const questionsQuestionCommentRouter = createProtectedRouter()
questionId: z.string(),
}),
async resolve({ ctx, input }) {
const questionCommentsData = await ctx.prisma.questionsQuestionComment.findMany({
const questionCommentsData =
await ctx.prisma.questionsQuestionComment.findMany({
include: {
user: {
select: {
@ -35,33 +36,27 @@ export const questionsQuestionCommentRouter = createProtectedRouter()
switch (currentValue.vote) {
case Vote.UPVOTE:
result += 1
result += 1;
break;
case Vote.DOWNVOTE:
result -= 1
result -= 1;
break;
}
return result;
},
0
0,
);
let userName = "";
if (data.user) {
userName = data.user.name!;
}
const questionComment: QuestionComment = {
content: data.content,
createdAt: data.createdAt,
id: data.id,
numVotes: votes,
user: userName,
user: data.user?.name ?? '',
};
return questionComment;
});
}
},
})
.mutation('create', {
input: z.object({
@ -87,7 +82,8 @@ export const questionsQuestionCommentRouter = createProtectedRouter()
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const questionCommentToUpdate = await ctx.prisma.questionsQuestionComment.findUnique({
const questionCommentToUpdate =
await ctx.prisma.questionsQuestionComment.findUnique({
where: {
id: input.id,
},
@ -117,7 +113,8 @@ export const questionsQuestionCommentRouter = createProtectedRouter()
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const questionCommentToDelete = await ctx.prisma.questionsQuestionComment.findUnique({
const questionCommentToDelete =
await ctx.prisma.questionsQuestionComment.findUnique({
where: {
id: input.id,
},
@ -143,7 +140,7 @@ export const questionsQuestionCommentRouter = createProtectedRouter()
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const {questionCommentId} = input
const { questionCommentId } = input;
return await ctx.prisma.questionsQuestionCommentVote.findUnique({
where: {
@ -175,9 +172,10 @@ export const questionsQuestionCommentRouter = createProtectedRouter()
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const {id, vote} = input
const { id, vote } = input;
const voteToUpdate = await ctx.prisma.questionsQuestionCommentVote.findUnique({
const voteToUpdate =
await ctx.prisma.questionsQuestionCommentVote.findUnique({
where: {
id: input.id,
},
@ -207,10 +205,12 @@ export const questionsQuestionCommentRouter = createProtectedRouter()
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const voteToDelete = await ctx.prisma.questionsQuestionCommentVote.findUnique({
const voteToDelete =
await ctx.prisma.questionsQuestionCommentVote.findUnique({
where: {
id: input.id,
},});
},
});
if (voteToDelete?.id !== userId) {
throw new TRPCError({

@ -28,6 +28,7 @@ export const questionsQuestionRouter = createProtectedRouter()
company: true,
location: true,
role: true,
seenAt: true,
},
},
user: {
@ -47,11 +48,15 @@ export const questionsQuestionRouter = createProtectedRouter()
return questionsData
.filter((data) => {
for (let i = 0; i < data.encounters.length; i++) {
const encounter = data.encounters[i]
const matchCompany = (!input.company || (encounter.company === input.company));
const matchLocation = (!input.location || (encounter.location === input.location));
const matchRole = (!input.company || (encounter.role === input.role));
if (matchCompany && matchLocation && matchRole) {return true};
const encounter = data.encounters[i];
const matchCompany =
!input.company || encounter.company === input.company;
const matchLocation =
!input.location || encounter.location === input.location;
const matchRole = !input.company || encounter.role === input.role;
if (matchCompany && matchLocation && matchRole) {
return true;
}
}
return false;
})
@ -62,53 +67,140 @@ export const questionsQuestionRouter = createProtectedRouter()
switch (currentValue.vote) {
case Vote.UPVOTE:
result += 1
result += 1;
break;
case Vote.DOWNVOTE:
result -= 1
result -= 1;
break;
}
return result;
},
0
0,
);
let userName = "";
if (data.user) {
userName = data.user.name!;
}
const question: Question = {
company: "",
company: data.encounters[0].company,
content: data.content,
id: data.id,
location: "",
location: data.encounters[0].location ?? 'Unknown location',
numAnswers: data._count.answers,
numComments: data._count.comments,
numVotes: votes,
role: "",
role: data.encounters[0].role ?? 'Unknown role',
seenAt: data.encounters[0].seenAt,
type: data.questionType,
updatedAt: data.updatedAt,
user: userName,
user: data.user?.name ?? '',
};
return question;
});
},
})
.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',
});
}
const votes: number = questionData.votes.reduce(
(previousValue: number, currentValue) => {
let result: number = previousValue;
switch (currentValue.vote) {
case Vote.UPVOTE:
result += 1;
break;
case Vote.DOWNVOTE:
result -= 1;
break;
}
return result;
},
0,
);
const question: Question = {
company: questionData.encounters[0].company,
content: questionData.content,
id: questionData.id,
location: questionData.encounters[0].location ?? 'Unknown location',
numAnswers: questionData._count.answers,
numComments: questionData._count.comments,
numVotes: votes,
role: questionData.encounters[0].role ?? 'Unknown role',
seenAt: questionData.encounters[0].seenAt,
type: questionData.questionType,
updatedAt: questionData.updatedAt,
user: questionData.user?.name ?? '',
};
return question;
},
})
.mutation('create', {
input: z.object({
company: z.string(),
content: z.string(),
location: z.string(),
questionType: z.nativeEnum(QuestionsQuestionType),
role: z.string().optional(),
seenAt: z.date(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
return await ctx.prisma.questionsQuestion.create({
const question = await ctx.prisma.questionsQuestion.create({
data: {
...input,
content: input.content,
questionType: input.questionType,
userId,
},
});
// Create question encounter
await ctx.prisma.questionsQuestionEncounter.create({
data: {
company: input.company,
location: input.location,
questionId: question.id,
role: input.role,
seenAt: input.seenAt,
userId,
},
});
return question;
},
})
.mutation('update', {
@ -116,7 +208,6 @@ export const questionsQuestionRouter = createProtectedRouter()
content: z.string().optional(),
id: z.string(),
questionType: z.nativeEnum(QuestionsQuestionType).optional(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
@ -179,11 +270,11 @@ export const questionsQuestionRouter = createProtectedRouter()
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const {questionId} = input
const { questionId } = input;
return await ctx.prisma.questionsQuestionVote.findUnique({
where: {
questionId_userId : {questionId,userId }
questionId_userId: { questionId, userId },
},
});
},
@ -211,7 +302,7 @@ export const questionsQuestionRouter = createProtectedRouter()
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const {id, vote} = input
const { id, vote } = input;
const voteToUpdate = await ctx.prisma.questionsQuestionVote.findUnique({
where: {
@ -246,7 +337,8 @@ export const questionsQuestionRouter = createProtectedRouter()
const voteToDelete = await ctx.prisma.questionsQuestionVote.findUnique({
where: {
id: input.id,
},});
},
});
if (voteToDelete?.id !== userId) {
throw new TRPCError({

@ -8,14 +8,19 @@ export type Question = {
numComments: number;
numVotes: number;
role: string;
seenAt: Date;
type: stringl;
updatedAt: Date;
user: string;
};
export type AnswerComment = {
content: string;
createdAt: Date;
id: string;
numVotes: number;
updatedAt: Date;
user: string;
};
export type Answer = {
@ -24,6 +29,7 @@ export type Answer = {
id: string;
numComments: number;
numVotes: number;
user: string;
};
export type QuestionComment = {

@ -1,3 +1,5 @@
import type { QuestionsQuestionType } from '@prisma/client';
import type { FilterChoices } from '~/components/questions/filter/FilterSection';
export const COMPANIES: FilterChoices = [
@ -12,18 +14,18 @@ export const COMPANIES: FilterChoices = [
];
// Code, design, behavioral
export const QUESTION_TYPES: FilterChoices = [
export const QUESTION_TYPES: FilterChoices<QuestionsQuestionType> = [
{
label: 'Coding',
value: 'coding',
value: 'CODING',
},
{
label: 'Design',
value: 'design',
value: 'SYSTEM_DESIGN',
},
{
label: 'Behavioral',
value: 'behavioral',
value: 'BEHAVIORAL',
},
];

@ -0,0 +1,7 @@
export default function createSlug(content: string) {
return content
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)+/g, '')
.substring(0, 100);
}

@ -1,14 +1,14 @@
import { useRouter } from 'next/router';
import { useCallback, useEffect, useState } from 'react';
export const useSearchFilter = (
export const useSearchFilter = <Value extends string = string>(
name: string,
defaultValues?: Array<string>,
defaultValues?: Array<Value>,
) => {
const [isInitialized, setIsInitialized] = useState(false);
const router = useRouter();
const [filters, setFilters] = useState<Array<string>>(defaultValues || []);
const [filters, setFilters] = useState<Array<Value>>(defaultValues || []);
useEffect(() => {
if (router.isReady && !isInitialized) {
@ -16,7 +16,7 @@ export const useSearchFilter = (
const query = router.query[name];
if (query) {
const queryValues = Array.isArray(query) ? query : [query];
setFilters(queryValues);
setFilters(queryValues as Array<Value>);
} else {
// Try to load from local storage
const localStorageValue = localStorage.getItem(name);
@ -37,7 +37,7 @@ export const useSearchFilter = (
}, [isInitialized, name, router]);
const setFiltersCallback = useCallback(
(newFilters: Array<string>) => {
(newFilters: Array<Value>) => {
setFilters(newFilters);
localStorage.setItem(name, JSON.stringify(newFilters));
router.replace({
@ -54,14 +54,17 @@ export const useSearchFilter = (
return [filters, setFiltersCallback, isInitialized] as const;
};
export const useSearchFilterSingle = (name: string, defaultValue: string) => {
export const useSearchFilterSingle = <Value extends string = string>(
name: string,
defaultValue: Value,
) => {
const [filters, setFilters, isInitialized] = useSearchFilter(name, [
defaultValue,
]);
return [
filters[0],
(value: string) => {
(value: Value) => {
setFilters([value]);
},
isInitialized,

Loading…
Cancel
Save