-
-
-
{
- // eslint-disable-next-line no-console
- console.log(data);
- }}
- />
+
+
+
+
+
{
+ // eslint-disable-next-line no-console
+ console.log(value);
+ }}
/>
-
+ {Array.from({ length: 10 }).map((_, index) => (
+
+ ))}
-
+
);
diff --git a/apps/portal/src/server/router/index.ts b/apps/portal/src/server/router/index.ts
index 50bf70a0..aa8446ca 100644
--- a/apps/portal/src/server/router/index.ts
+++ b/apps/portal/src/server/router/index.ts
@@ -4,6 +4,8 @@ import { companiesRouter } from './companies-router';
import { createRouter } from './context';
import { protectedExampleRouter } from './protected-example-router';
import { questionsAnswerCommentRouter } from './questions-answer-comment-router';
+import { questionsAnswerRouter } from './questions-answer-router';
+import { questionsQuestionCommentRouter } from './questions-question-comment-router';
import { questionsQuestionRouter } from './questions-question-router';
import { resumesRouter } from './resumes/resumes-resume-router';
import { resumesResumeUserRouter } from './resumes/resumes-resume-user-router';
@@ -28,6 +30,8 @@ export const appRouter = createRouter()
.merge('resumes.reviews.', resumeReviewsRouter)
.merge('resumes.reviews.user.', resumesReviewsUserRouter)
.merge('questions.answers.comments.', questionsAnswerCommentRouter)
+ .merge('questions.answers.', questionsAnswerRouter)
+ .merge('questions.questions.comments.', questionsQuestionCommentRouter)
.merge('questions.questions.', questionsQuestionRouter);
// Export type definition of API
diff --git a/apps/portal/src/server/router/questions-answer-router.ts b/apps/portal/src/server/router/questions-answer-router.ts
new file mode 100644
index 00000000..61a5b6c5
--- /dev/null
+++ b/apps/portal/src/server/router/questions-answer-router.ts
@@ -0,0 +1,235 @@
+import { z } from 'zod';
+import { Vote } from '@prisma/client';
+import { TRPCError } from '@trpc/server';
+
+import { createProtectedRouter } from './context';
+
+import type { Answer } from '~/types/questions';
+
+export const questionsAnswerRouter = createProtectedRouter()
+ .query('getAnswers', {
+ input: z.object({
+ questionId: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const answersData = await ctx.prisma.questionsAnswer.findMany({
+ include: {
+ _count: {
+ select: {
+ comments: true,
+ },
+ },
+ user: {
+ select: {
+ name: true,
+ },
+ },
+ votes: true,
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ where: {
+ ...input,
+ },
+ });
+ return answersData.map((data) => {
+ const votes:number = data.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
+ );
+
+ 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,
+ };
+ return answer;
+ });
+ }
+ })
+ .mutation('create', {
+ input: z.object({
+ content: z.string(),
+ questionId: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+
+ return await ctx.prisma.questionsAnswer.create({
+ data: {
+ ...input,
+ userId,
+ },
+ });
+ },
+ })
+ .mutation('update', {
+ input: z.object({
+ content: z.string().optional(),
+ id: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+ const {content, id} = input
+
+ const answerToUpdate = await ctx.prisma.questionsAnswer.findUnique({
+ where: {
+ id: input.id,
+ },
+ });
+
+ if (answerToUpdate?.id !== userId) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'User have no authorization to record.',
+ });
+ }
+
+ return await ctx.prisma.questionsAnswer.update({
+ data: {
+ content,
+ },
+ where: {
+ id,
+ },
+ });
+ },
+ })
+ .mutation('delete', {
+ input: z.object({
+ id: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+
+ const answerToDelete = await ctx.prisma.questionsAnswer.findUnique({
+ where: {
+ id: input.id,
+ },});
+
+ if (answerToDelete?.id !== userId) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'User have no authorization to record.',
+ });
+ }
+
+ return await ctx.prisma.questionsAnswer.delete({
+ where: {
+ id: input.id,
+ },
+ });
+ },
+ })
+ .query('getVote', {
+ input: z.object({
+ answerId: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+ const {answerId} = input
+
+ return await ctx.prisma.questionsAnswerVote.findUnique({
+ where: {
+ answerId_userId : { answerId, userId },
+ },
+ });
+ },
+ })
+ .mutation('createVote', {
+ input: z.object({
+ answerId: z.string(),
+ vote: z.nativeEnum(Vote),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+
+ return await ctx.prisma.questionsAnswerVote.create({
+ data: {
+ ...input,
+ userId,
+ },
+ });
+ },
+ })
+ .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.questionsAnswerVote.findUnique({
+ where: {
+ id: input.id,
+ },
+ });
+
+ if (voteToUpdate?.id !== userId) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'User have no authorization to record.',
+ });
+ }
+
+ return await ctx.prisma.questionsAnswerVote.update({
+ data: {
+ vote,
+ },
+ where: {
+ id,
+ },
+ });
+ },
+ })
+ .mutation('deleteVote', {
+ input: z.object({
+ id: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+
+ const voteToDelete = await ctx.prisma.questionsAnswerVote.findUnique({
+ where: {
+ id: input.id,
+ },});
+
+ if (voteToDelete?.id !== userId) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'User have no authorization to record.',
+ });
+ }
+
+ return await ctx.prisma.questionsAnswerVote.delete({
+ where: {
+ id: input.id,
+ },
+ });
+ },
+ });
\ No newline at end of file
diff --git a/apps/portal/src/server/router/questions-question-comment-router.ts b/apps/portal/src/server/router/questions-question-comment-router.ts
new file mode 100644
index 00000000..12f2bc21
--- /dev/null
+++ b/apps/portal/src/server/router/questions-question-comment-router.ts
@@ -0,0 +1,228 @@
+import { z } from 'zod';
+import { Vote } from '@prisma/client';
+import { TRPCError } from '@trpc/server';
+
+import { createProtectedRouter } from './context';
+
+import type { QuestionComment } from '~/types/questions';
+
+export const questionsQuestionCommentRouter = createProtectedRouter()
+ .query('getQuestionComments', {
+ input: z.object({
+ questionId: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const questionCommentsData = await ctx.prisma.questionsQuestionComment.findMany({
+ include: {
+ user: {
+ select: {
+ name: true,
+ },
+ },
+ votes: true,
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ where: {
+ ...input,
+ },
+ });
+ return questionCommentsData.map((data) => {
+ const votes:number = data.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
+ );
+
+ 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,
+ };
+ return questionComment;
+ });
+ }
+ })
+ .mutation('create', {
+ input: z.object({
+ content: z.string(),
+ questionId: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+
+ return await ctx.prisma.questionsQuestionComment.create({
+ data: {
+ ...input,
+ userId,
+ },
+ });
+ },
+ })
+ .mutation('update', {
+ input: z.object({
+ content: z.string().optional(),
+ id: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+
+ const questionCommentToUpdate = await ctx.prisma.questionsQuestionComment.findUnique({
+ where: {
+ id: input.id,
+ },
+ });
+
+ if (questionCommentToUpdate?.id !== userId) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'User have no authorization to record.',
+ });
+ }
+
+ return await ctx.prisma.questionsQuestionComment.update({
+ data: {
+ ...input,
+ },
+ where: {
+ id: input.id,
+ },
+ });
+ },
+ })
+ .mutation('delete', {
+ input: z.object({
+ id: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+
+ const questionCommentToDelete = await ctx.prisma.questionsQuestionComment.findUnique({
+ where: {
+ id: input.id,
+ },
+ });
+
+ if (questionCommentToDelete?.id !== userId) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'User have no authorization to record.',
+ });
+ }
+
+ return await ctx.prisma.questionsQuestionComment.delete({
+ where: {
+ id: input.id,
+ },
+ });
+ },
+ })
+ .query('getVote', {
+ input: z.object({
+ questionCommentId: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+ const {questionCommentId} = input
+
+ return await ctx.prisma.questionsQuestionCommentVote.findUnique({
+ where: {
+ questionCommentId_userId : {questionCommentId,userId },
+ },
+ });
+ },
+ })
+ .mutation('createVote', {
+ input: z.object({
+ questionCommentId: z.string(),
+ vote: z.nativeEnum(Vote),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+
+ return await ctx.prisma.questionsQuestionCommentVote.create({
+ data: {
+ ...input,
+ userId,
+ },
+ });
+ },
+ })
+ .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.questionsQuestionCommentVote.findUnique({
+ where: {
+ id: input.id,
+ },
+ });
+
+ if (voteToUpdate?.id !== userId) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'User have no authorization to record.',
+ });
+ }
+
+ return await ctx.prisma.questionsQuestionCommentVote.update({
+ data: {
+ vote,
+ },
+ where: {
+ id,
+ },
+ });
+ },
+ })
+ .mutation('deleteVote', {
+ input: z.object({
+ id: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+
+ const voteToDelete = await ctx.prisma.questionsQuestionCommentVote.findUnique({
+ where: {
+ id: input.id,
+ },});
+
+ if (voteToDelete?.id !== userId) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'User have no authorization to record.',
+ });
+ }
+
+ return await ctx.prisma.questionsQuestionCommentVote.delete({
+ where: {
+ id: input.id,
+ },
+ });
+ },
+ });
\ No newline at end of file
diff --git a/apps/portal/src/types/questions.d.ts b/apps/portal/src/types/questions.d.ts
index 50ace0ed..0d60ca76 100644
--- a/apps/portal/src/types/questions.d.ts
+++ b/apps/portal/src/types/questions.d.ts
@@ -16,4 +16,20 @@ export type AnswerComment = {
content: string;
id: string;
numVotes: number;
-};
\ No newline at end of file
+};
+
+export type Answer = {
+ content: string;
+ createdAt: Date;
+ id: string;
+ numComments: number;
+ numVotes: number;
+};
+
+export type QuestionComment = {
+ content: string;
+ createdAt: Date;
+ id: string;
+ numVotes: number;
+ user: string;
+};
diff --git a/apps/portal/src/utils/questions/constants.ts b/apps/portal/src/utils/questions/constants.ts
new file mode 100644
index 00000000..966e1a1f
--- /dev/null
+++ b/apps/portal/src/utils/questions/constants.ts
@@ -0,0 +1,108 @@
+import type { FilterChoices } from '~/components/questions/filter/FilterSection';
+
+export const COMPANIES: FilterChoices = [
+ {
+ label: 'Google',
+ value: 'google',
+ },
+ {
+ label: 'Meta',
+ value: 'meta',
+ },
+];
+
+// Code, design, behavioral
+export const QUESTION_TYPES: FilterChoices = [
+ {
+ label: 'Coding',
+ value: 'coding',
+ },
+ {
+ label: 'Design',
+ value: 'design',
+ },
+ {
+ label: 'Behavioral',
+ value: 'behavioral',
+ },
+];
+
+export const QUESTION_AGES: FilterChoices = [
+ {
+ label: 'Last month',
+ value: 'last-month',
+ },
+ {
+ label: 'Last 6 months',
+ value: 'last-6-months',
+ },
+ {
+ label: 'Last year',
+ value: 'last-year',
+ },
+ {
+ label: 'All',
+ value: 'all',
+ },
+];
+
+export const LOCATIONS: FilterChoices = [
+ {
+ label: 'Singapore',
+ value: 'singapore',
+ },
+ {
+ label: 'Menlo Park',
+ value: 'menlopark',
+ },
+ {
+ label: 'California',
+ value: 'california',
+ },
+ {
+ label: 'Hong Kong',
+ value: 'hongkong',
+ },
+ {
+ label: 'Taiwan',
+ value: 'taiwan',
+ },
+];
+
+export const SAMPLE_QUESTION = {
+ answerCount: 10,
+ commentCount: 10,
+ company: 'Google',
+ content:
+ 'Given an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums andiven an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums andiven an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums andiven an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums andiven an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums andiven an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums andiven an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums andiven an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums and',
+ location: 'Menlo Park, CA',
+ receivedCount: 12,
+ role: 'Software Engineer',
+ timestamp: 'Last month',
+ upvoteCount: 5,
+};
+
+export const SAMPLE_ANSWER = {
+ authorImageUrl: 'https://avatars.githubusercontent.com/u/66356390?v=4',
+ authorName: 'Jeff Sieu',
+ commentCount: 10,
+ content: 'This is a sample answer',
+ createdAt: new Date(2014, 8, 1, 11, 30, 40),
+ upvoteCount: 10,
+};
+
+export const SAMPLE_QUESTION_COMMENT = {
+ authorImageUrl: 'https://avatars.githubusercontent.com/u/66356390?v=4',
+ authorName: 'Jeff Sieu',
+ content: 'This is a sample question comment',
+ createdAt: new Date(2014, 8, 1, 11, 30, 40),
+ upvoteCount: 10,
+};
+
+export const SAMPLE_ANSWER_COMMENT = {
+ authorImageUrl: 'https://avatars.githubusercontent.com/u/66356390?v=4',
+ authorName: 'Jeff Sieu',
+ content: 'This is an sample answer comment',
+ createdAt: new Date(2014, 8, 1, 11, 30, 40),
+ upvoteCount: 10,
+};
diff --git a/apps/portal/src/utils/questions/useFormRegister.ts b/apps/portal/src/utils/questions/useFormRegister.ts
new file mode 100644
index 00000000..42ab2fc5
--- /dev/null
+++ b/apps/portal/src/utils/questions/useFormRegister.ts
@@ -0,0 +1,43 @@
+import type { ChangeEvent } from 'react';
+import { useCallback } from 'react';
+import type { FieldValues, UseFormRegister } from 'react-hook-form';
+
+export const useFormRegister =
(
+ register: UseFormRegister,
+) => {
+ const formRegister = useCallback(
+ (...args: Parameters) => {
+ const { onChange, ...rest } = register(...args);
+ return {
+ ...rest,
+ onChange: (value: string, event: ChangeEvent) => {
+ onChange(event);
+ },
+ };
+ },
+ [register],
+ );
+ return formRegister;
+};
+
+export const useSelectRegister = (
+ register: UseFormRegister,
+) => {
+ const formRegister = useCallback(
+ (...args: Parameters) => {
+ const { onChange, ...rest } = register(...args);
+ return {
+ ...rest,
+ onChange: (value: string) => {
+ onChange({
+ target: {
+ value,
+ },
+ });
+ },
+ };
+ },
+ [register],
+ );
+ return formRegister;
+};
diff --git a/apps/portal/src/utils/questions/useSearchFilter.ts b/apps/portal/src/utils/questions/useSearchFilter.ts
new file mode 100644
index 00000000..711f5ead
--- /dev/null
+++ b/apps/portal/src/utils/questions/useSearchFilter.ts
@@ -0,0 +1,69 @@
+import { useRouter } from 'next/router';
+import { useCallback, useEffect, useState } from 'react';
+
+export const useSearchFilter = (
+ name: string,
+ defaultValues?: Array,
+) => {
+ const [isInitialized, setIsInitialized] = useState(false);
+ const router = useRouter();
+
+ const [filters, setFilters] = useState>(defaultValues || []);
+
+ useEffect(() => {
+ if (router.isReady && !isInitialized) {
+ // Initialize from query params
+ const query = router.query[name];
+ if (query) {
+ const queryValues = Array.isArray(query) ? query : [query];
+ setFilters(queryValues);
+ } else {
+ // Try to load from local storage
+ const localStorageValue = localStorage.getItem(name);
+ if (localStorageValue !== null) {
+ const loadedFilters = JSON.parse(localStorageValue);
+ setFilters(loadedFilters);
+ router.replace({
+ pathname: router.pathname,
+ query: {
+ ...router.query,
+ [name]: loadedFilters,
+ },
+ });
+ }
+ }
+ setIsInitialized(true);
+ }
+ }, [isInitialized, name, router]);
+
+ const setFiltersCallback = useCallback(
+ (newFilters: Array) => {
+ setFilters(newFilters);
+ localStorage.setItem(name, JSON.stringify(newFilters));
+ router.replace({
+ pathname: router.pathname,
+ query: {
+ ...router.query,
+ [name]: newFilters,
+ },
+ });
+ },
+ [name, router],
+ );
+
+ return [filters, setFiltersCallback, isInitialized] as const;
+};
+
+export const useSearchFilterSingle = (name: string, defaultValue: string) => {
+ const [filters, setFilters, isInitialized] = useSearchFilter(name, [
+ defaultValue,
+ ]);
+
+ return [
+ filters[0],
+ (value: string) => {
+ setFilters([value]);
+ },
+ isInitialized,
+ ] as const;
+};
diff --git a/apps/portal/src/utils/questions/withHref.tsx b/apps/portal/src/utils/questions/withHref.tsx
new file mode 100644
index 00000000..681f7122
--- /dev/null
+++ b/apps/portal/src/utils/questions/withHref.tsx
@@ -0,0 +1,21 @@
+const withHref = >(
+ Component: React.ComponentType,
+) => {
+ return (
+ props: Props & {
+ href: string;
+ },
+ ) => {
+ const { href, ...others } = props;
+
+ return (
+
+
+
+ );
+ };
+};
+
+export default withHref;