[questions][feat] Add question lists (#438)

Co-authored-by: wlren <weilinwork99@gmail.com>
pull/439/head
Jeff Sieu 2 years ago committed by GitHub
parent 839eb31d65
commit 87aa16929b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,160 @@
import clsx from 'clsx';
import type { PropsWithChildren } from 'react';
import { useMemo } from 'react';
import { Fragment, useRef, useState } from 'react';
import { Menu, Transition } from '@headlessui/react';
import { CheckIcon, HeartIcon } from '@heroicons/react/20/solid';
import { trpc } from '~/utils/trpc';
export type AddToListDropdownProps = {
questionId: string;
};
export default function AddToListDropdown({
questionId,
}: AddToListDropdownProps) {
const [menuOpened, setMenuOpened] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const utils = trpc.useContext();
const { data: lists } = trpc.useQuery(['questions.lists.getListsByUser']);
const listsWithQuestionData = useMemo(() => {
return lists?.map((list) => ({
...list,
hasQuestion: list.questionEntries.some(
(entry) => entry.question.id === questionId,
),
}));
}, [lists, questionId]);
const { mutateAsync: addQuestionToList } = trpc.useMutation(
'questions.lists.createQuestionEntry',
{
// TODO: Add optimistic update
onSuccess: () => {
utils.invalidateQueries(['questions.lists.getListsByUser']);
},
},
);
const { mutateAsync: removeQuestionFromList } = trpc.useMutation(
'questions.lists.deleteQuestionEntry',
{
// TODO: Add optimistic update
onSuccess: () => {
utils.invalidateQueries(['questions.lists.getListsByUser']);
},
},
);
const addClickOutsideListener = () => {
document.addEventListener('click', handleClickOutside, true);
};
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
setMenuOpened(false);
document.removeEventListener('click', handleClickOutside, true);
}
};
const handleAddToList = async (listId: string) => {
await addQuestionToList({
listId,
questionId,
});
};
const handleDeleteFromList = async (listId: string) => {
const list = listsWithQuestionData?.find(
(listWithQuestion) => listWithQuestion.id === listId,
);
if (!list) {
return;
}
const entry = list.questionEntries.find(
(questionEntry) => questionEntry.question.id === questionId,
);
if (!entry) {
return;
}
await removeQuestionFromList({
id: entry.id,
});
};
const CustomMenuButton = ({ children }: PropsWithChildren<unknown>) => (
<button
className="focus:ring-primary-500 inline-flex w-full justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-100"
type="button"
onClick={() => {
addClickOutsideListener();
setMenuOpened(!menuOpened);
}}>
{children}
</button>
);
return (
<Menu ref={ref} as="div" className="relative inline-block text-left">
<div>
<Menu.Button as={CustomMenuButton}>
<HeartIcon aria-hidden="true" className="-ml-1 mr-2 h-5 w-5" />
Add to List
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
show={menuOpened}>
<Menu.Items
className="absolute right-0 z-10 mt-2 w-56 origin-top-right divide-y divide-slate-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
static={true}>
{menuOpened && (
<>
{(listsWithQuestionData ?? []).map((list) => (
<div key={list.id} className="py-1">
<Menu.Item>
{({ active }) => (
<button
className={clsx(
active
? 'bg-slate-100 text-slate-900'
: 'text-slate-700',
'group flex w-full items-center px-4 py-2 text-sm',
)}
type="button"
onClick={() => {
if (list.hasQuestion) {
handleDeleteFromList(list.id);
} else {
handleAddToList(list.id);
}
}}>
{list.hasQuestion && (
<CheckIcon
aria-hidden="true"
className="mr-3 h-5 w-5 text-slate-400 group-hover:text-slate-500"
/>
)}
{list.name}
</button>
)}
</Menu.Item>
</div>
))}
</>
)}
</Menu.Items>
</Transition>
</Menu>
);
}

