[questions][fix] ui fixes and enhancements (#514)

Co-authored-by: wlren <weilinwork99@gmail.com>
This commit is contained in:
Jeff Sieu
2022-11-06 02:14:58 +08:00
committed by GitHub
parent 8f4246da6d
commit 075f7bfba8
12 changed files with 476 additions and 230 deletions

View File

@@ -3,22 +3,52 @@ 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 { CheckIcon, HeartIcon, PlusIcon } from '@heroicons/react/20/solid';
import {
useAddQuestionToListAsync,
useCreateListAsync,
useRemoveQuestionFromListAsync,
} from '~/utils/questions/mutations';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import { trpc } from '~/utils/trpc';
import CreateListDialog from './CreateListDialog';
export type AddToListDropdownProps = {
questionId: string;
};
export type DropdownButtonProps = PropsWithChildren<{
onClick: () => void;
}>;
function DropdownButton({ onClick, children }: DropdownButtonProps) {
return (
<Menu.Item>
{({ active }) => (
<button
className={clsx(
active ? 'bg-slate-100 text-slate-900' : 'text-slate-700',
'flex w-full items-center px-4 py-2 text-sm',
)}
type="button"
onClick={onClick}>
{children}
</button>
)}
</Menu.Item>
);
}
export default function AddToListDropdown({
questionId,
}: AddToListDropdownProps) {
const [menuOpened, setMenuOpened] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const [show, setShow] = useState(false);
const utils = trpc.useContext();
const createListAsync = useCreateListAsync();
const { data: lists } = trpc.useQuery(['questions.lists.getListsByUser']);
const listsWithQuestionData = useMemo(() => {
@@ -30,25 +60,8 @@ export default function AddToListDropdown({
}));
}, [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 addQuestionToList = useAddQuestionToListAsync();
const removeQuestionFromList = useRemoveQuestionFromListAsync();
const addClickOutsideListener = () => {
document.addEventListener('click', handleClickOutside, true);
@@ -101,63 +114,79 @@ export default function AddToListDropdown({
);
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);
}
}}>
<div>
<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">
<DropdownButton
onClick={() => {
if (list.hasQuestion) {
handleDeleteFromList(list.id);
} else {
handleAddToList(list.id);
}
}}>
<div className="flex w-full flex-1 justify-between">
<span className="flex-1 overflow-hidden text-ellipsis text-start">
{list.name}
</span>
{list.hasQuestion && (
<CheckIcon
aria-hidden="true"
className="mr-3 h-5 w-5 text-slate-400 group-hover:text-slate-500"
className="h-5 w-5 text-slate-400"
/>
)}
{list.name}
</button>
)}
</Menu.Item>
</div>
))}
</>
)}
</Menu.Items>
</Transition>
</Menu>
</div>
</DropdownButton>
</div>
))}
<DropdownButton
onClick={() => {
setShow(true);
}}>
<PlusIcon
aria-hidden="true"
className="mr-3 h-5 w-5 text-slate-500"
/>
<span className="font-semibold text-slate-500">
Create new list
</span>
</DropdownButton>
</>
)}
</Menu.Items>
</Transition>
</Menu>
<CreateListDialog
show={show}
onCancel={() => {
setShow(false);
}}
onSubmit={async (data) => {
await createListAsync(data);
setShow(false);
}}
/>
</div>
);
}

View File

