[questions][feat] browse: add sort by, order by

pull/411/head
Jeff Sieu 3 years ago
parent 01495e8f6a
commit 2e7131583c

@ -4,29 +4,41 @@ import {
} from '@heroicons/react/24/outline';
import { Button, Select, TextInput } from '@tih/ui';
export type SortOption = {
export type SortOption<Value> = {
label: string;
value: string;
value: Value;
};
export type QuestionSearchBarProps<SortOptions extends Array<SortOption>> = {
onFilterOptionsToggle: () => void;
onSortChange?: (sortValue: SortOptions[number]['value']) => void;
sortOptions: SortOptions;
sortValue: SortOptions[number]['value'];
type SortOrderProps<SortOrder> = {
onSortOrderChange?: (sortValue: SortOrder) => void;
sortOrderOptions: ReadonlyArray<SortOption<SortOrder>>;
sortOrderValue: SortOrder;
};
export default function QuestionSearchBar<
SortOptions extends Array<SortOption>,
>({
onSortChange,
sortOptions,
sortValue,
type SortTypeProps<SortType> = {
onSortTypeChange?: (sortType: SortType) => void;
sortTypeOptions: ReadonlyArray<SortOption<SortType>>;
sortTypeValue: SortType;
};
export type QuestionSearchBarProps<SortType, SortOrder> =
SortOrderProps<SortOrder> &
SortTypeProps<SortType> & {
onFilterOptionsToggle: () => void;
};
export default function QuestionSearchBar<SortType, SortOrder>({
onSortOrderChange,
sortOrderOptions,
sortOrderValue,
onSortTypeChange,
sortTypeOptions,
sortTypeValue,
onFilterOptionsToggle,
}: QuestionSearchBarProps<SortOptions>) {
}: QuestionSearchBarProps<SortType, SortOrder>) {
return (
<div className="flex items-center gap-4">
<div className="flex-1">
<div className="flex flex-col items-stretch gap-x-4 gap-y-2 lg:flex-row lg:items-end">
<div className="flex-1 ">
<TextInput
isLabelHidden={true}
label="Search by content"
@ -35,27 +47,48 @@ export default function QuestionSearchBar<
startAddOnType="icon"
/>
</div>
<div className="flex items-center gap-2">
<span aria-hidden={true} className="align-middle text-sm font-medium">
Sort by:
</span>
<Select
display="inline"
isLabelHidden={true}
label="Sort by"
options={sortOptions}
value={sortValue}
onChange={onSortChange}
/>
</div>
<div className="lg:hidden">
<Button
addonPosition="start"
icon={AdjustmentsHorizontalIcon}
label="Filter options"
variant="tertiary"
onClick={onFilterOptionsToggle}
/>
<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 className="lg:hidden">
<Button
addonPosition="start"
icon={AdjustmentsHorizontalIcon}
label="Filter options"
variant="tertiary"
onClick={onFilterOptionsToggle}
/>
</div>
</div>
</div>
);

@ -19,7 +19,6 @@ import {
import CompanyTypeahead from '../typeahead/CompanyTypeahead';
import LocationTypeahead from '../typeahead/LocationTypeahead';
import RoleTypeahead from '../typeahead/RoleTypeahead';
import CompaniesTypeahead from '../../shared/CompaniesTypeahead';
import type { Month } from '../../shared/MonthYearPicker';
import MonthYearPicker from '../../shared/MonthYearPicker';

@ -13,6 +13,8 @@ import FilterSection from '~/components/questions/filter/FilterSection';
import QuestionSearchBar from '~/components/questions/QuestionSearchBar';
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 { ROLES } from '~/utils/questions/constants';
import {
@ -23,24 +25,25 @@ import {
} from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug';
import {
useSearchFilter,
useSearchFilterSingle,
} from '~/utils/questions/useSearchFilter';
useSearchParam,
useSearchParamSingle,
} from '~/utils/questions/useSearchParam';
import { trpc } from '~/utils/trpc';
import { SortOrder, SortType } from '~/types/questions.d';
import { SortType } from '~/types/questions.d';
import { SortOrder } from '~/types/questions.d';
export default function QuestionsBrowsePage() {
const router = useRouter();
const [selectedCompanies, setSelectedCompanies, areCompaniesInitialized] =
useSearchFilter('companies');
useSearchParam('companies');
const [
selectedQuestionTypes,
setSelectedQuestionTypes,
areQuestionTypesInitialized,
] = useSearchFilter<QuestionsQuestionType>('questionTypes', {
queryParamToValue: (param) => {
] = useSearchParam<QuestionsQuestionType>('questionTypes', {
stringToParam: (param) => {
const uppercaseParam = param.toUpperCase();
return (
QUESTION_TYPES.find(
@ -53,9 +56,9 @@ export default function QuestionsBrowsePage() {
selectedQuestionAge,
setSelectedQuestionAge,
isQuestionAgeInitialized,
] = useSearchFilterSingle<QuestionAge>('questionAge', {
] = useSearchParamSingle<QuestionAge>('questionAge', {
defaultValue: 'all',
queryParamToValue: (param) => {
stringToParam: (param) => {
const uppercaseParam = param.toUpperCase();
return (
QUESTION_AGES.find(
@ -66,9 +69,57 @@ export default function QuestionsBrowsePage() {
});
const [selectedRoles, setSelectedRoles, areRolesInitialized] =
useSearchFilter('roles');
useSearchParam('roles');
const [selectedLocations, setSelectedLocations, areLocationsInitialized] =
useSearchFilter('locations');
useSearchParam('locations');
const [sortOrder, setSortOrder, isSortOrderInitialized] =
useSearchParamSingle<SortOrder>('sortOrder', {
defaultValue: SortOrder.DESC,
paramToString: (value) => {
if (value === SortOrder.ASC) {
return 'ASC';
}
if (value === SortOrder.DESC) {
return 'DESC';
}
return null;
},
stringToParam: (param) => {
const uppercaseParam = param.toUpperCase();
if (uppercaseParam === 'ASC') {
return SortOrder.ASC;
}
if (uppercaseParam === 'DESC') {
return SortOrder.DESC;
}
return null;
},
});
const [sortType, setSortType, isSortTypeInitialized] =
useSearchParamSingle<SortType>('sortType', {
defaultValue: SortType.TOP,
paramToString: (value) => {
if (value === SortType.NEW) {
return 'NEW';
}
if (value === SortType.TOP) {
return 'TOP';
}
return null;
},
stringToParam: (param) => {
const uppercaseParam = param.toUpperCase();
if (uppercaseParam === 'NEW') {
return SortType.NEW;
}
if (uppercaseParam === 'TOP') {
return SortType.TOP;
}
return null;
},
});
const hasFilters = useMemo(
() =>
@ -106,8 +157,8 @@ export default function QuestionsBrowsePage() {
locations: selectedLocations,
questionTypes: selectedQuestionTypes,
roles: selectedRoles,
sortOrder: SortOrder.DESC,
sortType: SortType.NEW,
sortOrder,
sortType,
startDate,
},
],
@ -164,13 +215,15 @@ export default function QuestionsBrowsePage() {
}));
}, [selectedLocations]);
const areFiltersInitialized = useMemo(() => {
const areSearchOptionsInitialized = useMemo(() => {
return (
areCompaniesInitialized &&
areQuestionTypesInitialized &&
isQuestionAgeInitialized &&
areRolesInitialized &&
areLocationsInitialized
areLocationsInitialized &&
isSortTypeInitialized &&
isSortOrderInitialized
);
}, [
areCompaniesInitialized,
@ -178,11 +231,13 @@ export default function QuestionsBrowsePage() {
isQuestionAgeInitialized,
areRolesInitialized,
areLocationsInitialized,
isSortTypeInitialized,
isSortOrderInitialized,
]);
const { pathname } = router;
useEffect(() => {
if (areFiltersInitialized) {
if (areSearchOptionsInitialized) {
// Router.replace used instead of router.replace to avoid
// the page reloading itself since the router.replace
// callback changes on every page load
@ -194,13 +249,14 @@ export default function QuestionsBrowsePage() {
questionAge: selectedQuestionAge,
questionTypes: selectedQuestionTypes,
roles: selectedRoles,
sortOrder,
},
});
setLoaded(true);
}
}, [
areFiltersInitialized,
areSearchOptionsInitialized,
loaded,
pathname,
selectedCompanies,
@ -208,6 +264,7 @@ export default function QuestionsBrowsePage() {
selectedLocations,
selectedQuestionAge,
selectedQuestionTypes,
sortOrder,
]);
if (!loaded) {
@ -360,7 +417,7 @@ export default function QuestionsBrowsePage() {
<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-4">
<div className="flex flex-1 flex-col items-stretch justify-start gap-8">
<ContributeQuestionCard
onSubmit={(data) => {
createQuestion({
@ -374,24 +431,15 @@ export default function QuestionsBrowsePage() {
}}
/>
<QuestionSearchBar
sortOptions={[
{
label: 'Most recent',
value: 'most-recent',
},
{
label: 'Most upvotes',
value: 'most-upvotes',
},
]}
sortValue="most-recent"
sortOrderOptions={SORT_ORDERS}
sortOrderValue={sortOrder}
sortTypeOptions={SORT_TYPES}
sortTypeValue={sortType}
onFilterOptionsToggle={() => {
setFilterDrawerOpen(!filterDrawerOpen);
}}
onSortChange={(value) => {
// eslint-disable-next-line no-console
console.log(value);
}}
onSortOrderChange={setSortOrder}
onSortTypeChange={setSortType}
/>
<div className="flex flex-col gap-4 pb-4">
{(questions ?? []).map((question) => (

@ -2,6 +2,8 @@ import { QuestionsQuestionType } from '@prisma/client';
import type { FilterChoices } from '~/components/questions/filter/FilterSection';
import { SortOrder, SortType } from '~/types/questions.d';
export const APP_TITLE = 'Questions Bank';
export const COMPANIES: FilterChoices = [
@ -102,6 +104,28 @@ export const ROLES: FilterChoices = [
},
] as const;
export const SORT_ORDERS = [
{
label: 'Ascending',
value: SortOrder.ASC,
},
{
label: 'Descending',
value: SortOrder.DESC,
},
];
export const SORT_TYPES = [
{
label: 'New',
value: SortType.NEW,
},
{
label: 'Top',
value: SortType.TOP,
},
];
export const SAMPLE_QUESTION = {
answerCount: 10,
commentCount: 10,

@ -1,14 +1,27 @@
import { useRouter } from 'next/router';
import { useCallback, useEffect, useState } from 'react';
export const useSearchFilter = <Value extends string = string>(
type SearchParamOptions<Value> = [Value] extends [string]
? {
defaultValues?: Array<Value>;
paramToString?: (value: Value) => string | null;
stringToParam?: (param: string) => Value | null;
}
: {
defaultValues?: Array<Value>;
paramToString: (value: Value) => string | null;
stringToParam: (param: string) => Value | null;
};
export const useSearchParam = <Value = string>(
name: string,
opts: {
defaultValues?: Array<Value>;
queryParamToValue?: (param: string) => Value | null;
} = {},
opts?: SearchParamOptions<Value>,
) => {
const { defaultValues, queryParamToValue = (param) => param } = opts;
const {
defaultValues,
stringToParam = (param: string) => param,
paramToString: valueToQueryParam = (value: Value) => String(value),
} = opts ?? {};
const [isInitialized, setIsInitialized] = useState(false);
const router = useRouter();
@ -22,7 +35,7 @@ export const useSearchFilter = <Value extends string = string>(
const queryValues = Array.isArray(query) ? query : [query];
setFilters(
queryValues
.map(queryParamToValue)
.map(stringToParam)
.filter((value) => value !== null) as Array<Value>,
);
} else {
@ -35,31 +48,35 @@ export const useSearchFilter = <Value extends string = string>(
}
setIsInitialized(true);
}
}, [isInitialized, name, queryParamToValue, router]);
}, [isInitialized, name, stringToParam, router]);
const setFiltersCallback = useCallback(
(newFilters: Array<Value>) => {
setFilters(newFilters);
localStorage.setItem(name, JSON.stringify(newFilters));
localStorage.setItem(
name,
JSON.stringify(
newFilters.map(valueToQueryParam).filter((param) => param !== null),
),
);
},
[name],
[name, valueToQueryParam],
);
return [filters, setFiltersCallback, isInitialized] as const;
};
export const useSearchFilterSingle = <Value extends string = string>(
export const useSearchParamSingle = <Value = string>(
name: string,
opts: {
opts?: Omit<SearchParamOptions<Value>, 'defaultValues'> & {
defaultValue?: Value;
queryParamToValue?: (param: string) => Value | null;
} = {},
},
) => {
const { defaultValue, queryParamToValue } = opts;
const [filters, setFilters, isInitialized] = useSearchFilter(name, {
const { defaultValue, ...restOpts } = opts ?? {};
const [filters, setFilters, isInitialized] = useSearchParam<Value>(name, {
defaultValues: defaultValue !== undefined ? [defaultValue] : undefined,
queryParamToValue,
});
...restOpts,
} as SearchParamOptions<Value>);
return [
filters[0],
Loading…
Cancel
Save