From 90f8556f8c6d143d3e577be40050d78f79c539b5 Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Sun, 9 Oct 2022 17:52:39 +0800 Subject: [PATCH 1/8] [ui][typeahead] implementation --- .../components/global/ProductNavigation.tsx | 10 +- apps/storybook/stories/typeahead.stories.tsx | 76 ++++++++++++ .../HorizontalDivider/HorizontalDivider.tsx | 2 +- packages/ui/src/Typeahead/Typeahead.tsx | 109 ++++++++++++++++++ packages/ui/src/index.tsx | 3 + 5 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 apps/storybook/stories/typeahead.stories.tsx create mode 100644 packages/ui/src/Typeahead/Typeahead.tsx diff --git a/apps/portal/src/components/global/ProductNavigation.tsx b/apps/portal/src/components/global/ProductNavigation.tsx index e34cadd7..caae6c08 100644 --- a/apps/portal/src/components/global/ProductNavigation.tsx +++ b/apps/portal/src/components/global/ProductNavigation.tsx @@ -25,11 +25,11 @@ export default function ProductNavigation({ items, title }: Props) { {items.map((item) => item.children != null && item.children.length > 0 ? ( - + {item.name} ( {child.name} @@ -63,7 +63,7 @@ export default function ProductNavigation({ items, title }: Props) { ) : ( {item.name} diff --git a/apps/storybook/stories/typeahead.stories.tsx b/apps/storybook/stories/typeahead.stories.tsx new file mode 100644 index 00000000..0d846045 --- /dev/null +++ b/apps/storybook/stories/typeahead.stories.tsx @@ -0,0 +1,76 @@ +import React, { useState } from 'react'; +import type { ComponentMeta } from '@storybook/react'; +import type { TypeaheadOption } from '@tih/ui'; +import { Typeahead } from '@tih/ui'; + +export default { + argTypes: { + disabled: { + control: 'boolean', + }, + isLabelHidden: { + control: 'boolean', + }, + label: { + control: 'text', + }, + }, + component: Typeahead, + parameters: { + docs: { + iframeHeight: 400, + inlineStories: false, + }, + }, + title: 'Typeahead', +} as ComponentMeta; + +export function Basic({ + disabled, + isLabelHidden, + label, +}: Pick< + React.ComponentProps, + 'disabled' | 'isLabelHidden' | 'label' +>) { + const people = [ + { id: '1', label: 'Wade Cooper', value: '1' }, + { id: '2', label: 'Arlene Mccoy', value: '2' }, + { id: '3', label: 'Devon Webb', value: '3' }, + { id: '4', label: 'Tom Cook', value: '4' }, + { id: '5', label: 'Tanya Fox', value: '5' }, + { id: '6', label: 'Hellen Schmidt', value: '6' }, + ]; + const [selectedEntry, setSelectedEntry] = useState( + people[0], + ); + const [query, setQuery] = useState(''); + + const filteredPeople = + query === '' + ? people + : people.filter((person) => + person.label + .toLowerCase() + .replace(/\s+/g, '') + .includes(query.toLowerCase().replace(/\s+/g, '')), + ); + + return ( + + ); +} + +Basic.args = { + disabled: false, + isLabelHidden: false, + label: 'Author', +}; diff --git a/packages/ui/src/HorizontalDivider/HorizontalDivider.tsx b/packages/ui/src/HorizontalDivider/HorizontalDivider.tsx index c9f3d2b1..6e2396dc 100644 --- a/packages/ui/src/HorizontalDivider/HorizontalDivider.tsx +++ b/packages/ui/src/HorizontalDivider/HorizontalDivider.tsx @@ -8,7 +8,7 @@ export default function HorizontalDivider({ className }: Props) { return (
); } diff --git a/packages/ui/src/Typeahead/Typeahead.tsx b/packages/ui/src/Typeahead/Typeahead.tsx new file mode 100644 index 00000000..d80d9200 --- /dev/null +++ b/packages/ui/src/Typeahead/Typeahead.tsx @@ -0,0 +1,109 @@ +import clsx from 'clsx'; +import { Fragment, useState } from 'react'; +import { Combobox, Transition } from '@headlessui/react'; +import { ChevronUpDownIcon } from '@heroicons/react/20/solid'; + +export type TypeaheadOption = Readonly<{ + // String value to uniquely identify the option. + id: string; + label: string; + value: string; +}>; + +type Props = Readonly<{ + disabled?: boolean; + isLabelHidden?: boolean; + label: string; + onQueryChange: ( + value: string, + event: React.ChangeEvent, + ) => void; + onSelectOption: (option: TypeaheadOption) => void; + options: ReadonlyArray; + selectedOption: TypeaheadOption; +}>; + +export default function Typeahead({ + disabled = false, + isLabelHidden, + label, + options, + onQueryChange, + selectedOption, + onSelectOption, +}: Props) { + const [query, setQuery] = useState(''); + return ( + + + {label} + +
+
+ + (option as unknown as TypeaheadOption).label + } + onChange={(event) => { + !disabled && onQueryChange(event.target.value, event); + }} + /> + + +
+ setQuery('')} + as={Fragment} + leave="transition ease-in duration-100" + leaveFrom="opacity-100" + leaveTo="opacity-0"> + + {options.length === 0 && query !== '' ? ( +
+ Nothing found. +
+ ) : ( + options.map((option) => ( + + clsx( + 'relative cursor-default select-none py-2 px-4 text-slate-500', + active && 'bg-slate-100', + ) + } + value={option}> + {({ selected }) => ( + + {option.label} + + )} + + )) + )} +
+
+
+
+ ); +} diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index 1e4efb66..586920c7 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -49,3 +49,6 @@ export { default as TextArea } from './TextArea/TextArea'; // TextInput export * from './TextInput/TextInput'; export { default as TextInput } from './TextInput/TextInput'; +// Typeahead +export * from './Typeahead/Typeahead'; +export { default as Typeahead } from './Typeahead/Typeahead'; From c3c3dfceb540f04ffbcbed311a81833a4e62ae02 Mon Sep 17 00:00:00 2001 From: Keane Chan Date: Sun, 9 Oct 2022 17:55:06 +0800 Subject: [PATCH 2/8] Keane/update-submit-form (#338) * [resumes][fix] add isLoading effect on submit form * [resumes][fix] remove useeffect on browse page --- apps/portal/src/pages/resumes/index.tsx | 42 ++++++++---------------- apps/portal/src/pages/resumes/submit.tsx | 40 ++++++++++++++++------ 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/apps/portal/src/pages/resumes/index.tsx b/apps/portal/src/pages/resumes/index.tsx index fd0a304f..0cfde12e 100644 --- a/apps/portal/src/pages/resumes/index.tsx +++ b/apps/portal/src/pages/resumes/index.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import Head from 'next/head'; import { useRouter } from 'next/router'; import { useSession } from 'next-auth/react'; -import { Fragment, useEffect, useState } from 'react'; +import { Fragment, useState } from 'react'; import { Disclosure, Menu, Transition } from '@headlessui/react'; import { ChevronDownIcon, @@ -22,6 +22,7 @@ import { TOP_HITS, } from '~/components/resumes/browse/constants'; import FilterPill from '~/components/resumes/browse/FilterPill'; +import ResumeReviewsTitle from '~/components/resumes/ResumeReviewsTitle'; import type { Resume } from '~/types/resume'; @@ -42,12 +43,11 @@ const filters = [ options: LOCATION, }, ]; -import ResumeReviewsTitle from '~/components/resumes/ResumeReviewsTitle'; import { trpc } from '~/utils/trpc'; export default function ResumeHomePage() { - const { data } = useSession(); + const { data: sessionData } = useSession(); const router = useRouter(); const [tabsValue, setTabsValue] = useState(BROWSE_TABS_VALUES.ALL); const [searchValue, setSearchValue] = useState(''); @@ -55,41 +55,25 @@ export default function ResumeHomePage() { const allResumesQuery = trpc.useQuery(['resumes.resume.all'], { enabled: tabsValue === BROWSE_TABS_VALUES.ALL, + onSuccess: (data) => { + setResumes(data); + }, }); const starredResumesQuery = trpc.useQuery(['resumes.resume.browse.stars'], { enabled: tabsValue === BROWSE_TABS_VALUES.STARRED, + onSuccess: (data) => { + setResumes(data); + }, }); const myResumesQuery = trpc.useQuery(['resumes.resume.browse.my'], { enabled: tabsValue === BROWSE_TABS_VALUES.MY, + onSuccess: (data) => { + setResumes(data); + }, }); - useEffect(() => { - switch (tabsValue) { - case BROWSE_TABS_VALUES.ALL: { - setResumes(allResumesQuery.data ?? []); - break; - } - case BROWSE_TABS_VALUES.STARRED: { - setResumes(starredResumesQuery.data ?? []); - break; - } - case BROWSE_TABS_VALUES.MY: { - setResumes(myResumesQuery.data ?? []); - break; - } - default: { - setResumes([]); - } - } - }, [ - allResumesQuery.data, - starredResumesQuery.data, - myResumesQuery.data, - tabsValue, - ]); - const onClickNew = () => { - if (data?.user?.id) { + if (sessionData?.user?.id) { router.push('/resumes/submit'); } else { // TODO: Handle non-logged in user behaviour diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx index e6adec0f..1ca06031 100644 --- a/apps/portal/src/pages/resumes/submit.tsx +++ b/apps/portal/src/pages/resumes/submit.tsx @@ -40,6 +40,7 @@ export default function SubmitResumeForm() { const router = useRouter(); const [resumeFile, setResumeFile] = useState(); + const [isLoading, setIsLoading] = useState(false); const [invalidFileUploadError, setInvalidFileUploadError] = useState< string | null >(null); @@ -50,12 +51,18 @@ export default function SubmitResumeForm() { setValue, reset, formState: { errors }, - } = useForm(); + } = useForm({ + defaultValues: { + isChecked: false, + }, + }); const onSubmit: SubmitHandler = async (data) => { if (resumeFile == null) { + console.error('Resume file is empty'); return; } + setIsLoading(true); const formData = new FormData(); formData.append('key', RESUME_STORAGE_KEY); @@ -68,15 +75,27 @@ export default function SubmitResumeForm() { }); const { url } = res.data; - await resumeCreateMutation.mutate({ - additionalInfo: data.additionalInfo, - experience: data.experience, - location: data.location, - role: data.role, - title: data.title, - url, - }); - router.push('/resumes'); + resumeCreateMutation.mutate( + { + additionalInfo: data.additionalInfo, + experience: data.experience, + location: data.location, + role: data.role, + title: data.title, + url, + }, + { + onError: (error) => { + console.error(error); + }, + onSettled: () => { + setIsLoading(false); + }, + onSuccess: () => { + router.push('/resumes'); + }, + }, + ); }; const onUploadFile = (event: React.ChangeEvent) => { @@ -255,6 +274,7 @@ export default function SubmitResumeForm() {