@@ -1,9 +1,4 @@
import { useState } from 'react';
import {
BuildingOffice2Icon,
CalendarDaysIcon,
QuestionMarkCircleIcon,
} from '@heroicons/react/24/outline';
import { TextInput } from '@tih/ui';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
@@ -32,44 +27,19 @@ export default function ContributeQuestionCard({
return (
<div className="w-full">
<button
className="flex w-full 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 w-full flex-1 justify-between 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="w-full">
<TextInput
disabled={true}
isLabelHidden={true}
label="Question"
placeholder="Contribute a question"
onChange={handleOpenContribute}
/>
</div>
<div className="flex flex-wrap items-end justify-start gap-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>

View File

@@ -8,16 +8,15 @@ export type SortOption<Value> = {
value: Value;
};
const sortTypeOptions = SORT_TYPES;
const sortOrderOptions = SORT_ORDERS;
type SortOrderProps<Order> = {
onSortOrderChange?: (sortValue: Order) => void;
sortOrderOptions?: Array<SortOption<Order>>;
sortOrderValue: Order;
};
type SortTypeProps<Type> = {
onSortTypeChange?: (sortType: Type) => void;
sortTypeOptions?: Array<SortOption<Type>>;
sortTypeValue: Type;
};
@@ -29,17 +28,22 @@ export default function SortOptionsSelect({
sortOrderValue,
onSortTypeChange,
sortTypeValue,
sortOrderOptions,
sortTypeOptions,
}: SortOptionsSelectProps) {
const sortTypes = sortTypeOptions ?? SORT_TYPES;
const sortOrders = sortOrderOptions ?? SORT_ORDERS;
return (
<div className="flex items-end justify-end gap-4">
<div className="flex items-center gap-2">
<Select
display="inline"
label="Sort by"
options={sortTypeOptions}
options={sortTypes}
value={sortTypeValue}
onChange={(value) => {
const chosenOption = sortTypeOptions.find(
const chosenOption = sortTypes.find(
(option) => String(option.value) === value,
);
if (chosenOption) {
@@ -52,10 +56,10 @@ export default function SortOptionsSelect({
<Select
display="inline"
label="Order by"
options={sortOrderOptions}
options={sortOrders}
value={sortOrderValue}
onChange={(value) => {
const chosenOption = sortOrderOptions.find(
const chosenOption = sortOrders.find(
(option) => String(option.value) === value,
);
if (chosenOption) {

View File

@@ -90,7 +90,7 @@ type ReceivedStatisticsProps =
type CreateEncounterProps =
| {
createEncounterButtonText: string;
onReceivedSubmit: (data: CreateQuestionEncounterData) => void;
onReceivedSubmit: (data: CreateQuestionEncounterData) => Promise<void>;
showCreateEncounterButton: true;
}
| {
@@ -185,7 +185,7 @@ export default function BaseQuestionCard({
)}
<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">
<div className="z-10 flex items-center gap-2 text-slate-500">
{showAggregateStatistics && (
<>
<QuestionTypeBadge type={type} />
@@ -263,9 +263,8 @@ export default function BaseQuestionCard({
onCancel={() => {
setShowReceivedForm(false);
}}
onSubmit={(data) => {
onReceivedSubmit?.(data);
setShowReceivedForm(false);
onSubmit={async (data) => {
await onReceivedSubmit?.(data);
}}
/>
)}

View File

@@ -5,7 +5,7 @@ import { ArrowPathIcon } from '@heroicons/react/20/solid';
import type { QuestionsQuestionType } from '@prisma/client';
import type { TypeaheadOption } from '@tih/ui';
import { CheckboxInput } from '@tih/ui';
import { Button, HorizontalDivider, Select, TextArea } from '@tih/ui';
import { Button, Select, TextArea } from '@tih/ui';
import { QUESTION_TYPES } from '~/utils/questions/constants';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
@@ -187,11 +187,9 @@ export default function ContributeQuestionForm({
/>
</div>
</div>
<div className="w-full">
<HorizontalDivider />
</div>
<h2
className="text-primary-900 mb-3
className="text-primary-900
text-lg font-semibold
">
Are these questions the same as yours?
@@ -243,11 +241,13 @@ export default function ContributeQuestionForm({
/>
);
})}
{similarQuestions?.length === 0 && (
<p className="font-semibold text-slate-900">
No similar questions found.
</p>
)}
{similarQuestions?.length === 0 &&
contentToCheck?.length !== 0 &&
questionContent === contentToCheck && (
<p className="font-semibold text-slate-900">
No similar questions found.
</p>
)}
</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"

View File

@@ -1,5 +1,6 @@
import { startOfMonth } from 'date-fns';
import { useState } from 'react';
import { CheckIcon } from '@heroicons/react/20/solid';
import { Button } from '@tih/ui';
import type { Month } from '~/components/shared/MonthYearPicker';
@@ -22,7 +23,7 @@ export type CreateQuestionEncounterData = {
export type CreateQuestionEncounterFormProps = {
onCancel: () => void;
onSubmit: (data: CreateQuestionEncounterData) => void;
onSubmit: (data: CreateQuestionEncounterData) => Promise<void>;
};
export default function CreateQuestionEncounterForm({
@@ -30,6 +31,8 @@ export default function CreateQuestionEncounterForm({
onSubmit,
}: CreateQuestionEncounterFormProps) {
const [step, setStep] = useState(0);
const [loading, setLoading] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [selectedCompany, setSelectedCompany] = useState<string | null>(null);
const [selectedLocation, setSelectedLocation] = useState<Location | null>(
@@ -40,9 +43,18 @@ export default function CreateQuestionEncounterForm({
startOfMonth(new Date()),
);
if (submitted) {
return (
<div className="font-md flex items-center gap-1 rounded-full border bg-slate-50 py-1 pl-2 pr-3 text-sm text-slate-500">
<CheckIcon className="h-5 w-5" />
<p>Thank you for your response</p>
</div>
);
}
return (
<div className="flex items-center gap-2">
<p className="font-md text-md text-slate-600">
<p className="text-md text-slate-600">
I saw this question {step <= 1 ? 'at' : step === 2 ? 'for' : 'on'}
</p>
{step === 0 && (
@@ -128,9 +140,10 @@ export default function CreateQuestionEncounterForm({
)}
{step === 3 && (
<Button
isLoading={loading}
label="Submit"
variant="primary"
onClick={() => {
onClick={async () => {
if (
selectedCompany &&
selectedLocation &&
@@ -138,14 +151,20 @@ export default function CreateQuestionEncounterForm({
selectedDate
) {
const { cityId, stateId, countryId } = selectedLocation;
onSubmit({
cityId,
company: selectedCompany,
countryId,
role: selectedRole,
seenAt: selectedDate,
stateId,
});
setLoading(true);
try {
await onSubmit({
cityId,
company: selectedCompany,
countryId,
role: selectedRole,
seenAt: selectedDate,
stateId,
});
setSubmitted(true);
} finally {
setLoading(false);
}
}
}}
/>