[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,56 +28,54 @@ 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" onClick={handleOpenContribute}>
onClick={handleOpenContribute}> <TextInput
<TextInput disabled={true}
disabled={true} isLabelHidden={true}
isLabelHidden={true} label="Question"
label="Question" placeholder="Contribute a question"
placeholder="Contribute a question" onChange={handleOpenContribute}
onChange={handleOpenContribute} />
/> <div className="flex flex-wrap items-end justify-center gap-x-2">
<div className="flex flex-wrap items-end justify-center gap-x-2"> <div className="min-w-[150px] flex-1">
<div className="min-w-[150px] flex-1"> <TextInput
<TextInput disabled={true}
disabled={true} label="Company"
label="Company" startAddOn={BuildingOffice2Icon}
startAddOn={BuildingOffice2Icon} startAddOnType="icon"
startAddOnType="icon" onChange={handleOpenContribute}
onChange={handleOpenContribute} />
/>
</div>
<div className="min-w-[150px] flex-1">
<TextInput
disabled={true}
label="Question type"
startAddOn={QuestionMarkCircleIcon}
startAddOnType="icon"
onChange={handleOpenContribute}
/>
</div>
<div className="min-w-[150px] flex-1">
<TextInput
disabled={true}
label="Date"
startAddOn={CalendarDaysIcon}
startAddOnType="icon"
onChange={handleOpenContribute}
/>
</div>
<h1 className="bg-primary-600 hover:bg-primary-700 rounded-full px-3 py-2 text-white">
Contribute
</h1>
</div> </div>
</button> <div className="min-w-[150px] flex-1">
<TextInput
disabled={true}
label="Question type"
startAddOn={QuestionMarkCircleIcon}
startAddOnType="icon"
onChange={handleOpenContribute}
/>
</div>
<div className="min-w-[150px] flex-1">
<TextInput
disabled={true}
label="Date"
startAddOn={CalendarDaysIcon}
startAddOnType="icon"
onChange={handleOpenContribute}
/>
</div>
<h1 className="bg-primary-600 hover:bg-primary-700 rounded-full px-3 py-2 text-white">
Contribute
</h1>
</div>
<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,20 +447,20 @@ 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({ companyId: data.company,
companyId: data.company, content: data.questionContent,
content: data.questionContent, location: data.location,
location: data.location, questionType: data.questionType,
questionType: data.questionType, role: data.role,
role: data.role, seenAt: data.date,
seenAt: data.date, });
}); }}
}} />
/> <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 className="flex flex-col gap-2 pb-4"> </div>
{(questionsQueryData?.pages ?? []).flatMap( <div className="flex flex-col gap-2 pb-4">
({ data: questions }) => {(questionsQueryData?.pages ?? []).flatMap(
questions.map((question) => ( ({ data: questions }) =>
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,25 +505,25 @@ export default function QuestionsBrowsePage() {
type={question.type} type={question.type}
upvoteCount={question.numVotes} upvoteCount={question.numVotes}
/> />
)), );
)} }),
<Button )}
disabled={!hasNextPage || isFetchingNextPage} <Button
isLoading={isFetchingNextPage} disabled={!hasNextPage || isFetchingNextPage}
label={hasNextPage ? 'Load more' : 'Nothing more to load'} isLoading={isFetchingNextPage}
variant="tertiary" label={hasNextPage ? 'Load more' : 'Nothing more to load'}
onClick={() => { variant="tertiary"
fetchNextPage(); onClick={() => {
}} fetchNextPage();
/> }}
{questionCount === 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"> {questionCount === 0 && (
<NoSymbolIcon className="h-6 w-6" /> <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">
<p>Nothing found.</p> <NoSymbolIcon className="h-6 w-6" />
{hasFilters && <p>Try changing your search criteria.</p>} <p>Nothing found.</p>
</div> {hasFilters && <p>Try changing your search criteria.</p>}
)} </div>
</div> )}
</div> </div>
</div> </div>
</section> </section>

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,37 +173,38 @@ 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 }) => {
<QuestionListCard const { companyCounts, locationCounts, roleCounts } =
key={question.id} relabelQuestionAggregates(
companies={ question.aggregatedQuestionEncounters,
question.aggregatedQuestionEncounters.companyCounts );
}
content={question.content} return (
href={`/questions/${question.id}/${createSlug( <QuestionListCard
question.content, key={question.id}
)}`} companies={companyCounts}
locations={ content={question.content}
question.aggregatedQuestionEncounters.locationCounts href={`/questions/${question.id}/${createSlug(
} question.content,
questionId={question.id} )}`}
receivedCount={question.receivedCount} locations={locationCounts}
roles={ questionId={question.id}
question.aggregatedQuestionEncounters.roleCounts receivedCount={question.receivedCount}
} roles={roleCounts}
timestamp={question.seenAt.toLocaleDateString( timestamp={question.seenAt.toLocaleDateString(
undefined, undefined,
{ {
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',
}, },
)} )}
type={question.type} type={question.type}
onDelete={() => { onDelete={() => {
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;
}