mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 20:52:00 +08:00
[questions][ui] update roles typeahead, add sticky search bar (#451)
This commit is contained in:
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
26
apps/portal/src/utils/questions/relabelQuestionAggregates.ts
Normal file
26
apps/portal/src/utils/questions/relabelQuestionAggregates.ts
Normal 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;
|
||||||
|
}
|
Reference in New Issue
Block a user