[questions][feat] add question, view question list

pull/347/head
Jeff Sieu 3 years ago
parent 0335616d57
commit a9381edcb0

@ -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>

@ -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>> =
| {

@ -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,20 @@ 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="TODO"
upvoteCount={question.numVotes}
/>
))}
</div>

@ -1,5 +1,5 @@
import { z } from 'zod';
import {QuestionsQuestionType, Vote } from '@prisma/client';
import { QuestionsQuestionType, Vote } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import { createProtectedRouter } from './context';
@ -47,68 +47,91 @@ 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;
})
.map((data) => {
const votes:number = data.votes.reduce(
(previousValue:number, currentValue) => {
let result:number = previousValue;
const votes: number = data.votes.reduce(
(previousValue: number, currentValue) => {
let result: number = previousValue;
switch(currentValue.vote) {
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 = "";
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',
updatedAt: data.updatedAt,
user: userName,
};
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 +139,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 +201,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 +233,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 +268,8 @@ export const questionsQuestionRouter = createProtectedRouter()
const voteToDelete = await ctx.prisma.questionsQuestionVote.findUnique({
where: {
id: input.id,
},});
},
});
if (voteToDelete?.id !== userId) {
throw new TRPCError({

@ -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