@ -61,7 +61,7 @@ export default function ContributeQuestionDialog({
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<Dialog.Panel className="relative w-full max-w-5xl transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8">
<div className="bg-white p-6 pt-5 sm:pb-4">
<div className="bg-white px-6 pt-5">
<div className="flex flex-1 items-stretch">
<div className="mt-3 w-full sm:mt-0 sm:text-left">
<Dialog.Title

@ -0,0 +1,75 @@
import { useForm } from 'react-hook-form';
import { Button, Dialog, TextInput } from '@tih/ui';
import { useFormRegister } from '~/utils/questions/useFormRegister';
export type CreateListFormData = {
name: string;
};
export type CreateListDialogProps = {
onCancel: () => void;
onSubmit: (data: CreateListFormData) => Promise<void>;
show: boolean;
};
export default function CreateListDialog({
show,
onCancel,
onSubmit,
}: CreateListDialogProps) {
const {
register: formRegister,
handleSubmit,
formState: { isSubmitting },
reset,
} = useForm<CreateListFormData>();
const register = useFormRegister(formRegister);
const handleDialogCancel = () => {
onCancel();
reset();
};
return (
<Dialog
isShown={show}
primaryButton={undefined}
title="Create question list"
onClose={handleDialogCancel}>
<form
className="mt-5 gap-2 sm:flex sm:items-center"
onSubmit={handleSubmit(async (data) => {
await onSubmit(data);
reset();
})}>
<div className="w-full sm:max-w-xs">
<TextInput
id="listName"
isLabelHidden={true}
{...register('name')}
autoComplete="off"
label="Name"
placeholder="List name"
type="text"
/>
</div>
<Button
display="inline"
label="Cancel"
size="md"
variant="tertiary"
onClick={handleDialogCancel}
/>
<Button
display="inline"
isLoading={isSubmitting}
label="Create"
size="md"
type="submit"
variant="primary"
/>
</form>
</Dialog>
);
}

@ -0,0 +1,29 @@
import { Button, Dialog } from '@tih/ui';
export type DeleteListDialogProps = {
onCancel: () => void;
onDelete: () => void;
show: boolean;
};
export default function DeleteListDialog({
show,
onCancel,
onDelete,
}: DeleteListDialogProps) {
return (
<Dialog
isShown={show}
primaryButton={
<Button label="Delete" variant="primary" onClick={onDelete} />
}
secondaryButton={
<Button label="Cancel" variant="tertiary" onClick={onCancel} />
}
title="Delete List"
onClose={onCancel}>
<p>
Are you sure you want to delete this list? This action cannot be undone.
</p>
</Dialog>
);
}

@ -118,7 +118,7 @@ export default function LandingComponent({
onClick={() => {
if (company !== undefined && location !== undefined) {
return handleLandingQuery({
company: company.value,
company: company.label,
location: location.value,
questionType,
});

@ -3,8 +3,8 @@ import type { ProductNavigationItems } from '~/components/global/ProductNavigati
const navigation: ProductNavigationItems = [
{ href: '/questions/browse', name: 'Browse' },
{ href: '/questions/lists', name: 'My Lists' },
{ href: '/questions/my-questions', name: 'My Questions' },
{ href: '/questions/history', name: 'History' },
// { href: '/questions/my-questions', name: 'My Questions' },
// { href: '/questions/history', name: 'History' },
];
const config = {

@ -11,6 +11,7 @@ import { Button } from '@tih/ui';
import { useQuestionVote } from '~/utils/questions/useVote';
import AddToListDropdown from '../../AddToListDropdown';
import type { CreateQuestionEncounterData } from '../../forms/CreateQuestionEncounterForm';
import CreateQuestionEncounterForm from '../../forms/CreateQuestionEncounterForm';
import QuestionAggregateBadge from '../../QuestionAggregateBadge';
@ -47,6 +48,20 @@ type AnswerStatisticsProps =
showAnswerStatistics?: false;
};
type AggregateStatisticsProps =
| {
companies: Record<string, number>;
locations: Record<string, number>;
roles: Record<string, number>;
showAggregateStatistics: true;
}
| {
companies?: never;
locations?: never;
roles?: never;
showAggregateStatistics?: false;
};
type ActionButtonProps =
| {
actionButtonLabel: string;
@ -79,19 +94,26 @@ type CreateEncounterProps =
showCreateEncounterButton?: false;
};
type AddToListProps =
| {
showAddToList: true;
}
| {
showAddToList?: false;
};
export type BaseQuestionCardProps = ActionButtonProps &
AddToListProps &
AggregateStatisticsProps &
AnswerStatisticsProps &
CreateEncounterProps &
DeleteProps &
ReceivedStatisticsProps &
UpvoteProps & {
companies: Record<string, number>;
content: string;
locations: Record<string, number>;
questionId: string;
roles: Record<string, number>;
showHover?: boolean;
timestamp: string;
timestamp: string | null;
truncateContent?: boolean;
type: QuestionsQuestionType;
};
@ -104,6 +126,7 @@ export default function BaseQuestionCard({
receivedCount,
type,
showVoteButtons,
showAggregateStatistics,
showAnswerStatistics,
showReceivedStatistics,
showCreateEncounterButton,
@ -117,6 +140,7 @@ export default function BaseQuestionCard({
showHover,
onReceivedSubmit,
showDeleteButton,
showAddToList,
onDelete,
truncateContent = true,
}: BaseQuestionCardProps) {
@ -133,20 +157,35 @@ export default function BaseQuestionCard({
onUpvote={handleUpvote}
/>
)}
<div className="flex flex-col items-start gap-2">
<div className="flex items-baseline justify-between">
<div className="flex items-baseline gap-2 text-slate-500">
<QuestionTypeBadge type={type} />
<QuestionAggregateBadge statistics={companies} variant="primary" />
<QuestionAggregateBadge statistics={locations} variant="success" />
<QuestionAggregateBadge statistics={roles} variant="danger" />
<p className="text-xs">{timestamp}</p>
<div className="flex flex-1 flex-col items-start gap-2">
<div className="flex items-baseline justify-between self-stretch">
<div className="flex items-center gap-2 text-slate-500">
{showAggregateStatistics && (
<>
<QuestionTypeBadge type={type} />
<QuestionAggregateBadge
statistics={companies}
variant="primary"
/>
<QuestionAggregateBadge
statistics={locations}
variant="success"
/>
<QuestionAggregateBadge statistics={roles} variant="danger" />
</>
)}
{timestamp !== null && <p className="text-xs">{timestamp}</p>}
{showAddToList && (
<div className="pl-4">
<AddToListDropdown questionId={questionId} />
</div>
)}
</div>
{showActionButton && (
<Button
label={actionButtonLabel}
size="sm"
variant="tertiary"
variant="secondary"
onClick={onActionButtonClick}
/>
)}

@ -4,6 +4,8 @@ import BaseQuestionCard from './BaseQuestionCard';
export type QuestionOverviewCardProps = Omit<
BaseQuestionCardProps & {
showActionButton: false;
showAddToList: true;
showAggregateStatistics: true;
showAnswerStatistics: false;
showCreateEncounterButton: true;
showDeleteButton: false;
@ -13,6 +15,8 @@ export type QuestionOverviewCardProps = Omit<
| 'actionButtonLabel'
| 'onActionButtonClick'
| 'showActionButton'
| 'showAddToList'
| 'showAggregateStatistics'
| 'showAnswerStatistics'
| 'showCreateEncounterButton'
| 'showDeleteButton'
@ -25,6 +29,8 @@ export default function FullQuestionCard(props: QuestionOverviewCardProps) {
<BaseQuestionCard
{...props}
showActionButton={false}
showAddToList={true}
showAggregateStatistics={true}
showAnswerStatistics={false}
showCreateEncounterButton={true}
showReceivedStatistics={false}

@ -6,6 +6,7 @@ import BaseQuestionCard from './BaseQuestionCard';
export type QuestionListCardProps = Omit<
BaseQuestionCardProps & {
showActionButton: false;
showAggregateStatistics: true;
showAnswerStatistics: false;
showDeleteButton: true;
showVoteButtons: false;
@ -13,6 +14,7 @@ export type QuestionListCardProps = Omit<
| 'actionButtonLabel'
| 'onActionButtonClick'
| 'showActionButton'
| 'showAggregateStatistics'
| 'showAnswerStatistics'
| 'showDeleteButton'
| 'showVoteButtons'
@ -24,6 +26,7 @@ function QuestionListCardWithoutHref(props: QuestionListCardProps) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{...(props as any)}
showActionButton={false}
showAggregateStatistics={true}
showAnswerStatistics={false}
showDeleteButton={true}
showHover={true}

@ -6,6 +6,7 @@ import BaseQuestionCard from './BaseQuestionCard';
export type QuestionOverviewCardProps = Omit<
BaseQuestionCardProps & {
showActionButton: false;
showAggregateStatistics: true;
showAnswerStatistics: true;
showCreateEncounterButton: false;
showDeleteButton: false;
@ -16,6 +17,7 @@ export type QuestionOverviewCardProps = Omit<
| 'onActionButtonClick'
| 'onDelete'
| 'showActionButton'
| 'showAggregateStatistics'
| 'showAnswerStatistics'
| 'showCreateEncounterButton'
| 'showDeleteButton'
@ -28,6 +30,7 @@ function QuestionOverviewCardWithoutHref(props: QuestionOverviewCardProps) {
<BaseQuestionCard
{...props}
showActionButton={false}
showAggregateStatistics={true}
showAnswerStatistics={true}
showCreateEncounterButton={false}
showDeleteButton={false}

@ -4,7 +4,8 @@ import BaseQuestionCard from './BaseQuestionCard';
export type SimilarQuestionCardProps = Omit<
BaseQuestionCardProps & {
showActionButton: true;
showAnswerStatistics: true;
showAggregateStatistics: false;
showAnswerStatistics: false;
showCreateEncounterButton: false;
showDeleteButton: false;
showHover: true;
@ -14,6 +15,7 @@ export type SimilarQuestionCardProps = Omit<
| 'actionButtonLabel'
| 'onActionButtonClick'
| 'showActionButton'
| 'showAggregateStatistics'
| 'showAnswerStatistics'
| 'showCreateEncounterButton'
| 'showDeleteButton'
@ -30,12 +32,13 @@ export default function SimilarQuestionCard(props: SimilarQuestionCardProps) {
<BaseQuestionCard
actionButtonLabel="Yes, this is my question"
showActionButton={true}
showAnswerStatistics={true}
showAggregateStatistics={false}
showAnswerStatistics={false}
showCreateEncounterButton={false}
showDeleteButton={false}
showHover={true}
showReceivedStatistics={true}
showVoteButtons={true}
showReceivedStatistics={false}
showVoteButtons={false}
onActionButtonClick={onSimilarQuestionClick}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{...(rest as any)}

@ -1,14 +1,7 @@
import { startOfMonth } from 'date-fns';
import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import type { QuestionsQuestionType } from '@prisma/client';
import {
Button,
CheckboxInput,
HorizontalDivider,
Select,
TextArea,
} from '@tih/ui';
import { Button, HorizontalDivider, Select, TextArea } from '@tih/ui';
import { LOCATIONS, QUESTION_TYPES, ROLES } from '~/utils/questions/constants';
import {
@ -50,155 +43,130 @@ export default function ContributeQuestionForm({
date: startOfMonth(new Date()),
},
});
const register = useFormRegister(formRegister);
const selectRegister = useSelectRegister(formRegister);
const [canSubmit, setCanSubmit] = useState<boolean>(false);
const handleCheckSimilarQuestions = (checked: boolean) => {
setCanSubmit(checked);
};
return (
<form
className="flex flex-1 flex-col items-stretch justify-center gap-y-4"
onSubmit={handleSubmit(onSubmit)}>
<div className="min-w-[113px] max-w-[113px] flex-1">
<Select
defaultValue="coding"
label="Type"
options={QUESTION_TYPES}
required={true}
{...selectRegister('questionType')}
/>
</div>
<TextArea
label="Question Prompt"
placeholder="Contribute a question"
required={true}
rows={5}
{...register('questionContent')}
/>
<HorizontalDivider />
<h2 className="text-md text-primary-800 font-semibold">
Additional information
</h2>
<div className="flex flex-col flex-wrap items-stretch gap-2 sm:flex-row sm:items-end">
<div className="flex-1 sm:min-w-[150px] sm:max-w-[300px]">
<Controller
control={control}
name="location"
render={({ field }) => (
<LocationTypeahead
required={true}
onSelect={(option) => {
field.onChange(option.value);
}}
{...field}
value={LOCATIONS.find(
(location) => location.value === field.value,
)}
/>
)}
<div className="flex flex-col justify-between gap-4">
<form
className="flex flex-1 flex-col items-stretch justify-center gap-y-4"
onSubmit={handleSubmit(onSubmit)}>
<div className="min-w-[113px] max-w-[113px] flex-1">
<Select
defaultValue="coding"
label="Type"
options={QUESTION_TYPES}
required={true}
{...selectRegister('questionType')}
/>
</div>
<div className="flex-1 sm:min-w-[150px] sm:max-w-[300px]">
<Controller
control={control}
name="date"
render={({ field }) => (
<MonthYearPicker
monthRequired={true}
value={{
month: ((field.value.getMonth() as number) + 1) as Month,
year: field.value.getFullYear(),
}}
yearRequired={true}
onChange={({ month, year }) =>
field.onChange(startOfMonth(new Date(year, month - 1)))
}
/>
)}
/>
</div>
</div>
<div className="flex flex-col flex-wrap items-stretch gap-2 sm:flex-row sm:items-end">
<div className="flex-1 sm:min-w-[150px] sm:max-w-[300px]">
<Controller
control={control}
name="company"
render={({ field }) => (
<CompanyTypeahead
required={true}
onSelect={({ id }) => {
field.onChange(id);
}}
/>
)}
/>
<TextArea
label="Question Prompt"
placeholder="Contribute a question"
required={true}
rows={5}
{...register('questionContent')}
/>
<HorizontalDivider />
<h2 className="text-md text-primary-800 font-semibold">
Additional information
</h2>
<div className="flex flex-col flex-wrap items-stretch gap-2 sm:flex-row sm:items-end">
<div className="flex-1 sm:min-w-[150px] sm:max-w-[300px]">
<Controller
control={control}
name="location"
render={({ field }) => (
<LocationTypeahead
required={true}
onSelect={(option) => {
field.onChange(option.value);
}}
{...field}
value={LOCATIONS.find(
(location) => location.value === field.value,
)}
/>
)}
/>
</div>
<div className="flex-1 sm:min-w-[150px] sm:max-w-[300px]">
<Controller
control={control}
name="date"
render={({ field }) => (
<MonthYearPicker
monthRequired={true}
value={{
month: ((field.value.getMonth() as number) + 1) as Month,
year: field.value.getFullYear(),
}}
yearRequired={true}
onChange={({ month, year }) =>
field.onChange(startOfMonth(new Date(year, month - 1)))
}
/>
)}
/>
</div>
</div>
<div className="flex-1 sm:min-w-[150px] sm:max-w-[200px]">
<Controller
control={control}
name="role"
render={({ field }) => (
<RoleTypeahead
required={true}
onSelect={(option) => {
field.onChange(option.value);
}}
{...field}
value={ROLES.find((role) => role.value === field.value)}
/>
)}
/>
<div className="flex flex-col flex-wrap items-stretch gap-2 sm:flex-row sm:items-end">
<div className="flex-1 sm:min-w-[150px] sm:max-w-[300px]">
<Controller
control={control}
name="company"
render={({ field }) => (
<CompanyTypeahead
required={true}
onSelect={({ id }) => {
field.onChange(id);
}}
/>
)}
/>
</div>
<div className="flex-1 sm:min-w-[150px] sm:max-w-[200px]">
<Controller
control={control}
name="role"
render={({ field }) => (
<RoleTypeahead
required={true}
onSelect={(option) => {
field.onChange(option.value);
}}
{...field}
value={ROLES.find((role) => role.value === field.value)}
/>
)}
/>
</div>
</div>
</div>
{/* <div className="w-full">
<HorizontalDivider />
</div>
<h1 className="mb-3">
Are these questions the same as yours? TODO:Change to list
</h1>
<div>
<SimilarQuestionCard
content="Given an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums and an integer target, return indices"
location="Menlo Park, CA"
receivedCount={0}
role="Senior Engineering Manager"
timestamp="Today"
onSimilarQuestionClick={() => {
// eslint-disable-next-line no-console
console.log('hi!');
}}
/>
</div> */}
<div
className="bg-primary-50 flex w-full flex-col gap-y-2 py-3 shadow-[0_0_0_100vmax_theme(colors.primary.50)] sm:flex-row sm:justify-between"
style={{
// Hack to make the background bleed outside the container
clipPath: 'inset(0 -100vmax)',
}}>
<div className="my-2 flex sm:my-0">
<CheckboxInput
label="I have checked that my question is new"
value={canSubmit}
onChange={handleCheckSimilarQuestions}
/>
<div className="w-full">
<HorizontalDivider />
</div>
<div className="flex gap-x-2">
<button
className="focus:ring-primary-500 inline-flex w-full justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-base font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
type="button"
onClick={onDiscard}>
Discard
</button>
<Button
className="bg-primary-600 hover:bg-primary-700 focus:ring-primary-500 inline-flex w-full justify-center rounded-md border border-transparent px-4 py-2 text-base font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:bg-slate-400 sm:ml-3 sm:w-auto sm:text-sm"
disabled={!canSubmit}
label="Contribute"
type="submit"
variant="primary"></Button>
<div
className="bg-primary-50 flex w-full justify-end gap-y-2 py-3 shadow-[0_0_0_100vmax_theme(colors.primary.50)]"
style={{
// Hack to make the background bleed outside the container
clipPath: 'inset(0 -100vmax)',
}}>
<div className="flex gap-x-2">
<button
className="focus:ring-primary-500 inline-flex w-full justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-base font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
type="button"
onClick={onDiscard}>
Discard
</button>
<Button
className="bg-primary-600 hover:bg-primary-700 focus:ring-primary-500 inline-flex w-full justify-center rounded-md border border-transparent px-4 py-2 text-base font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:bg-slate-400 sm:ml-3 sm:w-auto sm:text-sm"
label="Contribute"
type="submit"
variant="primary"></Button>
</div>
</div>
</div>
</form>
</form>
</div>
);
}

@ -9,7 +9,6 @@ import { Button, SlideOut } from '@tih/ui';
import QuestionOverviewCard from '~/components/questions/card/question/QuestionOverviewCard';
import ContributeQuestionCard from '~/components/questions/ContributeQuestionCard';
import type { FilterOption } from '~/components/questions/filter/FilterSection';
import FilterSection from '~/components/questions/filter/FilterSection';
import QuestionSearchBar from '~/components/questions/QuestionSearchBar';
import CompanyTypeahead from '~/components/questions/typeahead/CompanyTypeahead';
@ -195,18 +194,6 @@ export default function QuestionsBrowsePage() {
const [loaded, setLoaded] = useState(false);
const [filterDrawerOpen, setFilterDrawerOpen] = useState(false);
const [selectedCompanyOptions, setSelectedCompanyOptions] = useState<
Array<FilterOption>
>([]);
const [selectedRoleOptions, setSelectedRoleOptions] = useState<
Array<FilterOption>
>([]);
const [selectedLocationOptions, setSelectedLocationOptions] = useState<
Array<FilterOption>
>([]);
const questionTypeFilterOptions = useMemo(() => {
return QUESTION_TYPES.map((questionType) => ({
...questionType,
@ -275,9 +262,37 @@ export default function QuestionsBrowsePage() {
sortType,
]);
const selectedCompanyOptions = useMemo(() => {
return selectedCompanies.map((company) => ({
checked: true,
id: company,
label: company,
value: company,
}));
}, [selectedCompanies]);
const selectedRoleOptions = useMemo(() => {
return selectedRoles.map((role) => ({
checked: true,
id: role,
label: role,
value: role,
}));
}, [selectedRoles]);
const selectedLocationOptions = useMemo(() => {
return selectedLocations.map((location) => ({
checked: true,
id: location,
label: location,
value: location,
}));
}, [selectedLocations]);
if (!loaded) {
return null;
}
const filterSidebar = (
<div className="divide-y divide-slate-200 px-4">
<Button
@ -293,9 +308,6 @@ export default function QuestionsBrowsePage() {
setSelectedQuestionAge('all');
setSelectedRoles([]);
setSelectedLocations([]);
setSelectedCompanyOptions([]);
setSelectedRoleOptions([]);
setSelectedLocationOptions([]);
}}
/>
<FilterSection
@ -306,8 +318,8 @@ export default function QuestionsBrowsePage() {
{...field}
clearOnSelect={true}
filterOption={(option) => {
return !selectedCompanyOptions.some((selectedOption) => {
return selectedOption.value === option.value;
return !selectedCompanies.some((company) => {
return company === option.value;
});
}}
isLabelHidden={true}
@ -323,19 +335,10 @@ export default function QuestionsBrowsePage() {
onOptionChange={(option) => {
if (option.checked) {
setSelectedCompanies([...selectedCompanies, option.label]);
setSelectedCompanyOptions((prevOptions) => [
...prevOptions,
{ ...option, checked: true },
]);
} else {
setSelectedCompanies(
selectedCompanies.filter((company) => company !== option.label),
);
setSelectedCompanyOptions((prevOptions) =>
prevOptions.filter(
(prevOption) => prevOption.label !== option.label,
),
);
}
}}
/>
@ -347,8 +350,8 @@ export default function QuestionsBrowsePage() {
{...field}
clearOnSelect={true}
filterOption={(option) => {
return !selectedRoleOptions.some((selectedOption) => {
return selectedOption.value === option.value;
return !selectedRoles.some((role) => {
return role === option.value;
});
}}
isLabelHidden={true}
@ -364,19 +367,10 @@ export default function QuestionsBrowsePage() {
onOptionChange={(option) => {
if (option.checked) {
setSelectedRoles([...selectedRoles, option.value]);
setSelectedRoleOptions((prevOptions) => [
...prevOptions,
{ ...option, checked: true },
]);
} else {
setSelectedRoles(
selectedCompanies.filter((role) => role !== option.value),
);
setSelectedRoleOptions((prevOptions) =>
prevOptions.filter(
(prevOption) => prevOption.value !== option.value,
),
);
}
}}
/>
@ -413,8 +407,8 @@ export default function QuestionsBrowsePage() {
{...field}
clearOnSelect={true}
filterOption={(option) => {
return !selectedLocationOptions.some((selectedOption) => {
return selectedOption.value === option.value;
return !selectedLocations.some((location) => {
return location === option.value;
});
}}
isLabelHidden={true}
@ -430,19 +424,10 @@ export default function QuestionsBrowsePage() {
onOptionChange={(option) => {
if (option.checked) {
setSelectedLocations([...selectedLocations, option.value]);
setSelectedLocationOptions((prevOptions) => [
...prevOptions,
{ ...option, checked: true },
]);
} else {
setSelectedLocations(
selectedLocations.filter((role) => role !== option.value),
);
setSelectedLocationOptions((prevOptions) =>
prevOptions.filter(
(prevOption) => prevOption.value !== option.value,
),
);
}
}}
/>

@ -8,60 +8,90 @@ import {
} from '@heroicons/react/24/outline';
import QuestionListCard from '~/components/questions/card/question/QuestionListCard';
import type { CreateListFormData } from '~/components/questions/CreateListDialog';
import CreateListDialog from '~/components/questions/CreateListDialog';
import DeleteListDialog from '~/components/questions/DeleteListDialog';
import { Button } from '~/../../../packages/ui/dist';
import { APP_TITLE } from '~/utils/questions/constants';
import { SAMPLE_QUESTION } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug';
import { trpc } from '~/utils/trpc';
export default function ListPage() {
const questions = [
SAMPLE_QUESTION,
SAMPLE_QUESTION,
SAMPLE_QUESTION,
SAMPLE_QUESTION,
SAMPLE_QUESTION,
SAMPLE_QUESTION,
SAMPLE_QUESTION,
SAMPLE_QUESTION,
SAMPLE_QUESTION,
SAMPLE_QUESTION,
SAMPLE_QUESTION,
SAMPLE_QUESTION,
SAMPLE_QUESTION,
SAMPLE_QUESTION,
SAMPLE_QUESTION,
];
const utils = trpc.useContext();
const { data: lists } = trpc.useQuery(['questions.lists.getListsByUser']);
const { mutateAsync: createList } = trpc.useMutation(
'questions.lists.create',
{
onSuccess: () => {
// TODO: Add optimistic update
utils.invalidateQueries(['questions.lists.getListsByUser']);
},
},
);
const { mutateAsync: deleteList } = trpc.useMutation(
'questions.lists.delete',
{
onSuccess: () => {
// TODO: Add optimistic update
utils.invalidateQueries(['questions.lists.getListsByUser']);
},
},
);
const { mutateAsync: deleteQuestionEntry } = trpc.useMutation(
'questions.lists.deleteQuestionEntry',
{
onSuccess: () => {
// TODO: Add optimistic update
utils.invalidateQueries(['questions.lists.getListsByUser']);
},
},
);
const lists = [
{ id: 1, name: 'list 1', questions },
{ id: 2, name: 'list 2', questions },
{ id: 3, name: 'list 3', questions },
{ id: 4, name: 'list 4', questions },
{ id: 5, name: 'list 5', questions },
];
const [selectedListIndex, setSelectedListIndex] = useState(0);
const [showDeleteListDialog, setShowDeleteListDialog] = useState(false);
const [showCreateListDialog, setShowCreateListDialog] = useState(false);
const [listIdToDelete, setListIdToDelete] = useState('');
const handleDeleteList = async (listId: string) => {
await deleteList({
id: listId,
});
setShowDeleteListDialog(false);
};
const handleDeleteListCancel = () => {
setShowDeleteListDialog(false);
};
const handleCreateList = async (data: CreateListFormData) => {
await createList({
name: data.name,
});
setShowCreateListDialog(false);
};
const handleCreateListCancel = () => {
setShowCreateListDialog(false);
};
const [selectedList, setSelectedList] = useState(
(lists ?? []).length > 0 ? lists[0].id : '',
);
const listOptions = (
<>
<ul className="flex flex-1 flex-col divide-y divide-solid divide-slate-200">
{lists.map((list) => (
{(lists ?? []).map((list, index) => (
<li
key={list.id}
className={`flex items-center hover:bg-slate-50 ${
selectedList === list.id ? 'bg-primary-100' : ''
selectedListIndex === index ? 'bg-primary-100' : ''
}`}>
<button
className="flex w-full flex-1 justify-between "
type="button"
onClick={() => {
setSelectedList(list.id);
// eslint-disable-next-line no-console
console.log(selectedList);
setSelectedListIndex(index);
}}>
<p className="text-primary-700 text-md p-3 font-medium">
<p className="text-primary-700 text-md p-3 pl-6 font-medium">
{list.name}
</p>
</button>
@ -85,7 +115,11 @@ export default function ListPage() {
? 'bg-violet-500 text-white'
: 'text-slate-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
type="button">
type="button"
onClick={() => {
setShowDeleteListDialog(true);
setListIdToDelete(list.id);
}}>
Delete
</button>
)}
@ -104,6 +138,7 @@ export default function ListPage() {
)}
</>
);
return (
<>
<Head>
@ -111,7 +146,7 @@ export default function ListPage() {
</Head>
<main className="flex flex-1 flex-col items-stretch">
<div className="flex h-full flex-1">
<aside className="w-[300px] overflow-y-auto border-l bg-white py-4 lg:block">
<aside className="w-[300px] overflow-y-auto border-r bg-white py-4 lg:block">
<div className="mb-2 flex items-center justify-between">
<h2 className="px-4 text-xl font-semibold">My Lists</h2>
<div className="px-4">
@ -124,6 +159,7 @@ export default function ListPage() {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setShowCreateListDialog(true);
}}
/>
</div>
@ -133,44 +169,63 @@ export default function ListPage() {
<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">
{selectedList && (
{lists?.[selectedListIndex] && (
<div className="flex flex-col gap-4 pb-4">
{(questions ?? []).map((question) => (
<QuestionListCard
key={question.id}
companies={question.companies}
content={question.content}
href={`/questions/${question.id}/${createSlug(
question.content,
)}`}
locations={question.locations}
questionId={question.id}
receivedCount={0}
roles={question.roles}
timestamp={question.seenAt.toLocaleDateString(
undefined,
{
month: 'short',
year: 'numeric',
},
)}
type={question.type}
onDelete={() => {
// eslint-disable-next-line no-console
console.log('delete');
}}
/>
))}
{questions?.length === 0 && (
{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 });
}}
/>
),
)}
{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">
<NoSymbolIcon className="h-6 w-6" />
<p>You have no added any questions to your list yet.</p>
<p>
You have not added any questions to your list yet.
</p>
</div>
)}
</div>
)}
</div>
</div>
<DeleteListDialog
show={showDeleteListDialog}
onCancel={handleDeleteListCancel}
onDelete={() => {
handleDeleteList(listIdToDelete);
}}></DeleteListDialog>
<CreateListDialog
show={showCreateListDialog}
onCancel={handleCreateListCancel}
onSubmit={handleCreateList}></CreateListDialog>
</section>
</div>
</main>

@ -9,6 +9,7 @@ import { offersProfileRouter } from './offers/offers-profile-router';
import { protectedExampleRouter } from './protected-example-router';
import { questionsAnswerCommentRouter } from './questions-answer-comment-router';
import { questionsAnswerRouter } from './questions-answer-router';
import { questionListRouter } from './questions-list-router';
import { questionsQuestionCommentRouter } from './questions-question-comment-router';
import { questionsQuestionEncounterRouter } from './questions-question-encounter-router';
import { questionsQuestionRouter } from './questions-question-router';
@ -40,6 +41,7 @@ export const appRouter = createRouter()
.merge('resumes.comments.votes.user.', resumesCommentsVotesUserRouter)
.merge('questions.answers.comments.', questionsAnswerCommentRouter)
.merge('questions.answers.', questionsAnswerRouter)
.merge('questions.lists.', questionListRouter)
.merge('questions.questions.comments.', questionsQuestionCommentRouter)
.merge('questions.questions.encounters.', questionsQuestionEncounterRouter)
.merge('questions.questions.', questionsQuestionRouter)

@ -1,6 +1,8 @@
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import createQuestionWithAggregateData from '~/utils/questions/server/createQuestionWithAggregateData';
import { createProtectedRouter } from './context';
export const questionListRouter = createProtectedRouter()
@ -8,11 +10,35 @@ export const questionListRouter = createProtectedRouter()
async resolve({ ctx }) {
const userId = ctx.session?.user?.id;
return await ctx.prisma.questionsList.findMany({
// TODO: Optimize by not returning question entries
const questionsLists = await ctx.prisma.questionsList.findMany({
include: {
questionEntries: {
include: {
question: true,
question: {
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
company: true,
location: true,
role: true,
seenAt: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
},
},
},
},
@ -20,23 +46,57 @@ export const questionListRouter = createProtectedRouter()
createdAt: 'asc',
},
where: {
id: userId,
userId,
},
});
const lists = questionsLists.map((list) => ({
...list,
questionEntries: list.questionEntries.map((entry) => ({
...entry,
question: createQuestionWithAggregateData(entry.question),
})),
}));
return lists;
},
})
.query('getListById', {
input: z.object({
listId: z.string(),
}),
async resolve({ ctx }) {
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { listId } = input;
return await ctx.prisma.questionsList.findMany({
const questionList = await ctx.prisma.questionsList.findFirst({
include: {
questionEntries: {
include: {
question: true,
question: {
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
company: true,
location: true,
role: true,
seenAt: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
},
},
},
},
@ -44,9 +104,25 @@ export const questionListRouter = createProtectedRouter()
createdAt: 'asc',
},
where: {
id: userId,
id: listId,
userId,
},
});
if (!questionList) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Question list not found',
});
}
return {
...questionList,
questionEntries: questionList.questionEntries.map((questionEntry) => ({
...questionEntry,
question: createQuestionWithAggregateData(questionEntry.question),
})),
};
},
})
.mutation('create', {
@ -111,7 +187,7 @@ export const questionListRouter = createProtectedRouter()
},
});
if (listToDelete?.id !== userId) {
if (listToDelete?.userId !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
@ -139,7 +215,7 @@ export const questionListRouter = createProtectedRouter()
},
});
if (listToAugment?.id !== userId) {
if (listToAugment?.userId !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
@ -170,10 +246,10 @@ export const questionListRouter = createProtectedRouter()
},
});
if (entryToDelete?.id !== userId) {
if (entryToDelete === null) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
code: 'NOT_FOUND',
message: 'Entry not found.',
});
}
@ -183,7 +259,7 @@ export const questionListRouter = createProtectedRouter()
},
});
if (listToAugment?.id !== userId) {
if (listToAugment?.userId !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',

@ -35,17 +35,17 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
latestSeenAt < encounter.seenAt ? encounter.seenAt : latestSeenAt;
if (!(encounter.company!.name in companyCounts)) {
companyCounts[encounter.company!.name] = 1;
companyCounts[encounter.company!.name] = 0;
}
companyCounts[encounter.company!.name] += 1;
if (!(encounter.location in locationCounts)) {
locationCounts[encounter.location] = 1;
locationCounts[encounter.location] = 0;
}
locationCounts[encounter.location] += 1;
if (!(encounter.role in roleCounts)) {
roleCounts[encounter.role] = 1;
roleCounts[encounter.role] = 0;
}
roleCounts[encounter.role] += 1;
}
@ -93,7 +93,7 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
}
if (
!questionToUpdate.lastSeenAt ||
questionToUpdate.lastSeenAt === null ||
questionToUpdate.lastSeenAt < input.seenAt
) {
await tx.questionsQuestion.update({

@ -2,9 +2,10 @@ import { z } from 'zod';
import { QuestionsQuestionType, Vote } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import createQuestionWithAggregateData from '~/utils/questions/server/createQuestionWithAggregateData';
import { createProtectedRouter } from './context';
import type { Question } from '~/types/questions';
import { SortOrder, SortType } from '~/types/questions.d';
export const questionsQuestionRouter = createProtectedRouter()
@ -122,72 +123,9 @@ export const questionsQuestionRouter = createProtectedRouter()
},
});
const processedQuestionsData = questionsData.map((data) => {
const votes: number = data.votes.reduce(
(previousValue: number, currentValue) => {
let result: number = previousValue;
switch (currentValue.vote) {
case Vote.UPVOTE:
result += 1;
break;
case Vote.DOWNVOTE:
result -= 1;
break;
}
return result;
},
0,
);
const companyCounts: Record<string, number> = {};
const locationCounts: Record<string, number> = {};
const roleCounts: Record<string, number> = {};
let latestSeenAt = data.encounters[0].seenAt;
for (let i = 0; i < data.encounters.length; i++) {
const encounter = data.encounters[i];
latestSeenAt =
latestSeenAt < encounter.seenAt ? encounter.seenAt : latestSeenAt;
if (!(encounter.company!.name in companyCounts)) {
companyCounts[encounter.company!.name] = 1;
}
companyCounts[encounter.company!.name] += 1;
if (!(encounter.location in locationCounts)) {
locationCounts[encounter.location] = 1;
}
locationCounts[encounter.location] += 1;
if (!(encounter.role in roleCounts)) {
roleCounts[encounter.role] = 1;
}
roleCounts[encounter.role] += 1;
}
const question: Question = {
aggregatedQuestionEncounters: {
companyCounts,
latestSeenAt,
locationCounts,
roleCounts,
},
content: data.content,
id: data.id,
numAnswers: data._count.answers,
numComments: data._count.comments,
numVotes: votes,
receivedCount: data.encounters.length,
seenAt: latestSeenAt,
type: data.questionType,
updatedAt: data.updatedAt,
user: data.user?.name ?? '',
};
return question;
});
const processedQuestionsData = questionsData.map(
createQuestionWithAggregateData,
);
let nextCursor: typeof cursor | undefined = undefined;
@ -252,68 +190,8 @@ export const questionsQuestionRouter = createProtectedRouter()
message: 'Question not found',
});
}
const votes: number = questionData.votes.reduce(
(previousValue: number, currentValue) => {
let result: number = previousValue;
switch (currentValue.vote) {
case Vote.UPVOTE:
result += 1;
break;
case Vote.DOWNVOTE:
result -= 1;
break;
}
return result;
},
0,
);
const companyCounts: Record<string, number> = {};
const locationCounts: Record<string, number> = {};
const roleCounts: Record<string, number> = {};
let latestSeenAt = questionData.encounters[0].seenAt;
for (const encounter of questionData.encounters) {
latestSeenAt =
latestSeenAt < encounter.seenAt ? encounter.seenAt : latestSeenAt;
if (!(encounter.company!.name in companyCounts)) {
companyCounts[encounter.company!.name] = 1;
}
companyCounts[encounter.company!.name] += 1;
if (!(encounter.location in locationCounts)) {
locationCounts[encounter.location] = 1;
}
locationCounts[encounter.location] += 1;
if (!(encounter.role in roleCounts)) {
roleCounts[encounter.role] = 1;
}
roleCounts[encounter.role] += 1;
}
const question: Question = {
aggregatedQuestionEncounters: {
companyCounts,
latestSeenAt,
locationCounts,
roleCounts,
},
content: questionData.content,
id: questionData.id,
numAnswers: questionData._count.answers,
numComments: questionData._count.comments,
numVotes: votes,
receivedCount: questionData.encounters.length,
seenAt: questionData.encounters[0].seenAt,
type: questionData.questionType,
updatedAt: questionData.updatedAt,
user: questionData.user?.name ?? '',
};
return question;
return createQuestionWithAggregateData(questionData);
},
})
.mutation('create', {

@ -0,0 +1,92 @@
import type {
Company,
QuestionsQuestion,
QuestionsQuestionVote,
} from '@prisma/client';
import { Vote } from '@prisma/client';
import type { Question } from '~/types/questions';
type QuestionWithAggregatableData = QuestionsQuestion & {
_count: {
answers: number;
comments: number;
};
encounters: Array<{
company: Company | null;
location: string;
role: string;
seenAt: Date;
}>;
user: {
name: string | null;
} | null;
votes: Array<QuestionsQuestionVote>;
};
export default function createQuestionWithAggregateData(
data: QuestionWithAggregatableData,
): Question {
const votes: number = data.votes.reduce(
(previousValue: number, currentValue) => {
let result: number = previousValue;
switch (currentValue.vote) {
case Vote.UPVOTE:
result += 1;
break;
case Vote.DOWNVOTE:
result -= 1;
break;
}
return result;
},
0,
);
const companyCounts: Record<string, number> = {};
const locationCounts: Record<string, number> = {};
const roleCounts: Record<string, number> = {};
let latestSeenAt = data.encounters[0].seenAt;
for (const encounter of data.encounters) {
latestSeenAt =
latestSeenAt < encounter.seenAt ? encounter.seenAt : latestSeenAt;
if (!(encounter.company!.name in companyCounts)) {
companyCounts[encounter.company!.name] = 0;
}
companyCounts[encounter.company!.name] += 1;
if (!(encounter.location in locationCounts)) {
locationCounts[encounter.location] = 0;
}
locationCounts[encounter.location] += 1;
if (!(encounter.role in roleCounts)) {
roleCounts[encounter.role] = 0;
}
roleCounts[encounter.role] += 1;
}
const question: Question = {
aggregatedQuestionEncounters: {
companyCounts,
latestSeenAt,
locationCounts,
roleCounts,
},
content: data.content,
id: data.id,
numAnswers: data._count.answers,
numComments: data._count.comments,
numVotes: votes,
receivedCount: data.encounters.length,
seenAt: data.encounters[0].seenAt,
type: data.questionType,
updatedAt: data.updatedAt,
user: data.user?.name ?? '',
};
return question;
}

@ -25,7 +25,7 @@ export const useSearchParam = <Value = string>(
const [isInitialized, setIsInitialized] = useState(false);
const router = useRouter();
const [filters, setFilters] = useState<Array<Value>>(defaultValues || []);
const [params, setParams] = useState<Array<Value>>(defaultValues || []);
useEffect(() => {
if (router.isReady && !isInitialized) {
@ -33,7 +33,7 @@ export const useSearchParam = <Value = string>(
const query = router.query[name];
if (query) {
const queryValues = Array.isArray(query) ? query : [query];
setFilters(
setParams(
queryValues
.map(stringToParam)
.filter((value) => value !== null) as Array<Value>,
@ -43,27 +43,27 @@ export const useSearchParam = <Value = string>(
const localStorageValue = localStorage.getItem(name);
if (localStorageValue !== null) {
const loadedFilters = JSON.parse(localStorageValue);
setFilters(loadedFilters);
setParams(loadedFilters);
}
}
setIsInitialized(true);
}
}, [isInitialized, name, stringToParam, router]);
const setFiltersCallback = useCallback(
(newFilters: Array<Value>) => {
setFilters(newFilters);
const setParamsCallback = useCallback(
(newParams: Array<Value>) => {
setParams(newParams);
localStorage.setItem(
name,
JSON.stringify(
newFilters.map(valueToQueryParam).filter((param) => param !== null),
newParams.map(valueToQueryParam).filter((param) => param !== null),
),
);
},
[name, valueToQueryParam],
);
return [filters, setFiltersCallback, isInitialized] as const;
return [params, setParamsCallback, isInitialized] as const;
};
export const useSearchParamSingle = <Value = string>(
@ -73,14 +73,14 @@ export const useSearchParamSingle = <Value = string>(
},
) => {
const { defaultValue, ...restOpts } = opts ?? {};
const [filters, setFilters, isInitialized] = useSearchParam<Value>(name, {
const [params, setParams, isInitialized] = useSearchParam<Value>(name, {
defaultValues: defaultValue !== undefined ? [defaultValue] : undefined,
...restOpts,
} as SearchParamOptions<Value>);
return [
filters[0],
(value: Value) => setFilters([value]),
params[0],
(value: Value) => setParams([value]),
isInitialized,
] as const;
};

Loading…
Cancel
Save