mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 20:52:00 +08:00
[questions][feat] integrate backend (#347)
This commit is contained in:
@ -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({
|
||||
|
6
apps/portal/src/types/questions.d.ts
vendored
6
apps/portal/src/types/questions.d.ts
vendored
@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
7
apps/portal/src/utils/questions/createSlug.ts
Normal file
7
apps/portal/src/utils/questions/createSlug.ts
Normal file
@ -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,
|
||||
|
Reference in New Issue
Block a user