[questions][ui] update roles typeahead, add sticky search bar (#451)

This commit is contained in:
Jeff Sieu
2022-10-30 19:32:33 +08:00
committed by GitHub
parent de94958ce1
commit 389862feb3
6 changed files with 176 additions and 129 deletions

View File

@ -28,7 +28,6 @@ export default function ContributeQuestionCard({
}; };
return ( return (
<div>
<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="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"
@ -72,12 +71,11 @@ export default function ContributeQuestionCard({
Contribute Contribute
</h1> </h1>
</div> </div>
</button>
<ContributeQuestionDialog <ContributeQuestionDialog
show={showDraftDialog} show={showDraftDialog}
onCancel={handleDraftDialogCancel} onCancel={handleDraftDialogCancel}
onSubmit={onSubmit} onSubmit={onSubmit}
/> />
</div> </button>
); );
} }

View File

@ -1,13 +1,21 @@
import { ROLES } from '~/utils/questions/constants'; import { JobTitleLabels } from '~/components/shared/JobTitles';
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead'; import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
import ExpandedTypeahead from './ExpandedTypeahead'; import ExpandedTypeahead from './ExpandedTypeahead';
import type { FilterChoices } from '../filter/FilterSection';
export type RoleTypeaheadProps = Omit< export type RoleTypeaheadProps = Omit<
ExpandedTypeaheadProps, ExpandedTypeaheadProps,
'label' | 'onQueryChange' | 'options' 'label' | 'onQueryChange' | 'options'
>; >;
const ROLES: FilterChoices = Object.entries(JobTitleLabels).map(
([slug, label]) => ({
id: slug,
label,
value: slug,
}),
);
export default function RoleTypeahead(props: RoleTypeaheadProps) { export default function RoleTypeahead(props: RoleTypeaheadProps) {
return ( return (
<ExpandedTypeahead <ExpandedTypeahead

View File

@ -1,5 +1,6 @@
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useMemo } 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, Select, TextArea } from '@tih/ui';
@ -11,6 +12,7 @@ import FullScreenSpinner from '~/components/questions/FullScreenSpinner';
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 relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { useFormRegister } from '~/utils/questions/useFormRegister'; import { useFormRegister } from '~/utils/questions/useFormRegister';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
@ -52,6 +54,14 @@ export default function QuestionPage() {
{ questionId: questionId as string }, { questionId: questionId as string },
]); ]);
const relabeledAggregatedEncounters = useMemo(() => {
if (!aggregatedEncounters) {
return aggregatedEncounters;
}
return relabelQuestionAggregates(aggregatedEncounters);
}, [aggregatedEncounters]);
const utils = trpc.useContext(); const utils = trpc.useContext();
const { data: comments } = trpc.useQuery([ const { data: comments } = trpc.useQuery([
@ -138,11 +148,11 @@ export default function QuestionPage() {
<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}
companies={aggregatedEncounters?.companyCounts ?? {}} companies={relabeledAggregatedEncounters?.companyCounts ?? {}}
locations={aggregatedEncounters?.locationCounts ?? {}} locations={relabeledAggregatedEncounters?.locationCounts ?? {}}
questionId={question.id} questionId={question.id}
receivedCount={undefined} receivedCount={undefined}
roles={aggregatedEncounters?.roleCounts ?? {}} roles={relabeledAggregatedEncounters?.roleCounts ?? {}}
timestamp={question.seenAt.toLocaleDateString(undefined, { timestamp={question.seenAt.toLocaleDateString(undefined, {
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',

View File

@ -14,6 +14,7 @@ import QuestionSearchBar from '~/components/questions/QuestionSearchBar';
import CompanyTypeahead from '~/components/questions/typeahead/CompanyTypeahead'; import CompanyTypeahead from '~/components/questions/typeahead/CompanyTypeahead';
import LocationTypeahead from '~/components/questions/typeahead/LocationTypeahead'; import LocationTypeahead from '~/components/questions/typeahead/LocationTypeahead';
import RoleTypeahead from '~/components/questions/typeahead/RoleTypeahead'; import RoleTypeahead from '~/components/questions/typeahead/RoleTypeahead';
import { JobTitleLabels } from '~/components/shared/JobTitles';
import type { QuestionAge } from '~/utils/questions/constants'; import type { QuestionAge } from '~/utils/questions/constants';
import { SORT_TYPES } from '~/utils/questions/constants'; import { SORT_TYPES } from '~/utils/questions/constants';
@ -21,6 +22,7 @@ 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';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { import {
useSearchParam, useSearchParam,
useSearchParamSingle, useSearchParamSingle,
@ -176,7 +178,7 @@ export default function QuestionsBrowsePage() {
return undefined; return undefined;
} }
return questionsQueryData.pages.reduce( return questionsQueryData.pages.reduce(
(acc, page) => acc + page.data.length, (acc, page) => acc + (page.data.length as number),
0, 0,
); );
}, [questionsQueryData]); }, [questionsQueryData]);
@ -275,7 +277,7 @@ export default function QuestionsBrowsePage() {
return selectedRoles.map((role) => ({ return selectedRoles.map((role) => ({
checked: true, checked: true,
id: role, id: role,
label: role, label: JobTitleLabels[role as keyof typeof JobTitleLabels],
value: role, value: role,
})); }));
}, [selectedRoles]); }, [selectedRoles]);
@ -371,7 +373,7 @@ export default function QuestionsBrowsePage() {
setSelectedRoles([...selectedRoles, option.value]); setSelectedRoles([...selectedRoles, option.value]);
} else { } else {
setSelectedRoles( setSelectedRoles(
selectedCompanies.filter((role) => role !== option.value), selectedRoles.filter((role) => role !== option.value),
); );
} }
}} }}
@ -445,8 +447,7 @@ export default function QuestionsBrowsePage() {
<main className="flex flex-1 flex-col items-stretch"> <main className="flex flex-1 flex-col items-stretch">
<div className="flex h-full flex-1"> <div className="flex h-full flex-1">
<section className="flex min-h-0 flex-1 flex-col items-center overflow-auto"> <section className="flex min-h-0 flex-1 flex-col items-center overflow-auto">
<div className="flex min-h-0 max-w-3xl flex-1 p-4"> <div className="m-4 flex max-w-3xl flex-1 flex-col items-stretch justify-start gap-8">
<div className="flex flex-1 flex-col items-stretch justify-start gap-8">
<ContributeQuestionCard <ContributeQuestionCard
onSubmit={(data) => { onSubmit={(data) => {
createQuestion({ createQuestion({
@ -459,6 +460,7 @@ export default function QuestionsBrowsePage() {
}); });
}} }}
/> />
<div className="sticky top-0 border-b border-slate-300 bg-slate-50 py-4">
<QuestionSearchBar <QuestionSearchBar
sortOrderOptions={SORT_ORDERS} sortOrderOptions={SORT_ORDERS}
sortOrderValue={sortOrder} sortOrderValue={sortOrder}
@ -470,28 +472,29 @@ export default function QuestionsBrowsePage() {
onSortOrderChange={setSortOrder} onSortOrderChange={setSortOrder}
onSortTypeChange={setSortType} onSortTypeChange={setSortType}
/> />
</div>
<div className="flex flex-col gap-2 pb-4"> <div className="flex flex-col gap-2 pb-4">
{(questionsQueryData?.pages ?? []).flatMap( {(questionsQueryData?.pages ?? []).flatMap(
({ data: questions }) => ({ data: questions }) =>
questions.map((question) => ( questions.map((question) => {
const { companyCounts, locationCounts, roleCounts } =
relabelQuestionAggregates(
question.aggregatedQuestionEncounters,
);
return (
<QuestionOverviewCard <QuestionOverviewCard
key={question.id} key={question.id}
answerCount={question.numAnswers} answerCount={question.numAnswers}
companies={ companies={companyCounts}
question.aggregatedQuestionEncounters.companyCounts
}
content={question.content} content={question.content}
href={`/questions/${question.id}/${createSlug( href={`/questions/${question.id}/${createSlug(
question.content, question.content,
)}`} )}`}
locations={ locations={locationCounts}
question.aggregatedQuestionEncounters.locationCounts
}
questionId={question.id} questionId={question.id}
receivedCount={question.receivedCount} receivedCount={question.receivedCount}
roles={ roles={roleCounts}
question.aggregatedQuestionEncounters.roleCounts
}
timestamp={question.seenAt.toLocaleDateString( timestamp={question.seenAt.toLocaleDateString(
undefined, undefined,
{ {
@ -502,7 +505,8 @@ export default function QuestionsBrowsePage() {
type={question.type} type={question.type}
upvoteCount={question.numVotes} upvoteCount={question.numVotes}
/> />
)), );
}),
)} )}
<Button <Button
disabled={!hasNextPage || isFetchingNextPage} disabled={!hasNextPage || isFetchingNextPage}
@ -522,7 +526,6 @@ export default function QuestionsBrowsePage() {
)} )}
</div> </div>
</div> </div>
</div>
</section> </section>
<aside className="hidden w-[300px] overflow-y-auto border-l bg-white py-4 lg:block"> <aside className="hidden w-[300px] overflow-y-auto border-l bg-white py-4 lg:block">
<h2 className="px-4 text-xl font-semibold">Filter by</h2> <h2 className="px-4 text-xl font-semibold">Filter by</h2>

View File

@ -15,6 +15,7 @@ import DeleteListDialog from '~/components/questions/DeleteListDialog';
import { Button } from '~/../../../packages/ui/dist'; import { Button } from '~/../../../packages/ui/dist';
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 relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
export default function ListPage() { export default function ListPage() {
@ -172,24 +173,24 @@ export default function ListPage() {
{lists?.[selectedListIndex] && ( {lists?.[selectedListIndex] && (
<div className="flex flex-col gap-4 pb-4"> <div className="flex flex-col gap-4 pb-4">
{lists[selectedListIndex].questionEntries.map( {lists[selectedListIndex].questionEntries.map(
({ question, id: entryId }) => ( ({ question, id: entryId }) => {
const { companyCounts, locationCounts, roleCounts } =
relabelQuestionAggregates(
question.aggregatedQuestionEncounters,
);
return (
<QuestionListCard <QuestionListCard
key={question.id} key={question.id}
companies={ companies={companyCounts}
question.aggregatedQuestionEncounters.companyCounts
}
content={question.content} content={question.content}
href={`/questions/${question.id}/${createSlug( href={`/questions/${question.id}/${createSlug(
question.content, question.content,
)}`} )}`}
locations={ locations={locationCounts}
question.aggregatedQuestionEncounters.locationCounts
}
questionId={question.id} questionId={question.id}
receivedCount={question.receivedCount} receivedCount={question.receivedCount}
roles={ roles={roleCounts}
question.aggregatedQuestionEncounters.roleCounts
}
timestamp={question.seenAt.toLocaleDateString( timestamp={question.seenAt.toLocaleDateString(
undefined, undefined,
{ {
@ -202,7 +203,8 @@ export default function ListPage() {
deleteQuestionEntry({ id: entryId }); deleteQuestionEntry({ id: entryId });
}} }}
/> />
), );
},
)} )}
{lists[selectedListIndex].questionEntries?.length === 0 && ( {lists[selectedListIndex].questionEntries?.length === 0 && (
<div className="flex w-full items-center justify-center gap-2 rounded-md border border-slate-300 bg-slate-200 p-4 text-slate-600"> <div className="flex w-full items-center justify-center gap-2 rounded-md border border-slate-300 bg-slate-200 p-4 text-slate-600">

View File

@ -0,0 +1,26 @@
import { JobTitleLabels } from '~/components/shared/JobTitles';
import type { AggregatedQuestionEncounter } from '~/types/questions';
export default function relabelQuestionAggregates({
locationCounts,
companyCounts,
roleCounts,
latestSeenAt,
}: AggregatedQuestionEncounter) {
const newRoleCounts = Object.fromEntries(
Object.entries(roleCounts).map(([roleId, count]) => [
JobTitleLabels[roleId as keyof typeof JobTitleLabels],
count,
]),
);
const relabeledAggregate: AggregatedQuestionEncounter = {
companyCounts,
latestSeenAt,
locationCounts,
roleCounts: newRoleCounts,
};
return relabeledAggregate;
}