[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 (
<div>
<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"
type="button"
onClick={handleOpenContribute}>
<TextInput
disabled={true}
isLabelHidden={true}
label="Question"
placeholder="Contribute a question"
onChange={handleOpenContribute}
/>
<div className="flex flex-wrap items-end justify-center gap-x-2">
<div className="min-w-[150px] flex-1">
<TextInput
disabled={true}
label="Company"
startAddOn={BuildingOffice2Icon}
startAddOnType="icon"
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>
<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"
type="button"
onClick={handleOpenContribute}>
<TextInput
disabled={true}
isLabelHidden={true}
label="Question"
placeholder="Contribute a question"
onChange={handleOpenContribute}
/>
<div className="flex flex-wrap items-end justify-center gap-x-2">
<div className="min-w-[150px] flex-1">
<TextInput
disabled={true}
label="Company"
startAddOn={BuildingOffice2Icon}
startAddOnType="icon"
onChange={handleOpenContribute}
/>
</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
show={showDraftDialog}
onCancel={handleDraftDialogCancel}
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 ExpandedTypeahead from './ExpandedTypeahead';
import type { FilterChoices } from '../filter/FilterSection';
export type RoleTypeaheadProps = Omit<
ExpandedTypeaheadProps,
'label' | 'onQueryChange' | 'options'
>;
const ROLES: FilterChoices = Object.entries(JobTitleLabels).map(
([slug, label]) => ({
id: slug,
label,
value: slug,
}),
);
export default function RoleTypeahead(props: RoleTypeaheadProps) {
return (
<ExpandedTypeahead

View File

@ -1,5 +1,6 @@
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
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 createSlug from '~/utils/questions/createSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { useFormRegister } from '~/utils/questions/useFormRegister';
import { trpc } from '~/utils/trpc';
@ -52,6 +54,14 @@ export default function QuestionPage() {
{ questionId: questionId as string },
]);
const relabeledAggregatedEncounters = useMemo(() => {
if (!aggregatedEncounters) {
return aggregatedEncounters;
}
return relabelQuestionAggregates(aggregatedEncounters);
}, [aggregatedEncounters]);
const utils = trpc.useContext();
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">
<FullQuestionCard
{...question}
companies={aggregatedEncounters?.companyCounts ?? {}}
locations={aggregatedEncounters?.locationCounts ?? {}}
companies={relabeledAggregatedEncounters?.companyCounts ?? {}}
locations={relabeledAggregatedEncounters?.locationCounts ?? {}}
questionId={question.id}
receivedCount={undefined}
roles={aggregatedEncounters?.roleCounts ?? {}}
roles={relabeledAggregatedEncounters?.roleCounts ?? {}}
timestamp={question.seenAt.toLocaleDateString(undefined, {
month: 'short',
year: 'numeric',

View File

@ -14,6 +14,7 @@ import QuestionSearchBar from '~/components/questions/QuestionSearchBar';
import CompanyTypeahead from '~/components/questions/typeahead/CompanyTypeahead';
import LocationTypeahead from '~/components/questions/typeahead/LocationTypeahead';
import RoleTypeahead from '~/components/questions/typeahead/RoleTypeahead';
import { JobTitleLabels } from '~/components/shared/JobTitles';
import type { QuestionAge } 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 { QUESTION_AGES, QUESTION_TYPES } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import {
useSearchParam,
useSearchParamSingle,
@ -176,7 +178,7 @@ export default function QuestionsBrowsePage() {
return undefined;
}
return questionsQueryData.pages.reduce(
(acc, page) => acc + page.data.length,
(acc, page) => acc + (page.data.length as number),
0,
);
}, [questionsQueryData]);
@ -275,7 +277,7 @@ export default function QuestionsBrowsePage() {
return selectedRoles.map((role) => ({
checked: true,
id: role,
label: role,
label: JobTitleLabels[role as keyof typeof JobTitleLabels],
value: role,
}));
}, [selectedRoles]);
@ -371,7 +373,7 @@ export default function QuestionsBrowsePage() {
setSelectedRoles([...selectedRoles, option.value]);
} else {
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">
<div className="flex h-full flex-1">
<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="flex flex-1 flex-col items-stretch justify-start gap-8">
<ContributeQuestionCard
onSubmit={(data) => {
createQuestion({
companyId: data.company,
content: data.questionContent,
location: data.location,
questionType: data.questionType,
role: data.role,
seenAt: data.date,
});
}}
/>
<div className="m-4 flex max-w-3xl flex-1 flex-col items-stretch justify-start gap-8">
<ContributeQuestionCard
onSubmit={(data) => {
createQuestion({
companyId: data.company,
content: data.questionContent,
location: data.location,
questionType: data.questionType,
role: data.role,
seenAt: data.date,
});
}}
/>
<div className="sticky top-0 border-b border-slate-300 bg-slate-50 py-4">
<QuestionSearchBar
sortOrderOptions={SORT_ORDERS}
sortOrderValue={sortOrder}
@ -470,28 +472,29 @@ export default function QuestionsBrowsePage() {
onSortOrderChange={setSortOrder}
onSortTypeChange={setSortType}
/>
<div className="flex flex-col gap-2 pb-4">
{(questionsQueryData?.pages ?? []).flatMap(
({ data: questions }) =>
questions.map((question) => (
</div>
<div className="flex flex-col gap-2 pb-4">
{(questionsQueryData?.pages ?? []).flatMap(
({ data: questions }) =>
questions.map((question) => {
const { companyCounts, locationCounts, roleCounts } =
relabelQuestionAggregates(
question.aggregatedQuestionEncounters,
);
return (
<QuestionOverviewCard
key={question.id}
answerCount={question.numAnswers}
companies={
question.aggregatedQuestionEncounters.companyCounts
}
companies={companyCounts}
content={question.content}
href={`/questions/${question.id}/${createSlug(
question.content,
)}`}
locations={
question.aggregatedQuestionEncounters.locationCounts
}
locations={locationCounts}
questionId={question.id}
receivedCount={question.receivedCount}
roles={
question.aggregatedQuestionEncounters.roleCounts
}
roles={roleCounts}
timestamp={question.seenAt.toLocaleDateString(
undefined,
{
@ -502,25 +505,25 @@ export default function QuestionsBrowsePage() {
type={question.type}
upvoteCount={question.numVotes}
/>
)),
)}
<Button
disabled={!hasNextPage || isFetchingNextPage}
isLoading={isFetchingNextPage}
label={hasNextPage ? 'Load more' : 'Nothing more to load'}
variant="tertiary"
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">
<NoSymbolIcon className="h-6 w-6" />
<p>Nothing found.</p>
{hasFilters && <p>Try changing your search criteria.</p>}
</div>
)}
</div>
);
}),
)}
<Button
disabled={!hasNextPage || isFetchingNextPage}
isLoading={isFetchingNextPage}
label={hasNextPage ? 'Load more' : 'Nothing more to load'}
variant="tertiary"
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">
<NoSymbolIcon className="h-6 w-6" />
<p>Nothing found.</p>
{hasFilters && <p>Try changing your search criteria.</p>}
</div>
)}
</div>
</div>
</section>

View File

@ -15,6 +15,7 @@ import DeleteListDialog from '~/components/questions/DeleteListDialog';
import { Button } from '~/../../../packages/ui/dist';
import { APP_TITLE } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { trpc } from '~/utils/trpc';
export default function ListPage() {
@ -172,37 +173,38 @@ export default function ListPage() {
{lists?.[selectedListIndex] && (
<div className="flex flex-col gap-4 pb-4">
{lists[selectedListIndex].questionEntries.map(
({ question, id: entryId }) => (
<QuestionListCard
key={question.id}
companies={
question.aggregatedQuestionEncounters.companyCounts
}
content={question.content}
href={`/questions/${question.id}/${createSlug(
question.content,
)}`}
locations={
question.aggregatedQuestionEncounters.locationCounts
}
questionId={question.id}
receivedCount={question.receivedCount}
roles={
question.aggregatedQuestionEncounters.roleCounts
}
timestamp={question.seenAt.toLocaleDateString(
undefined,
{
month: 'short',
year: 'numeric',
},
)}
type={question.type}
onDelete={() => {
deleteQuestionEntry({ id: entryId });
}}
/>
),
({ question, id: entryId }) => {
const { companyCounts, locationCounts, roleCounts } =
relabelQuestionAggregates(
question.aggregatedQuestionEncounters,
);
return (
<QuestionListCard
key={question.id}
companies={companyCounts}
content={question.content}
href={`/questions/${question.id}/${createSlug(
question.content,
)}`}
locations={locationCounts}
questionId={question.id}
receivedCount={question.receivedCount}
roles={roleCounts}
timestamp={question.seenAt.toLocaleDateString(
undefined,
{
month: 'short',
year: 'numeric',
},
)}
type={question.type}
onDelete={() => {
deleteQuestionEntry({ id: entryId });
}}
/>
);
},
)}
{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">

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;
}