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

pull/457/head
hpkoh 3 years ago
commit 1c16930bf5

@ -28,9 +28,9 @@ export default function ContributeQuestionCard({
}; };
return ( return (
<div> <div className="w-full">
<button <button
className="flex flex-col items-stretch justify-center gap-2 rounded-md border border-slate-300 bg-white p-4 text-left hover:bg-slate-100" className="w-full flex flex-col items-stretch justify-center gap-2 rounded-md border border-slate-300 bg-white p-4 text-left hover:bg-slate-100"
type="button" type="button"
onClick={handleOpenContribute}> onClick={handleOpenContribute}>
<TextInput <TextInput

@ -2,40 +2,19 @@ import {
AdjustmentsHorizontalIcon, AdjustmentsHorizontalIcon,
MagnifyingGlassIcon, MagnifyingGlassIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { Button, Select, TextInput } from '@tih/ui'; import { Button, TextInput } from '@tih/ui';
export type SortOption<Value> = { import type { SortOptionsSelectProps } from './SortOptionsSelect';
label: string; import SortOptionsSelect from './SortOptionsSelect';
value: Value;
};
type SortOrderProps<SortOrder> = {
onSortOrderChange?: (sortValue: SortOrder) => void;
sortOrderOptions: ReadonlyArray<SortOption<SortOrder>>;
sortOrderValue: SortOrder;
};
type SortTypeProps<SortType> = { export type QuestionSearchBarProps = SortOptionsSelectProps & {
onSortTypeChange?: (sortType: SortType) => void; onFilterOptionsToggle: () => void;
sortTypeOptions: ReadonlyArray<SortOption<SortType>>;
sortTypeValue: SortType;
}; };
export type QuestionSearchBarProps<SortType, SortOrder> = export default function QuestionSearchBar({
SortOrderProps<SortOrder> &
SortTypeProps<SortType> & {
onFilterOptionsToggle: () => void;
};
export default function QuestionSearchBar<SortType, SortOrder>({
onSortOrderChange,
sortOrderOptions,
sortOrderValue,
onSortTypeChange,
sortTypeOptions,
sortTypeValue,
onFilterOptionsToggle, onFilterOptionsToggle,
}: QuestionSearchBarProps<SortType, SortOrder>) { ...sortOptionsSelectProps
}: QuestionSearchBarProps) {
return ( return (
<div className="flex flex-col items-stretch gap-x-4 gap-y-2 lg:flex-row lg:items-end"> <div className="flex flex-col items-stretch gap-x-4 gap-y-2 lg:flex-row lg:items-end">
<div className="flex-1 "> <div className="flex-1 ">
@ -48,38 +27,7 @@ export default function QuestionSearchBar<SortType, SortOrder>({
/> />
</div> </div>
<div className="flex items-end justify-end gap-4"> <div className="flex items-end justify-end gap-4">
<div className="flex items-center gap-2"> <SortOptionsSelect {...sortOptionsSelectProps} />
<Select
display="inline"
label="Sort by"
options={sortTypeOptions}
value={sortTypeValue}
onChange={(value) => {
const chosenOption = sortTypeOptions.find(
(option) => String(option.value) === value,
);
if (chosenOption) {
onSortTypeChange?.(chosenOption.value);
}
}}
/>
</div>
<div className="flex items-center gap-2">
<Select
display="inline"
label="Order by"
options={sortOrderOptions}
value={sortOrderValue}
onChange={(value) => {
const chosenOption = sortOrderOptions.find(
(option) => String(option.value) === value,
);
if (chosenOption) {
onSortOrderChange?.(chosenOption.value);
}
}}
/>
</div>
<div className="lg:hidden"> <div className="lg:hidden">
<Button <Button
addonPosition="start" addonPosition="start"

@ -0,0 +1,69 @@
import { Select } from '~/../../../packages/ui/dist';
import { SORT_ORDERS, SORT_TYPES } from '~/utils/questions/constants';
import type { SortOrder, SortType } from '~/types/questions.d';
export type SortOption<Value> = {
label: string;
value: Value;
};
const sortTypeOptions = SORT_TYPES;
const sortOrderOptions = SORT_ORDERS;
type SortOrderProps<Order> = {
onSortOrderChange?: (sortValue: Order) => void;
sortOrderValue: Order;
};
type SortTypeProps<Type> = {
onSortTypeChange?: (sortType: Type) => void;
sortTypeValue: Type;
};
export type SortOptionsSelectProps = SortOrderProps<SortOrder> &
SortTypeProps<SortType>;
export default function SortOptionsSelect({
onSortOrderChange,
sortOrderValue,
onSortTypeChange,
sortTypeValue,
}: SortOptionsSelectProps) {
return (
<div className="flex items-end justify-end gap-4">
<div className="flex items-center gap-2">
<Select
display="inline"
label="Sort by"
options={sortTypeOptions}
value={sortTypeValue}
onChange={(value) => {
const chosenOption = sortTypeOptions.find(
(option) => String(option.value) === value,
);
if (chosenOption) {
onSortTypeChange?.(chosenOption.value);
}
}}
/>
</div>
<div className="flex items-center gap-2">
<Select
display="inline"
label="Order by"
options={sortOrderOptions}
value={sortOrderValue}
onChange={(value) => {
const chosenOption = sortOrderOptions.find(
(option) => String(option.value) === value,
);
if (chosenOption) {
onSortOrderChange?.(chosenOption.value);
}
}}
/>
</div>
</div>
);
}

@ -1,17 +1,21 @@
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline'; import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
import { Button, Select, TextArea } from '@tih/ui'; import { Button, TextArea } from '@tih/ui';
import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem'; import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem';
import FullAnswerCard from '~/components/questions/card/FullAnswerCard'; import FullAnswerCard from '~/components/questions/card/FullAnswerCard';
import FullScreenSpinner from '~/components/questions/FullScreenSpinner'; import FullScreenSpinner from '~/components/questions/FullScreenSpinner';
import SortOptionsSelect from '~/components/questions/SortOptionsSelect';
import { APP_TITLE } from '~/utils/questions/constants'; import { APP_TITLE } from '~/utils/questions/constants';
import { useFormRegister } from '~/utils/questions/useFormRegister'; import { useFormRegister } from '~/utils/questions/useFormRegister';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import { SortOrder, SortType } from '~/types/questions.d';
export type AnswerCommentData = { export type AnswerCommentData = {
commentContent: string; commentContent: string;
}; };
@ -19,6 +23,13 @@ export type AnswerCommentData = {
export default function QuestionPage() { export default function QuestionPage() {
const router = useRouter(); const router = useRouter();
const [commentSortOrder, setCommentSortOrder] = useState<SortOrder>(
SortOrder.DESC,
);
const [commentSortType, setCommentSortType] = useState<SortType>(
SortType.NEW,
);
const { const {
register: comRegister, register: comRegister,
reset: resetComment, reset: resetComment,
@ -36,10 +47,20 @@ export default function QuestionPage() {
{ answerId: answerId as string }, { answerId: answerId as string },
]); ]);
const { data: comments } = trpc.useQuery([ const { data: answerCommentsData } = trpc.useInfiniteQuery(
'questions.answers.comments.getAnswerComments', [
{ answerId: answerId as string }, 'questions.answers.comments.getAnswerComments',
]); {
answerId: answerId as string,
sortOrder: commentSortOrder,
sortType: commentSortType,
},
],
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
keepPreviousData: true,
},
);
const { mutate: addComment } = trpc.useMutation( const { mutate: addComment } = trpc.useMutation(
'questions.answers.comments.user.create', 'questions.answers.comments.user.create',
@ -47,7 +68,11 @@ export default function QuestionPage() {
onSuccess: () => { onSuccess: () => {
utils.invalidateQueries([ utils.invalidateQueries([
'questions.answers.comments.getAnswerComments', 'questions.answers.comments.getAnswerComments',
{ answerId: answerId as string }, {
answerId: answerId as string,
sortOrder: SortOrder.DESC,
sortType: SortType.NEW,
},
]); ]);
}, },
}, },
@ -108,32 +133,6 @@ export default function QuestionPage() {
rows={2} rows={2}
/> />
<div className="my-3 flex justify-between"> <div className="my-3 flex justify-between">
<div className="flex items-baseline gap-2">
<span aria-hidden={true} className="text-sm">
Sort by:
</span>
<Select
display="inline"
isLabelHidden={true}
label="Sort by"
options={[
{
label: 'Most recent',
value: 'most-recent',
},
{
label: 'Most upvotes',
value: 'most-upvotes',
},
]}
value="most-recent"
onChange={(value) => {
// eslint-disable-next-line no-console
console.log(value);
}}
/>
</div>
<Button <Button
disabled={!isCommentDirty || !isCommentValid} disabled={!isCommentDirty || !isCommentValid}
label="Post" label="Post"
@ -142,18 +141,34 @@ export default function QuestionPage() {
/> />
</div> </div>
</form> </form>
<div className="flex flex-col gap-2">
{(comments ?? []).map((comment) => ( <div className="flex items-center justify-between gap-2">
<AnswerCommentListItem <p className="text-lg">Comments</p>
key={comment.id} <div className="flex items-end gap-2">
answerCommentId={comment.id} <SortOptionsSelect
authorImageUrl={comment.userImage} sortOrderValue={commentSortOrder}
authorName={comment.user} sortTypeValue={commentSortType}
content={comment.content} onSortOrderChange={setCommentSortOrder}
createdAt={comment.createdAt} onSortTypeChange={setCommentSortType}
upvoteCount={comment.numVotes} />
/> </div>
))} </div>
{/* TODO: Allow to load more pages */}
{(answerCommentsData?.pages ?? []).flatMap(
({ processedQuestionAnswerCommentsData: comments }) =>
comments.map((comment) => (
<AnswerCommentListItem
key={comment.id}
answerCommentId={comment.id}
authorImageUrl={comment.userImage}
authorName={comment.user}
content={comment.content}
createdAt={comment.createdAt}
upvoteCount={comment.numVotes}
/>
)),
)}
</div>
</div> </div>
</div> </div>
</div> </div>

@ -1,19 +1,23 @@
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline'; import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
import { Button, Collapsible, Select, TextArea } from '@tih/ui'; import { Button, Collapsible, HorizontalDivider, TextArea } from '@tih/ui';
import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem'; import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem';
import FullQuestionCard from '~/components/questions/card/question/FullQuestionCard'; import FullQuestionCard from '~/components/questions/card/question/FullQuestionCard';
import QuestionAnswerCard from '~/components/questions/card/QuestionAnswerCard'; import QuestionAnswerCard from '~/components/questions/card/QuestionAnswerCard';
import FullScreenSpinner from '~/components/questions/FullScreenSpinner'; import FullScreenSpinner from '~/components/questions/FullScreenSpinner';
import SortOptionsSelect from '~/components/questions/SortOptionsSelect';
import { APP_TITLE } from '~/utils/questions/constants'; import { APP_TITLE } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug'; import createSlug from '~/utils/questions/createSlug';
import { useFormRegister } from '~/utils/questions/useFormRegister'; import { useFormRegister } from '~/utils/questions/useFormRegister';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import { SortOrder, SortType } from '~/types/questions.d';
export type AnswerQuestionData = { export type AnswerQuestionData = {
answerContent: string; answerContent: string;
}; };
@ -24,6 +28,19 @@ export type QuestionCommentData = {
export default function QuestionPage() { export default function QuestionPage() {
const router = useRouter(); const router = useRouter();
const [answerSortOrder, setAnswerSortOrder] = useState<SortOrder>(
SortOrder.DESC,
);
const [answerSortType, setAnswerSortType] = useState<SortType>(SortType.NEW);
const [commentSortOrder, setCommentSortOrder] = useState<SortOrder>(
SortOrder.DESC,
);
const [commentSortType, setCommentSortType] = useState<SortType>(
SortType.NEW,
);
const { const {
register: ansRegister, register: ansRegister,
handleSubmit, handleSubmit,
@ -54,10 +71,20 @@ export default function QuestionPage() {
const utils = trpc.useContext(); const utils = trpc.useContext();
const { data: comments } = trpc.useQuery([ const { data: commentData } = trpc.useInfiniteQuery(
'questions.questions.comments.getQuestionComments', [
{ questionId: questionId as string }, 'questions.questions.comments.getQuestionComments',
]); {
questionId: questionId as string,
sortOrder: commentSortOrder,
sortType: commentSortType,
},
],
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
keepPreviousData: true,
},
);
const { mutate: addComment } = trpc.useMutation( const { mutate: addComment } = trpc.useMutation(
'questions.questions.comments.user.create', 'questions.questions.comments.user.create',
@ -70,10 +97,20 @@ export default function QuestionPage() {
}, },
); );
const { data: answers } = trpc.useQuery([ const { data: answerData } = trpc.useInfiniteQuery(
'questions.answers.getAnswers', [
{ questionId: questionId as string }, 'questions.answers.getAnswers',
]); {
questionId: questionId as string,
sortOrder: answerSortOrder,
sortType: answerSortType,
},
],
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
keepPreviousData: true,
},
);
const { mutate: addAnswer } = trpc.useMutation( const { mutate: addAnswer } = trpc.useMutation(
'questions.answers.user.create', 'questions.answers.user.create',
@ -134,7 +171,7 @@ export default function QuestionPage() {
variant="secondary" variant="secondary"
/> />
</div> </div>
<div className="flex w-full justify-center overflow-y-auto py-4 px-5"> <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"> <div className="flex max-w-7xl flex-1 flex-col gap-2">
<FullQuestionCard <FullQuestionCard
{...question} {...question}
@ -159,69 +196,62 @@ export default function QuestionPage() {
}} }}
/> />
<div className="mx-2"> <div className="mx-2">
<Collapsible label={`${(comments ?? []).length} comment(s)`}> <Collapsible label={`${question.numComments} comment(s)`}>
<form <div className="mt-4 px-4">
className="mb-2" <form
onSubmit={handleCommentSubmit(handleSubmitComment)}> className="mb-2"
<TextArea onSubmit={handleCommentSubmit(handleSubmitComment)}>
{...commentRegister('commentContent', { <TextArea
minLength: 1, {...commentRegister('commentContent', {
required: true, minLength: 1,
})} required: true,
label="Post a comment" })}
required={true} label="Post a comment"
resize="vertical" required={true}
rows={2} resize="vertical"
/> rows={2}
<div className="my-3 flex justify-between"> />
<div className="flex items-baseline gap-2"> <div className="my-3 flex justify-between">
<span aria-hidden={true} className="text-sm"> <Button
Sort by: disabled={!isCommentDirty || !isCommentValid}
</span> label="Post"
<Select type="submit"
display="inline" variant="primary"
isLabelHidden={true}
label="Sort by"
options={[
{
label: 'Most recent',
value: 'most-recent',
},
{
label: 'Most upvotes',
value: 'most-upvotes',
},
]}
value="most-recent"
onChange={(value) => {
// eslint-disable-next-line no-console
console.log(value);
}}
/> />
</div> </div>
</form>
<Button {/* TODO: Add button to load more */}
disabled={!isCommentDirty || !isCommentValid} <div className="flex flex-col gap-2">
label="Post" <div className="flex items-center justify-between gap-2">
type="submit" <p className="text-lg">Comments</p>
variant="primary" <div className="flex items-end gap-2">
/> <SortOptionsSelect
sortOrderValue={commentSortOrder}
sortTypeValue={commentSortType}
onSortOrderChange={setCommentSortOrder}
onSortTypeChange={setCommentSortType}
/>
</div>
</div>
{(commentData?.pages ?? []).flatMap(
({ processedQuestionCommentsData: comments }) =>
comments.map((comment) => (
<AnswerCommentListItem
key={comment.id}
answerCommentId={comment.id}
authorImageUrl={comment.userImage}
authorName={comment.user}
content={comment.content}
createdAt={comment.createdAt}
upvoteCount={comment.numVotes}
/>
)),
)}
</div> </div>
</form> </div>
{(comments ?? []).map((comment) => (
<AnswerCommentListItem
key={comment.id}
answerCommentId={comment.id}
authorImageUrl={comment.userImage}
authorName={comment.user}
content={comment.content}
createdAt={comment.createdAt}
upvoteCount={comment.numVotes}
/>
))}
</Collapsible> </Collapsible>
</div> </div>
<HorizontalDivider />
<form onSubmit={handleSubmit(handleSubmitAnswer)}> <form onSubmit={handleSubmit(handleSubmitAnswer)}>
<TextArea <TextArea
{...answerRegister('answerContent', { {...answerRegister('answerContent', {
@ -234,34 +264,6 @@ export default function QuestionPage() {
rows={5} rows={5}
/> />
<div className="mt-3 mb-1 flex justify-between"> <div className="mt-3 mb-1 flex justify-between">
<div className="flex items-baseline justify-start gap-2">
<p>{(answers ?? []).length} answers</p>
<div className="flex items-baseline gap-2">
<span aria-hidden={true} className="text-sm">
Sort by:
</span>
<Select
display="inline"
isLabelHidden={true}
label="Sort by"
options={[
{
label: 'Most recent',
value: 'most-recent',
},
{
label: 'Most upvotes',
value: 'most-upvotes',
},
]}
value="most-recent"
onChange={(value) => {
// eslint-disable-next-line no-console
console.log(value);
}}
/>
</div>
</div>
<Button <Button
disabled={!isDirty || !isValid} disabled={!isDirty || !isValid}
label="Contribute" label="Contribute"
@ -270,21 +272,36 @@ export default function QuestionPage() {
/> />
</div> </div>
</form> </form>
{(answers ?? []).map((answer) => ( <div className="flex items-center justify-between gap-2">
<QuestionAnswerCard <p className="text-xl">{question.numAnswers} answers</p>
key={answer.id} <div className="flex items-end gap-2">
answerId={answer.id} <SortOptionsSelect
authorImageUrl={answer.userImage} sortOrderValue={answerSortOrder}
authorName={answer.user} sortTypeValue={answerSortType}
commentCount={answer.numComments} onSortOrderChange={setAnswerSortOrder}
content={answer.content} onSortTypeChange={setAnswerSortType}
createdAt={answer.createdAt} />
href={`${router.asPath}/answer/${answer.id}/${createSlug( </div>
answer.content, </div>
)}`} {/* TODO: Add button to load more */}
upvoteCount={answer.numVotes} {(answerData?.pages ?? []).flatMap(
/> ({ processedAnswersData: answers }) =>
))} answers.map((answer) => (
<QuestionAnswerCard
key={answer.id}
answerId={answer.id}
authorImageUrl={answer.userImage}
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> </div>
</div> </div>
</div> </div>

@ -16,8 +16,6 @@ import LocationTypeahead from '~/components/questions/typeahead/LocationTypeahea
import RoleTypeahead from '~/components/questions/typeahead/RoleTypeahead'; import RoleTypeahead from '~/components/questions/typeahead/RoleTypeahead';
import type { QuestionAge } from '~/utils/questions/constants'; import type { QuestionAge } from '~/utils/questions/constants';
import { SORT_TYPES } from '~/utils/questions/constants';
import { SORT_ORDERS } from '~/utils/questions/constants';
import { APP_TITLE } from '~/utils/questions/constants'; import { APP_TITLE } from '~/utils/questions/constants';
import { QUESTION_AGES, QUESTION_TYPES } from '~/utils/questions/constants'; import { QUESTION_AGES, QUESTION_TYPES } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug'; import createSlug from '~/utils/questions/createSlug';
@ -457,9 +455,7 @@ export default function QuestionsBrowsePage() {
}} }}
/> />
<QuestionSearchBar <QuestionSearchBar
sortOrderOptions={SORT_ORDERS}
sortOrderValue={sortOrder} sortOrderValue={sortOrder}
sortTypeOptions={SORT_TYPES}
sortTypeValue={sortType} sortTypeValue={sortType}
onFilterOptionsToggle={() => { onFilterOptionsToggle={() => {
setFilterDrawerOpen(!filterDrawerOpen); setFilterDrawerOpen(!filterDrawerOpen);

Loading…
Cancel
Save