From 6c91ec2077cb8c721da2e85e3da12701cc636cb7 Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Sat, 8 Oct 2022 15:11:44 +0800 Subject: [PATCH 01/11] [ui][radio list] implementation --- apps/storybook/stories/radio-list.stories.tsx | 135 ++++++++++++++++++ packages/ui/src/RadioList/RadioList.tsx | 59 ++++++++ packages/ui/src/RadioList/RadioListContext.ts | 20 +++ packages/ui/src/RadioList/RadioListItem.tsx | 42 ++++++ packages/ui/src/index.tsx | 3 + 5 files changed, 259 insertions(+) create mode 100644 apps/storybook/stories/radio-list.stories.tsx create mode 100644 packages/ui/src/RadioList/RadioList.tsx create mode 100644 packages/ui/src/RadioList/RadioListContext.ts create mode 100644 packages/ui/src/RadioList/RadioListItem.tsx diff --git a/apps/storybook/stories/radio-list.stories.tsx b/apps/storybook/stories/radio-list.stories.tsx new file mode 100644 index 00000000..7da530bb --- /dev/null +++ b/apps/storybook/stories/radio-list.stories.tsx @@ -0,0 +1,135 @@ +import React, { useState } from 'react'; +import type { ComponentMeta } from '@storybook/react'; +import type { RadioListOrientation } from '@tih/ui'; +import { HorizontalDivider } from '@tih/ui'; +import { RadioList } from '@tih/ui'; + +const RadioListOrientations: ReadonlyArray = [ + 'horizontal', + 'vertical', +]; + +export default { + argTypes: { + description: { + control: 'text', + }, + label: { + control: 'text', + }, + orientation: { + control: { type: 'select' }, + options: RadioListOrientations, + }, + }, + component: RadioList, + title: 'RadioList', +} as ComponentMeta; + +export function Basic({ + description, + label, +}: Pick, 'description' | 'label'>) { + const items = [ + { + label: 'Apple', + value: 'apple', + }, + { + label: 'Banana', + value: 'banana', + }, + { + label: 'Orange', + value: 'orange', + }, + ]; + + return ( + + {items.map(({ label: itemLabel, value }) => ( + + ))} + + ); +} + +Basic.args = { + description: 'Your favorite fruit', + label: 'Choose a fruit', +}; + +export function Controlled() { + const items = [ + { + label: 'Apple', + value: 'apple', + }, + { + label: 'Banana', + value: 'banana', + }, + { + label: 'Orange', + value: 'orange', + }, + ]; + + const [value, setValue] = useState('apple'); + + return ( + setValue(newValue)}> + {items.map(({ label: itemLabel, value: itemValue }) => ( + + ))} + + ); +} + +export function Orientation() { + const items = [ + { + label: 'Apple', + value: 'apple', + }, + { + label: 'Banana', + value: 'banana', + }, + { + label: 'Orange', + value: 'orange', + }, + ]; + + return ( +
+ + {items.map(({ label: itemLabel, value: itemValue }) => ( + + ))} + + + + {items.map(({ label: itemLabel, value: itemValue }) => ( + + ))} + +
+ ); +} diff --git a/packages/ui/src/RadioList/RadioList.tsx b/packages/ui/src/RadioList/RadioList.tsx new file mode 100644 index 00000000..1d94fe8b --- /dev/null +++ b/packages/ui/src/RadioList/RadioList.tsx @@ -0,0 +1,59 @@ +import clsx from 'clsx'; +import type { ChangeEvent } from 'react'; + +import { RadioListContext } from './RadioListContext'; +import RadioListItem from './RadioListItem'; + +export type RadioListOrientation = 'horizontal' | 'vertical'; + +type Props = Readonly<{ + children: ReadonlyArray>; + defaultValue?: T; + description?: string; + isLabelHidden?: boolean; + label: string; + name?: string; + onChange?: (value: T, event: ChangeEvent) => void; + orientation?: RadioListOrientation; + value?: T; +}>; + +RadioList.Item = RadioListItem; + +export default function RadioList({ + children, + defaultValue, + description, + orientation = 'vertical', + isLabelHidden, + name, + label, + value, + onChange, +}: Props) { + return ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TODO: Figure out how to type the onChange. + +
+
+ + {description && ( +

{description}

+ )} +
+
+ TODO +
+ {children} +
+
+
+
+ ); +} diff --git a/packages/ui/src/RadioList/RadioListContext.ts b/packages/ui/src/RadioList/RadioListContext.ts new file mode 100644 index 00000000..6058bc09 --- /dev/null +++ b/packages/ui/src/RadioList/RadioListContext.ts @@ -0,0 +1,20 @@ +import type { ChangeEvent } from 'react'; +import { createContext, useContext } from 'react'; + +type RadioListContextValue = { + defaultValue?: T; + name?: string; + onChange?: ( + value: T, + event: ChangeEvent, + ) => undefined | void; + value?: T; +}; + +export const RadioListContext = + createContext | null>(null); +export function useRadioListContext(): RadioListContextValue | null { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TODO: Figure out how to type useContext with generics. + return useContext(RadioListContext); +} diff --git a/packages/ui/src/RadioList/RadioListItem.tsx b/packages/ui/src/RadioList/RadioListItem.tsx new file mode 100644 index 00000000..fd0e8c43 --- /dev/null +++ b/packages/ui/src/RadioList/RadioListItem.tsx @@ -0,0 +1,42 @@ +import { useId } from 'react'; + +import { useRadioListContext } from './RadioListContext'; + +type Props = Readonly<{ + label?: string; + value: T; +}>; + +export default function RadioListItem({ label, value }: Props) { + const id = useId(); + const context = useRadioListContext(); + + return ( +
+ { + context?.onChange?.(value, event); + } + : undefined + } + /> + +
+ ); +} diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index 2e14a0fe..545db2db 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -22,6 +22,9 @@ export { default as HorizontalDivider } from './HorizontalDivider/HorizontalDivi // Pagination export * from './Pagination/Pagination'; export { default as Pagination } from './Pagination/Pagination'; +// RadioList +export * from './RadioList/RadioList'; +export { default as RadioList } from './RadioList/RadioList'; // Select export * from './Select/Select'; export { default as Select } from './Select/Select'; From 827550a5fda257d68361e1a84611ac35bb7835f4 Mon Sep 17 00:00:00 2001 From: Ren Weilin <66356390+wlren@users.noreply.github.com> Date: Sat, 8 Oct 2022 16:08:12 +0800 Subject: [PATCH 02/11] [questions][feat] add homepage layout (#312) * [questions][feat] add homepage layout * [questions][fix] fix rebase errors * [questions][fix] startAddOn for search bar * [questions][feat] add nav bar * [questions][chore]Remove margins * [questions][feat] add filter section * [questions][ui] change filter section alignment * [questions][ui]Search bar in one row * [questions][ui] Contribute questions dialog * [questions][ui] wording changes Co-authored-by: Jeff Sieu --- apps/portal/public/logo.svg | 39 ++++ .../questions/ContributeQuestionCard.tsx | 102 +++++++++ .../questions/ContributeQuestionModal.tsx | 96 +++++++++ .../src/components/questions/NavBar.tsx | 56 +++++ .../questions/QuestionOverviewCard.tsx | 72 +++++++ .../questions/QuestionSearchBar.tsx | 42 ++++ .../questions/filter/FilterSection.tsx | 62 ++++++ .../questions/ui-patch/Checkbox.tsx | 25 +++ apps/portal/src/pages/questions/index.tsx | 200 +++++++++++++++++- packages/ui/src/Collapsible/Collapsible.tsx | 2 +- 10 files changed, 691 insertions(+), 5 deletions(-) create mode 100644 apps/portal/public/logo.svg create mode 100644 apps/portal/src/components/questions/ContributeQuestionCard.tsx create mode 100644 apps/portal/src/components/questions/ContributeQuestionModal.tsx create mode 100644 apps/portal/src/components/questions/NavBar.tsx create mode 100644 apps/portal/src/components/questions/QuestionOverviewCard.tsx create mode 100644 apps/portal/src/components/questions/QuestionSearchBar.tsx create mode 100644 apps/portal/src/components/questions/filter/FilterSection.tsx create mode 100644 apps/portal/src/components/questions/ui-patch/Checkbox.tsx diff --git a/apps/portal/public/logo.svg b/apps/portal/public/logo.svg new file mode 100644 index 00000000..ed21574f --- /dev/null +++ b/apps/portal/public/logo.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/portal/src/components/questions/ContributeQuestionCard.tsx b/apps/portal/src/components/questions/ContributeQuestionCard.tsx new file mode 100644 index 00000000..bd6c4d78 --- /dev/null +++ b/apps/portal/src/components/questions/ContributeQuestionCard.tsx @@ -0,0 +1,102 @@ +import type { ComponentProps, ForwardedRef } from 'react'; +import { useState } from 'react'; +import { forwardRef } from 'react'; +import type { UseFormRegisterReturn } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; +import { + BuildingOffice2Icon, + CalendarDaysIcon, + QuestionMarkCircleIcon, +} from '@heroicons/react/24/outline'; +import { Button, TextInput } from '@tih/ui'; + +import ContributeQuestionModal from './ContributeQuestionModal'; + +export type ContributeQuestionData = { + company: string; + date: Date; + questionContent: string; + questionType: string; +}; + +type TextInputProps = ComponentProps; + +type FormTextInputProps = Omit & + Pick, 'onChange'>; + +function FormTextInputWithRef( + props: FormTextInputProps, + ref?: ForwardedRef, +) { + const { onChange, ...rest } = props; + return ( + onChange(event)} + /> + ); +} + +const FormTextInput = forwardRef(FormTextInputWithRef); + +export type ContributeQuestionCardProps = { + onSubmit: (data: ContributeQuestionData) => void; +}; + +export default function ContributeQuestionCard({ + onSubmit, +}: ContributeQuestionCardProps) { + const { register, handleSubmit } = useForm(); + const [isOpen, setOpen] = useState(false); + return ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + + ); +} diff --git a/apps/portal/src/components/questions/ContributeQuestionModal.tsx b/apps/portal/src/components/questions/ContributeQuestionModal.tsx new file mode 100644 index 00000000..1da146e8 --- /dev/null +++ b/apps/portal/src/components/questions/ContributeQuestionModal.tsx @@ -0,0 +1,96 @@ +import type { Dispatch, SetStateAction } from 'react'; +import { Fragment, useState } from 'react'; +import { Dialog, Transition } from '@headlessui/react'; + +import Checkbox from './ui-patch/Checkbox'; + +export type ContributeQuestionModalProps = { + contributeState: boolean; + setContributeState: Dispatch>; +}; + +export default function ContributeQuestionModal({ + contributeState, + setContributeState, +}: ContributeQuestionModalProps) { + const [canSubmit, setCanSubmit] = useState(false); + + const handleCheckSimilarQuestions = (checked: boolean) => { + setCanSubmit(checked); + }; + + return ( + + setContributeState(false)}> + +
+ + +
+
+ + +
+
+
+ + Question Draft + +
+

+ Question Contribution form +

+
+
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+
+ ); +} diff --git a/apps/portal/src/components/questions/NavBar.tsx b/apps/portal/src/components/questions/NavBar.tsx new file mode 100644 index 00000000..99b76457 --- /dev/null +++ b/apps/portal/src/components/questions/NavBar.tsx @@ -0,0 +1,56 @@ +import Link from 'next/link'; + +const navigation = [ + { href: '/questions/landing', name: '*Landing*' }, + { href: '/questions', name: 'Home' }, + { href: '#', name: 'My Lists' }, + { href: '#', name: 'My Questions' }, + { href: '#', name: 'History' }, +]; + +export default function NavBar() { + return ( +
+ +
+ ); +} diff --git a/apps/portal/src/components/questions/QuestionOverviewCard.tsx b/apps/portal/src/components/questions/QuestionOverviewCard.tsx new file mode 100644 index 00000000..31435c04 --- /dev/null +++ b/apps/portal/src/components/questions/QuestionOverviewCard.tsx @@ -0,0 +1,72 @@ +import { + ChatBubbleBottomCenterTextIcon, + ChevronDownIcon, + ChevronUpIcon, + EyeIcon, +} from '@heroicons/react/24/outline'; +import { Badge, Button } from '@tih/ui'; + +export type QuestionOverviewCardProps = { + answerCount: number; + content: string; + location: string; + role: string; + similarCount: number; + timestamp: string; + upvoteCount: number; +}; + +export default function QuestionOverviewCard({ + answerCount, + content, + similarCount, + upvoteCount, + timestamp, + role, + location, +}: QuestionOverviewCardProps) { + return ( +
+
+
+
+
+ +

+ {timestamp} · {location} · {role} +

+
+

{content}

+
+
+
+
+ ); +} diff --git a/apps/portal/src/components/questions/QuestionSearchBar.tsx b/apps/portal/src/components/questions/QuestionSearchBar.tsx new file mode 100644 index 00000000..02ba0a89 --- /dev/null +++ b/apps/portal/src/components/questions/QuestionSearchBar.tsx @@ -0,0 +1,42 @@ +import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; +import { Select, TextInput } from '@tih/ui'; + +export type SortOption = { + label: string; + value: string; +}; + +export type QuestionSearchBarProps> = { + onSortChange?: (sortValue: SortOptions[number]['value']) => void; + sortOptions: SortOptions; + sortValue: SortOptions[number]['value']; +}; + +export default function QuestionSearchBar< + SortOptions extends Array, +>({ + onSortChange, + sortOptions, + sortValue, +}: QuestionSearchBarProps) { + return ( +
+
+ +
+ Sort by: + +
+ ); +} diff --git a/apps/portal/src/components/questions/filter/FilterSection.tsx b/apps/portal/src/components/questions/filter/FilterSection.tsx new file mode 100644 index 00000000..5954e669 --- /dev/null +++ b/apps/portal/src/components/questions/filter/FilterSection.tsx @@ -0,0 +1,62 @@ +import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; +import { Collapsible, TextInput } from '@tih/ui'; + +import Checkbox from '../ui-patch/Checkbox'; + +export type FilterOptions = { + checked: boolean; + label: string; + value: string; +}; + +export type FilterSectionProps = { + label: string; + onOptionChange: (optionValue: string, checked: boolean) => void; + options: Array; +} & ( + | { + searchPlaceholder: string; + showAll?: never; + } + | { + searchPlaceholder?: never; + showAll: true; + } +); + +export default function FilterSection({ + label, + options, + searchPlaceholder, + showAll, + onOptionChange, +}: FilterSectionProps) { + return ( +
+ +
+ {!showAll && ( + + )} +
+ {options.map((option) => ( + { + onOptionChange(option.value, checked); + }} + /> + ))} +
+
+
+
+ ); +} diff --git a/apps/portal/src/components/questions/ui-patch/Checkbox.tsx b/apps/portal/src/components/questions/ui-patch/Checkbox.tsx new file mode 100644 index 00000000..413c12fe --- /dev/null +++ b/apps/portal/src/components/questions/ui-patch/Checkbox.tsx @@ -0,0 +1,25 @@ +import { useId } from 'react'; + +export type CheckboxProps = { + checked: boolean; + label: string; + onChange: (checked: boolean) => void; +}; + +export default function Checkbox({ label, checked, onChange }: CheckboxProps) { + const id = useId(); + return ( +
+ onChange(event.target.checked)} + /> + +
+ ); +} diff --git a/apps/portal/src/pages/questions/index.tsx b/apps/portal/src/pages/questions/index.tsx index f8fbc5ec..cf6611e3 100644 --- a/apps/portal/src/pages/questions/index.tsx +++ b/apps/portal/src/pages/questions/index.tsx @@ -1,10 +1,202 @@ -import QuestionBankTitle from '~/components/questions/QuestionBankTitle'; +import { useMemo, useState } from 'react'; + +import ContributeQuestionCard from '~/components/questions/ContributeQuestionCard'; +import type { FilterOptions } from '~/components/questions/filter/FilterSection'; +import FilterSection from '~/components/questions/filter/FilterSection'; +import NavBar from '~/components/questions/NavBar'; +import QuestionOverviewCard from '~/components/questions/QuestionOverviewCard'; +import QuestionSearchBar from '~/components/questions/QuestionSearchBar'; + +type FilterChoices = Array>; + +const companies: FilterChoices = [ + { + label: 'Google', + value: 'Google', + }, + { + label: 'Meta', + value: 'meta', + }, +]; + +// Code, design, behavioral +const questionTypes: FilterChoices = [ + { + label: 'Code', + value: 'code', + }, + { + label: 'Design', + value: 'design', + }, + { + label: 'Behavioral', + value: 'behavioral', + }, +]; + +const questionAges: FilterChoices = [ + { + label: 'Last month', + value: 'last-month', + }, + { + label: 'Last 6 months', + value: 'last-6-months', + }, + { + label: 'Last year', + value: 'last-year', + }, +]; + +const locations: FilterChoices = [ + { + label: 'Singapore', + value: 'singapore', + }, +]; export default function QuestionsHomePage() { + const [selectedCompanies, setSelectedCompanies] = useState>([]); + const [selectedQuestionTypes, setSelectedQuestionTypes] = useState< + Array + >([]); + const [selectedQuestionAges, setSelectedQuestionAges] = useState< + Array + >([]); + const [selectedLocations, setSelectedLocations] = useState>([]); + + const companyFilterOptions = useMemo(() => { + return companies.map((company) => ({ + ...company, + checked: selectedCompanies.includes(company.value), + })); + }, [selectedCompanies]); + + const questionTypeFilterOptions = useMemo(() => { + return questionTypes.map((questionType) => ({ + ...questionType, + checked: selectedQuestionTypes.includes(questionType.value), + })); + }, [selectedQuestionTypes]); + + const questionAgeFilterOptions = useMemo(() => { + return questionAges.map((questionAge) => ({ + ...questionAge, + checked: selectedQuestionAges.includes(questionAge.value), + })); + }, [selectedQuestionAges]); + + const locationFilterOptions = useMemo(() => { + return locations.map((location) => ({ + ...location, + checked: selectedLocations.includes(location.value), + })); + }, [selectedLocations]); + return ( -
-
- +
+
+ +
+
+
+

Filter by

+
+ { + if (checked) { + setSelectedCompanies((prev) => [...prev, optionValue]); + } else { + setSelectedCompanies((prev) => + prev.filter((company) => company !== optionValue), + ); + } + }} + /> + { + if (checked) { + setSelectedQuestionTypes((prev) => [...prev, optionValue]); + } else { + setSelectedQuestionTypes((prev) => + prev.filter((questionType) => questionType !== optionValue), + ); + } + }} + /> + { + if (checked) { + setSelectedQuestionAges((prev) => [...prev, optionValue]); + } else { + setSelectedQuestionAges((prev) => + prev.filter((questionAge) => questionAge !== optionValue), + ); + } + }} + /> + { + if (checked) { + setSelectedLocations((prev) => [...prev, optionValue]); + } else { + setSelectedLocations((prev) => + prev.filter((location) => location !== optionValue), + ); + } + }} + /> +
+
+
+
+
+ { + // eslint-disable-next-line no-console + console.log(data); + }} + /> + + +
+
+
); diff --git a/packages/ui/src/Collapsible/Collapsible.tsx b/packages/ui/src/Collapsible/Collapsible.tsx index 93e5ba10..211c54fd 100644 --- a/packages/ui/src/Collapsible/Collapsible.tsx +++ b/packages/ui/src/Collapsible/Collapsible.tsx @@ -23,7 +23,7 @@ export default function Collapsible({ children, defaultOpen, label }: Props) { /> {label} - + {children} From 2f50694016755ea4bec134205ac1f85bab2926f7 Mon Sep 17 00:00:00 2001 From: Su Yin <53945359+tnsyn@users.noreply.github.com> Date: Sat, 8 Oct 2022 20:57:04 +0800 Subject: [PATCH 03/11] [resumes][feat] Fetch resumes for browse tabs (#326) * [resumes][fix] Change browse list item styling * [resumes][feat] Add protected tabs router for browse page * [resumes][feat] Fetch all, starred and my resumes in browse page * [resumes][fix] Fix overflow y scrolling * [resumes][fix] Use date-fns to format upload time text --- .../resumes/browse/BrowseListItem.tsx | 13 +-- .../components/resumes/browse/constants.ts | 6 ++ apps/portal/src/pages/resumes/index.tsx | 58 ++++++++++-- apps/portal/src/server/router/index.ts | 2 + .../resumes-resume-protected-tabs-router.ts | 93 +++++++++++++++++++ apps/portal/src/server/router/resumes.ts | 2 +- 6 files changed, 158 insertions(+), 16 deletions(-) create mode 100644 apps/portal/src/server/router/resumes-resume-protected-tabs-router.ts diff --git a/apps/portal/src/components/resumes/browse/BrowseListItem.tsx b/apps/portal/src/components/resumes/browse/BrowseListItem.tsx index 3588f0c0..3c10a463 100644 --- a/apps/portal/src/components/resumes/browse/BrowseListItem.tsx +++ b/apps/portal/src/components/resumes/browse/BrowseListItem.tsx @@ -1,3 +1,4 @@ +import { formatDistanceToNow } from 'date-fns'; import Link from 'next/link'; import type { UrlObject } from 'url'; import { ChevronRightIcon } from '@heroicons/react/20/solid'; @@ -13,8 +14,8 @@ type Props = Readonly<{ export default function BrowseListItem({ href, resumeInfo }: Props) { return ( -
-
+
+
{resumeInfo.title}
{resumeInfo.role} @@ -33,11 +34,11 @@ export default function BrowseListItem({ href, resumeInfo }: Props) {
-
- {/* TODO: Replace hardcoded days ago with calculated days ago*/} - Uploaded 2 days ago by {resumeInfo.user} +
+ Uploaded {formatDistanceToNow(resumeInfo.createdAt)} ago by{' '} + {resumeInfo.user}
- +
); diff --git a/apps/portal/src/components/resumes/browse/constants.ts b/apps/portal/src/components/resumes/browse/constants.ts index 6a52fc7f..af98c36d 100644 --- a/apps/portal/src/components/resumes/browse/constants.ts +++ b/apps/portal/src/components/resumes/browse/constants.ts @@ -1,3 +1,9 @@ +export const BROWSE_TABS_VALUES = { + ALL: 'all', + MY: 'my', + STARRED: 'starred', +}; + export const SORT_OPTIONS = [ { current: true, href: '#', name: 'Latest' }, { current: false, href: '#', name: 'Popular' }, diff --git a/apps/portal/src/pages/resumes/index.tsx b/apps/portal/src/pages/resumes/index.tsx index fb0ca8b0..84515f81 100644 --- a/apps/portal/src/pages/resumes/index.tsx +++ b/apps/portal/src/pages/resumes/index.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import { Fragment, useState } from 'react'; +import { Fragment, useEffect, useState } from 'react'; import { Disclosure, Menu, Transition } from '@headlessui/react'; import { ChevronDownIcon, @@ -11,6 +11,7 @@ import { Tabs, TextInput } from '@tih/ui'; import BrowseListItem from '~/components/resumes/browse/BrowseListItem'; import { + BROWSE_TABS_VALUES, EXPERIENCE, LOCATION, ROLES, @@ -19,6 +20,8 @@ import { } from '~/components/resumes/browse/constants'; import FilterPill from '~/components/resumes/browse/FilterPill'; +import type { Resume } from '~/types/resume'; + const filters = [ { id: 'roles', @@ -41,12 +44,47 @@ import ResumeReviewsTitle from '~/components/resumes/ResumeReviewsTitle'; import { trpc } from '~/utils/trpc'; export default function ResumeHomePage() { - const [tabsValue, setTabsValue] = useState('all'); + const [tabsValue, setTabsValue] = useState(BROWSE_TABS_VALUES.ALL); const [searchValue, setSearchValue] = useState(''); - const resumesQuery = trpc.useQuery(['resumes.resume.list']); + const [resumes, setResumes] = useState(Array()); + + const allResumesQuery = trpc.useQuery(['resumes.resume.all'], { + enabled: tabsValue === BROWSE_TABS_VALUES.ALL, + }); + const starredResumesQuery = trpc.useQuery(['resumes.resume.browse.stars'], { + enabled: tabsValue === BROWSE_TABS_VALUES.STARRED, + }); + const myResumesQuery = trpc.useQuery(['resumes.resume.browse.my'], { + enabled: tabsValue === BROWSE_TABS_VALUES.MY, + }); + + useEffect(() => { + switch (tabsValue) { + case BROWSE_TABS_VALUES.ALL: { + setResumes(allResumesQuery.data ?? Array()); + break; + } + case BROWSE_TABS_VALUES.STARRED: { + setResumes(starredResumesQuery.data ?? Array()); + break; + } + case BROWSE_TABS_VALUES.MY: { + setResumes(myResumesQuery.data ?? Array()); + break; + } + default: { + setResumes(Array()); + } + } + }, [ + allResumesQuery.data, + starredResumesQuery.data, + myResumesQuery.data, + tabsValue, + ]); return ( -
+
@@ -64,15 +102,15 @@ export default function ResumeHomePage() { tabs={[ { label: 'All Resumes', - value: 'all', + value: BROWSE_TABS_VALUES.ALL, }, { label: 'Starred Resumes', - value: 'starred', + value: BROWSE_TABS_VALUES.STARRED, }, { label: 'My Resumes', - value: 'my', + value: BROWSE_TABS_VALUES.MY, }, ]} value={tabsValue} @@ -223,12 +261,14 @@ export default function ResumeHomePage() {
- {resumesQuery.isLoading ? ( + {allResumesQuery.isLoading || + starredResumesQuery.isLoading || + myResumesQuery.isLoading ? (
Loading...
) : (
    - {resumesQuery.data?.map((resumeObj) => ( + {resumes.map((resumeObj) => (
  • diff --git a/apps/portal/src/server/router/index.ts b/apps/portal/src/server/router/index.ts index ddeff431..cae45dde 100644 --- a/apps/portal/src/server/router/index.ts +++ b/apps/portal/src/server/router/index.ts @@ -4,6 +4,7 @@ import { createRouter } from './context'; import { protectedExampleRouter } from './protected-example-router'; import { resumesRouter } from './resumes'; import { resumesDetailsRouter } from './resumes-details-router'; +import { resumesResumeProtectedTabsRouter } from './resumes-resume-protected-tabs-router'; import { resumesResumeUserRouter } from './resumes-resume-user-router'; import { resumeReviewsRouter } from './resumes-reviews-router'; import { resumesReviewsUserRouter } from './resumes-reviews-user-router'; @@ -21,6 +22,7 @@ export const appRouter = createRouter() .merge('resumes.resume.', resumesRouter) .merge('resumes.details.', resumesDetailsRouter) .merge('resumes.resume.user.', resumesResumeUserRouter) + .merge('resumes.resume.browse.', resumesResumeProtectedTabsRouter) .merge('resumes.reviews.', resumeReviewsRouter) .merge('resumes.reviews.user.', resumesReviewsUserRouter); diff --git a/apps/portal/src/server/router/resumes-resume-protected-tabs-router.ts b/apps/portal/src/server/router/resumes-resume-protected-tabs-router.ts new file mode 100644 index 00000000..dd6535bb --- /dev/null +++ b/apps/portal/src/server/router/resumes-resume-protected-tabs-router.ts @@ -0,0 +1,93 @@ +import { createProtectedRouter } from './context'; + +import type { Resume } from '~/types/resume'; + +export const resumesResumeProtectedTabsRouter = createProtectedRouter() + .query('stars', { + async resolve({ ctx }) { + const userId = ctx.session?.user?.id; + const resumeStarsData = await ctx.prisma.resumesStar.findMany({ + include: { + resume: { + include: { + _count: { + select: { + comments: true, + stars: true, + }, + }, + }, + }, + user: { + select: { + name: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + where: { + userId, + }, + }); + return resumeStarsData.map((rs) => { + const resume: Resume = { + additionalInfo: rs.resume.additionalInfo, + createdAt: rs.resume.createdAt, + experience: rs.resume.experience, + id: rs.id, + location: rs.resume.location, + numComments: rs.resume._count.comments, + numStars: rs.resume._count.stars, + role: rs.resume.role, + title: rs.resume.title, + url: rs.resume.url, + user: rs.user.name!, + }; + return resume; + }); + }, + }) + .query('my', { + async resolve({ ctx }) { + const userId = ctx.session?.user?.id; + const resumesData = await ctx.prisma.resumesResume.findMany({ + include: { + _count: { + select: { + comments: true, + stars: true, + }, + }, + user: { + select: { + name: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + where: { + userId, + }, + }); + return resumesData.map((r) => { + const resume: Resume = { + additionalInfo: r.additionalInfo, + createdAt: r.createdAt, + experience: r.experience, + id: r.id, + location: r.location, + numComments: r._count.comments, + numStars: r._count.stars, + role: r.role, + title: r.title, + url: r.url, + user: r.user.name!, + }; + return resume; + }); + }, + }); diff --git a/apps/portal/src/server/router/resumes.ts b/apps/portal/src/server/router/resumes.ts index 2e7f9f9c..2125f446 100644 --- a/apps/portal/src/server/router/resumes.ts +++ b/apps/portal/src/server/router/resumes.ts @@ -2,7 +2,7 @@ import { createRouter } from './context'; import type { Resume } from '~/types/resume'; -export const resumesRouter = createRouter().query('list', { +export const resumesRouter = createRouter().query('all', { async resolve({ ctx }) { const resumesData = await ctx.prisma.resumesResume.findMany({ include: { From 53be75b7d5870bddff84cfdc23ff42cfa65845a9 Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Sat, 8 Oct 2022 21:20:05 +0800 Subject: [PATCH 04/11] [ui][select] support required --- apps/storybook/stories/select.stories.tsx | 29 +++++++++++++++++++++++ packages/ui/src/Select/Select.tsx | 8 +++++++ 2 files changed, 37 insertions(+) diff --git a/apps/storybook/stories/select.stories.tsx b/apps/storybook/stories/select.stories.tsx index 145936ed..35c74362 100644 --- a/apps/storybook/stories/select.stories.tsx +++ b/apps/storybook/stories/select.stories.tsx @@ -177,6 +177,35 @@ export function Disabled() { ); } +export function Required() { + const [value, setValue] = useState('apple'); + + return ( +
    + ( defaultValue={defaultValue != null ? String(defaultValue) : undefined} disabled={disabled} id={id} + required={required} value={value != null ? String(value) : undefined} onChange={(event) => { onChange?.(event.target.value); From a82890329946d11f8c885243abd1af968365a62f Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Sat, 8 Oct 2022 21:20:29 +0800 Subject: [PATCH 05/11] [ui][radio list] support required, disabled and item descriptions --- apps/storybook/stories/radio-list.stories.tsx | 127 ++++++++++++++++++ packages/ui/src/RadioList/RadioList.tsx | 49 ++++--- packages/ui/src/RadioList/RadioListContext.ts | 1 + packages/ui/src/RadioList/RadioListItem.tsx | 90 +++++++++---- 4 files changed, 223 insertions(+), 44 deletions(-) diff --git a/apps/storybook/stories/radio-list.stories.tsx b/apps/storybook/stories/radio-list.stories.tsx index 7da530bb..4217bd13 100644 --- a/apps/storybook/stories/radio-list.stories.tsx +++ b/apps/storybook/stories/radio-list.stories.tsx @@ -83,6 +83,7 @@ export function Controlled() { return ( setValue(newValue)}> @@ -93,6 +94,132 @@ export function Controlled() { ); } +export function Required() { + const items = [ + { + label: 'Apple', + value: 'apple', + }, + { + label: 'Banana', + value: 'banana', + }, + { + label: 'Orange', + value: 'orange', + }, + ]; + + const [value, setValue] = useState('apple'); + + return ( + setValue(newValue)}> + {items.map(({ label: itemLabel, value: itemValue }) => ( + + ))} + + ); +} + +export function Disabled() { + const items = [ + { + description: 'A red fruit', + disabled: false, + label: 'Apple', + value: 'apple', + }, + { + description: 'A yellow fruit', + disabled: true, + label: 'Banana', + value: 'banana', + }, + { + description: 'An orange fruit', + disabled: false, + label: 'Orange', + value: 'orange', + }, + ]; + + const [value, setValue] = useState('apple'); + + return ( +
    + setValue(newValue)}> + {items.map(({ label: itemLabel, value: itemValue }) => ( + + ))} + + + + {items.map( + ({ description, label: itemLabel, value: itemValue, disabled }) => ( + + ), + )} + +
    + ); +} + +export function ItemDescriptions() { + const items = [ + { + description: 'A red fruit', + label: 'Apple', + value: 'apple', + }, + { + description: 'A yellow fruit', + label: 'Banana', + value: 'banana', + }, + { + description: 'An orange fruit', + label: 'Orange', + value: 'orange', + }, + ]; + + const [value, setValue] = useState('apple'); + + return ( + setValue(newValue)}> + {items.map(({ description, label: itemLabel, value: itemValue }) => ( + + ))} + + ); +} + export function Orientation() { const items = [ { diff --git a/packages/ui/src/RadioList/RadioList.tsx b/packages/ui/src/RadioList/RadioList.tsx index 1d94fe8b..974491f1 100644 --- a/packages/ui/src/RadioList/RadioList.tsx +++ b/packages/ui/src/RadioList/RadioList.tsx @@ -1,5 +1,6 @@ import clsx from 'clsx'; import type { ChangeEvent } from 'react'; +import { useId } from 'react'; import { RadioListContext } from './RadioListContext'; import RadioListItem from './RadioListItem'; @@ -10,11 +11,13 @@ type Props = Readonly<{ children: ReadonlyArray>; defaultValue?: T; description?: string; + disabled?: boolean; isLabelHidden?: boolean; label: string; name?: string; onChange?: (value: T, event: ChangeEvent) => void; orientation?: RadioListOrientation; + required?: boolean; value?: T; }>; @@ -24,35 +27,47 @@ export default function RadioList({ children, defaultValue, description, + disabled, orientation = 'vertical', isLabelHidden, name, label, + required, value, onChange, }: Props) { + const labelId = useId(); return ( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore TODO: Figure out how to type the onChange. - +
    -
    - +
    + {description && ( -

    {description}

    +

    {description}

    )}
    -
    - TODO -
    - {children} -
    -
    +
    + {children} +
    ); diff --git a/packages/ui/src/RadioList/RadioListContext.ts b/packages/ui/src/RadioList/RadioListContext.ts index 6058bc09..0e23210f 100644 --- a/packages/ui/src/RadioList/RadioListContext.ts +++ b/packages/ui/src/RadioList/RadioListContext.ts @@ -3,6 +3,7 @@ import { createContext, useContext } from 'react'; type RadioListContextValue = { defaultValue?: T; + disabled?: boolean; name?: string; onChange?: ( value: T, diff --git a/packages/ui/src/RadioList/RadioListItem.tsx b/packages/ui/src/RadioList/RadioListItem.tsx index fd0e8c43..3b852a89 100644 --- a/packages/ui/src/RadioList/RadioListItem.tsx +++ b/packages/ui/src/RadioList/RadioListItem.tsx @@ -1,42 +1,78 @@ +import clsx from 'clsx'; import { useId } from 'react'; import { useRadioListContext } from './RadioListContext'; type Props = Readonly<{ - label?: string; + description?: string; + disabled?: boolean; + label: string; value: T; }>; -export default function RadioListItem({ label, value }: Props) { +export default function RadioListItem({ + description, + disabled: disabledProp = false, + label, + value, +}: Props) { const id = useId(); + const descriptionId = useId(); const context = useRadioListContext(); + const disabled = context?.disabled ?? disabledProp; return ( -
    - { - context?.onChange?.(value, event); - } - : undefined - } - /> - +
    +
    + { + context?.onChange?.(value, event); + } + : undefined + } + /> +
    +
    + + {description && ( +

    + {description} +

    + )} +
    ); } From 101f6c7d705df29a0249fbe648ef02271d48753f Mon Sep 17 00:00:00 2001 From: Su Yin <53945359+tnsyn@users.noreply.github.com> Date: Sat, 8 Oct 2022 21:50:19 +0800 Subject: [PATCH 06/11] [resumes][feat] Add basic linking of pages (#328) * [resumes][fix] Fix scrolling on upload page * [resumes][feat] Add basic linking of pages * [resumes][fix] Remove link to resume detail page --- apps/portal/src/pages/resumes/index.tsx | 15 ++++++++++++++- apps/portal/src/pages/resumes/submit.tsx | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/portal/src/pages/resumes/index.tsx b/apps/portal/src/pages/resumes/index.tsx index 84515f81..ddbfbf5d 100644 --- a/apps/portal/src/pages/resumes/index.tsx +++ b/apps/portal/src/pages/resumes/index.tsx @@ -1,4 +1,6 @@ import clsx from 'clsx'; +import { useRouter } from 'next/router'; +import { useSession } from 'next-auth/react'; import { Fragment, useEffect, useState } from 'react'; import { Disclosure, Menu, Transition } from '@headlessui/react'; import { @@ -44,6 +46,8 @@ import ResumeReviewsTitle from '~/components/resumes/ResumeReviewsTitle'; import { trpc } from '~/utils/trpc'; export default function ResumeHomePage() { + const { data } = useSession(); + const router = useRouter(); const [tabsValue, setTabsValue] = useState(BROWSE_TABS_VALUES.ALL); const [searchValue, setSearchValue] = useState(''); const [resumes, setResumes] = useState(Array()); @@ -83,6 +87,14 @@ export default function ResumeHomePage() { tabsValue, ]); + const onClickNew = () => { + if (data?.user?.id) { + router.push('/resumes/submit'); + } else { + // TODO: Handle non-logged in user behaviour + } + }; + return (
    @@ -177,7 +189,8 @@ export default function ResumeHomePage() {
    diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx index 54bb0e6d..bff5ee83 100644 --- a/apps/portal/src/pages/resumes/submit.tsx +++ b/apps/portal/src/pages/resumes/submit.tsx @@ -126,7 +126,7 @@ export default function SubmitResumeForm() { Upload a resume -
    +
    From bead5bff14527d2c5117680cbf4854bf0ef396db Mon Sep 17 00:00:00 2001 From: Keane Chan Date: Sat, 8 Oct 2022 23:53:22 +0800 Subject: [PATCH 07/11] [resumes][feat] add required fields and use text area (#329) * [resumes][feat] add required fields and update UI * [resumes][refactor] use same lists --- .../components/resumes/browse/constants.ts | 34 ++++--- .../resumes/comments/CommentsForm.tsx | 18 ++-- apps/portal/src/pages/resumes/index.tsx | 5 +- apps/portal/src/pages/resumes/submit.tsx | 90 +++++++------------ 4 files changed, 66 insertions(+), 81 deletions(-) diff --git a/apps/portal/src/components/resumes/browse/constants.ts b/apps/portal/src/components/resumes/browse/constants.ts index af98c36d..18448cee 100644 --- a/apps/portal/src/components/resumes/browse/constants.ts +++ b/apps/portal/src/components/resumes/browse/constants.ts @@ -23,19 +23,33 @@ export const ROLES = [ label: 'Full-Stack Engineer', value: 'Full-Stack Engineer', }, - { checked: false, label: 'Frontend Engineer', value: 'frontend-engineer' }, - { checked: false, label: 'Backend Engineer', value: 'backend-engineer' }, - { checked: false, label: 'DevOps Engineer', value: 'devops-engineer' }, - { checked: false, label: 'iOS Engineer', value: 'ios-engineer' }, - { checked: false, label: 'Android Engineer', value: 'android-engineer' }, + { checked: false, label: 'Frontend Engineer', value: 'Frontend Engineer' }, + { checked: false, label: 'Backend Engineer', value: 'Backend Engineer' }, + { checked: false, label: 'DevOps Engineer', value: 'DevOps Engineer' }, + { checked: false, label: 'iOS Engineer', value: 'iOS Engineer' }, + { checked: false, label: 'Android Engineer', value: 'Android Engineer' }, ]; export const EXPERIENCE = [ - { checked: false, label: 'Freshman', value: 'freshman' }, - { checked: false, label: 'Sophomore', value: 'sophomore' }, - { checked: false, label: 'Junior', value: 'junior' }, - { checked: false, label: 'Senior', value: 'senior' }, - { checked: false, label: 'Fresh Grad (0-1 years)', value: 'freshgrad' }, + { checked: false, label: 'Freshman', value: 'Freshman' }, + { checked: false, label: 'Sophomore', value: 'Sophomore' }, + { checked: false, label: 'Junior', value: 'Junior' }, + { checked: false, label: 'Senior', value: 'Senior' }, + { + checked: false, + label: 'Fresh Grad (0-1 years)', + value: 'Fresh Grad (0-1 years)', + }, + { + checked: false, + label: 'Mid-level (2 - 5 years)', + value: 'Mid-level (2 - 5 years)', + }, + { + checked: false, + label: 'Senior (5+ years)', + value: 'Senior (5+ years)', + }, ]; export const LOCATION = [ diff --git a/apps/portal/src/components/resumes/comments/CommentsForm.tsx b/apps/portal/src/components/resumes/comments/CommentsForm.tsx index c7b7dc6a..a14f9e3e 100644 --- a/apps/portal/src/components/resumes/comments/CommentsForm.tsx +++ b/apps/portal/src/components/resumes/comments/CommentsForm.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; -import { Button, Dialog, TextInput } from '@tih/ui'; +import { Button, Dialog, TextArea } from '@tih/ui'; import { trpc } from '~/utils/trpc'; @@ -86,45 +86,39 @@ export default function CommentsForm({
    - {/* TODO: Convert TextInput to TextArea */}
    - onValueChange('general', value)} /> - onValueChange('education', value)} /> - onValueChange('experience', value)} /> - onValueChange('projects', value)} /> - onValueChange('skills', value)} />
    diff --git a/apps/portal/src/pages/resumes/index.tsx b/apps/portal/src/pages/resumes/index.tsx index ddbfbf5d..99891d5e 100644 --- a/apps/portal/src/pages/resumes/index.tsx +++ b/apps/portal/src/pages/resumes/index.tsx @@ -283,7 +283,10 @@ export default function ResumeHomePage() {
      {resumes.map((resumeObj) => (
    • - +
    • ))}
    diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx index bff5ee83..b95a202a 100644 --- a/apps/portal/src/pages/resumes/submit.tsx +++ b/apps/portal/src/pages/resumes/submit.tsx @@ -1,10 +1,17 @@ +import clsx from 'clsx'; import Head from 'next/head'; import { useRouter } from 'next/router'; import { useMemo, useState } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { PaperClipIcon } from '@heroicons/react/24/outline'; -import { Button, Select, TextInput } from '@tih/ui'; +import { Button, Select, TextArea, TextInput } from '@tih/ui'; + +import { + EXPERIENCE, + LOCATION, + ROLES, +} from '~/components/resumes/browse/constants'; import { trpc } from '~/utils/trpc'; @@ -13,7 +20,7 @@ const TITLE_PLACEHOLDER = const ADDITIONAL_INFO_PLACEHOLDER = `e.g. I’m applying for company XYZ. I have been resume-rejected by N companies that I have applied for. Please help me to review so company XYZ gives me an interview!`; const FILE_UPLOAD_ERROR = 'Please upload a PDF file that is less than 10MB.'; -const MAX_FILE_SIZE_LIMIT = 10485760; +const MAX_FILE_SIZE_LIMIT = 10000000; type IFormInput = { additionalInfo?: string; @@ -25,52 +32,6 @@ type IFormInput = { }; export default function SubmitResumeForm() { - // TODO: Use enums instead - const roleItems = [ - { - label: 'Frontend Engineer', - value: 'Frontend Engineer', - }, - { - label: 'Full-Stack Engineer', - value: 'Full-Stack Engineer', - }, - { - label: 'Backend Engineer', - value: 'Backend Engineer', - }, - ]; - - const experienceItems = [ - { - label: 'Fresh Graduate (0 - 1 years)', - value: 'Fresh Graduate (0 - 1 years)', - }, - { - label: 'Mid', - value: 'Mid', - }, - { - label: 'Senior', - value: 'Senior', - }, - ]; - - const locationItems = [ - { - label: 'United States', - value: 'United States', - }, - { - label: 'Singapore', - value: 'Singapore', - }, - { - label: 'India', - value: 'India', - }, - ]; - const resumeCreateMutation = trpc.useMutation('resumes.resume.user.create'); const router = useRouter(); @@ -139,6 +100,7 @@ export default function SubmitResumeForm() { errorMessage={errors?.title && 'Title cannot be empty!'} label="Title" placeholder={TITLE_PLACEHOLDER} + required={true} onChange={(val) => setValue('title', val)} />
    @@ -146,7 +108,8 @@ export default function SubmitResumeForm() { setValue('experience', val)} />
    @@ -163,27 +127,39 @@ export default function SubmitResumeForm() { {...register('location', { required: true })} label="Location" name="location" - options={locationItems} + options={LOCATION} + required={true} onChange={(val) => setValue('location', val)} />

    Upload resume (PDF format) +

    -
    +
    {resumeFile &&

    {resumeFile.name}

    }
    -
    +
    -

    or drag and drop

    PDF up to 10MB

    @@ -201,8 +176,7 @@ export default function SubmitResumeForm() { )}
    - {/* TODO: Use TextInputArea instead */} - Date: Sun, 9 Oct 2022 01:36:21 +0800 Subject: [PATCH 08/11] [questions][feat] add questions models (#323) * [questions] [feat] add questions models * [questions][feat] add question types * [questions][chore] update schema naming scheme * [questions][chore] update naming scheme * [questions][chore] updating naming scheme * [questions][feat] add location, role and comapny * [questions][feat] update vote enum --- .../migration.sql | 171 ++++++++++++++++++ .../migration.sql | 11 ++ .../migration.sql | 12 ++ .../migration.sql | 30 +++ apps/portal/prisma/schema.prisma | 171 ++++++++++++++++-- .../router/questions-question-router.ts | 0 apps/portal/src/types/questions-question.d.ts | 0 7 files changed, 383 insertions(+), 12 deletions(-) create mode 100644 apps/portal/prisma/migrations/20221007110225_add_questions_model/migration.sql create mode 100644 apps/portal/prisma/migrations/20221008090846_add_question_types/migration.sql create mode 100644 apps/portal/prisma/migrations/20221008114522_add_questions_company_loaction_role/migration.sql create mode 100644 apps/portal/prisma/migrations/20221008165306_update_vote_enum/migration.sql create mode 100644 apps/portal/src/server/router/questions-question-router.ts create mode 100644 apps/portal/src/types/questions-question.d.ts diff --git a/apps/portal/prisma/migrations/20221007110225_add_questions_model/migration.sql b/apps/portal/prisma/migrations/20221007110225_add_questions_model/migration.sql new file mode 100644 index 00000000..5fdad411 --- /dev/null +++ b/apps/portal/prisma/migrations/20221007110225_add_questions_model/migration.sql @@ -0,0 +1,171 @@ +-- CreateEnum +CREATE TYPE "QuestionsVote" AS ENUM ('NO_VOTE', 'UPVOTE', 'DOWNVOTE'); + +-- CreateTable +CREATE TABLE "QuestionsQuestion" ( + "id" TEXT NOT NULL, + "userId" TEXT, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "QuestionsQuestion_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "QuestionsQuestionEncounter" ( + "id" TEXT NOT NULL, + "questionId" TEXT NOT NULL, + "userId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "QuestionsQuestionEncounter_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "QuestionsQuestionVote" ( + "id" TEXT NOT NULL, + "questionId" TEXT NOT NULL, + "userId" TEXT, + "vote" "QuestionsVote" NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "QuestionsQuestionVote_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "QuestionsQuestionComment" ( + "id" TEXT NOT NULL, + "questionId" TEXT NOT NULL, + "userId" TEXT, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "QuestionsQuestionComment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "QuestionsQuestionCommentVote" ( + "id" TEXT NOT NULL, + "questionCommentId" TEXT NOT NULL, + "userId" TEXT, + "vote" "QuestionsVote" NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "QuestionsQuestionCommentVote_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "QuestionsAnswer" ( + "id" TEXT NOT NULL, + "questionId" TEXT NOT NULL, + "userId" TEXT, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "QuestionsAnswer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "QuestionsAnswerVote" ( + "id" TEXT NOT NULL, + "answerId" TEXT NOT NULL, + "userId" TEXT, + "vote" "QuestionsVote" NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "QuestionsAnswerVote_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "QuestionsAnswerComment" ( + "id" TEXT NOT NULL, + "answerId" TEXT NOT NULL, + "userId" TEXT, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "QuestionsAnswerComment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "QuestionsAnswerCommentVote" ( + "id" TEXT NOT NULL, + "answerCommentId" TEXT NOT NULL, + "userId" TEXT, + "vote" "QuestionsVote" NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "QuestionsAnswerCommentVote_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "QuestionsQuestionVote_questionId_userId_key" ON "QuestionsQuestionVote"("questionId", "userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "QuestionsQuestionCommentVote_questionCommentId_userId_key" ON "QuestionsQuestionCommentVote"("questionCommentId", "userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "QuestionsAnswerVote_answerId_userId_key" ON "QuestionsAnswerVote"("answerId", "userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "QuestionsAnswerCommentVote_answerCommentId_userId_key" ON "QuestionsAnswerCommentVote"("answerCommentId", "userId"); + +-- AddForeignKey +ALTER TABLE "QuestionsQuestion" ADD CONSTRAINT "QuestionsQuestion_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsQuestionEncounter" ADD CONSTRAINT "QuestionsQuestionEncounter_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsQuestionEncounter" ADD CONSTRAINT "QuestionsQuestionEncounter_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "QuestionsQuestion"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsQuestionVote" ADD CONSTRAINT "QuestionsQuestionVote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsQuestionVote" ADD CONSTRAINT "QuestionsQuestionVote_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "QuestionsQuestion"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsQuestionComment" ADD CONSTRAINT "QuestionsQuestionComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsQuestionComment" ADD CONSTRAINT "QuestionsQuestionComment_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "QuestionsQuestion"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsQuestionCommentVote" ADD CONSTRAINT "QuestionsQuestionCommentVote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsQuestionCommentVote" ADD CONSTRAINT "QuestionsQuestionCommentVote_questionCommentId_fkey" FOREIGN KEY ("questionCommentId") REFERENCES "QuestionsQuestionComment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsAnswer" ADD CONSTRAINT "QuestionsAnswer_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsAnswer" ADD CONSTRAINT "QuestionsAnswer_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "QuestionsQuestion"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsAnswerVote" ADD CONSTRAINT "QuestionsAnswerVote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsAnswerVote" ADD CONSTRAINT "QuestionsAnswerVote_answerId_fkey" FOREIGN KEY ("answerId") REFERENCES "QuestionsAnswer"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsAnswerComment" ADD CONSTRAINT "QuestionsAnswerComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsAnswerComment" ADD CONSTRAINT "QuestionsAnswerComment_answerId_fkey" FOREIGN KEY ("answerId") REFERENCES "QuestionsAnswer"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsAnswerCommentVote" ADD CONSTRAINT "QuestionsAnswerCommentVote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsAnswerCommentVote" ADD CONSTRAINT "QuestionsAnswerCommentVote_answerCommentId_fkey" FOREIGN KEY ("answerCommentId") REFERENCES "QuestionsAnswerComment"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/portal/prisma/migrations/20221008090846_add_question_types/migration.sql b/apps/portal/prisma/migrations/20221008090846_add_question_types/migration.sql new file mode 100644 index 00000000..0b35f78d --- /dev/null +++ b/apps/portal/prisma/migrations/20221008090846_add_question_types/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - Added the required column `questionType` to the `QuestionsQuestion` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "QuestionsQuestionType" AS ENUM ('CODING', 'SYSTEM_DESIGN', 'BEHAVIORAL'); + +-- AlterTable +ALTER TABLE "QuestionsQuestion" ADD COLUMN "questionType" "QuestionsQuestionType" NOT NULL; diff --git a/apps/portal/prisma/migrations/20221008114522_add_questions_company_loaction_role/migration.sql b/apps/portal/prisma/migrations/20221008114522_add_questions_company_loaction_role/migration.sql new file mode 100644 index 00000000..0a5830e1 --- /dev/null +++ b/apps/portal/prisma/migrations/20221008114522_add_questions_company_loaction_role/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `company` to the `QuestionsQuestionEncounter` table without a default value. This is not possible if the table is not empty. + - Added the required column `location` to the `QuestionsQuestionEncounter` table without a default value. This is not possible if the table is not empty. + - Added the required column `role` to the `QuestionsQuestionEncounter` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "QuestionsQuestionEncounter" ADD COLUMN "company" TEXT NOT NULL, +ADD COLUMN "location" TEXT NOT NULL, +ADD COLUMN "role" TEXT NOT NULL; diff --git a/apps/portal/prisma/migrations/20221008165306_update_vote_enum/migration.sql b/apps/portal/prisma/migrations/20221008165306_update_vote_enum/migration.sql new file mode 100644 index 00000000..bd09564a --- /dev/null +++ b/apps/portal/prisma/migrations/20221008165306_update_vote_enum/migration.sql @@ -0,0 +1,30 @@ +/* + Warnings: + + - Changed the type of `vote` on the `QuestionsAnswerCommentVote` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + - Changed the type of `vote` on the `QuestionsAnswerVote` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + - Changed the type of `vote` on the `QuestionsQuestionCommentVote` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + - Changed the type of `vote` on the `QuestionsQuestionVote` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- CreateEnum +CREATE TYPE "Vote" AS ENUM ('UPVOTE', 'DOWNVOTE'); + +-- AlterTable +ALTER TABLE "QuestionsAnswerCommentVote" DROP COLUMN "vote", +ADD COLUMN "vote" "Vote" NOT NULL; + +-- AlterTable +ALTER TABLE "QuestionsAnswerVote" DROP COLUMN "vote", +ADD COLUMN "vote" "Vote" NOT NULL; + +-- AlterTable +ALTER TABLE "QuestionsQuestionCommentVote" DROP COLUMN "vote", +ADD COLUMN "vote" "Vote" NOT NULL; + +-- AlterTable +ALTER TABLE "QuestionsQuestionVote" DROP COLUMN "vote", +ADD COLUMN "vote" "Vote" NOT NULL; + +-- DropEnum +DROP TYPE "QuestionsVote"; diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index 7ece9c5d..13287fea 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -37,18 +37,32 @@ model Session { } model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? - accounts Account[] - sessions Session[] - todos Todo[] - resumesResumes ResumesResume[] - resumesStars ResumesStar[] - resumesComments ResumesComment[] - resumesCommentVotes ResumesCommentVote[] + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] + todos Todo[] + resumesResumes ResumesResume[] + resumesStars ResumesStar[] + resumesComments ResumesComment[] + resumesCommentVotes ResumesCommentVote[] + questionsQuestions QuestionsQuestion[] + questionsQuestionEncounters QuestionsQuestionEncounter[] + questionsQuestionVotes QuestionsQuestionVote[] + questionsQuestionComments QuestionsQuestionComment[] + questionsQuestionCommentVotes QuestionsQuestionCommentVote[] + questionsAnswers QuestionsAnswer[] + questionsAnswerVotes QuestionsAnswerVote[] + questionsAnswerComments QuestionsAnswerComment[] + questionsAnswerCommentVotes QuestionsAnswerCommentVote[] +} + +enum Vote { + UPVOTE + DOWNVOTE } model VerificationToken { @@ -162,4 +176,137 @@ model ResumesCommentVote { // Add Questions project models here, prefix all models with "Questions", // use camelCase for field names, and try to name them consistently // across all models in this file. + +enum QuestionsQuestionType { + CODING + SYSTEM_DESIGN + BEHAVIORAL +} + +model QuestionsQuestion { + id String @id @default(cuid()) + userId String? + content String @db.Text + questionType QuestionsQuestionType + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + encounters QuestionsQuestionEncounter[] + votes QuestionsQuestionVote[] + comments QuestionsQuestionComment[] + answers QuestionsAnswer[] +} + +model QuestionsQuestionEncounter { + id String @id @default(cuid()) + questionId String + userId String? + // TODO: sync with models + company String @db.Text + location String @db.Text + role String @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) +} + +model QuestionsQuestionVote { + id String @id @default(cuid()) + questionId String + userId String? + vote Vote + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + + @@unique([questionId, userId]) +} + +model QuestionsQuestionComment { + id String @id @default(cuid()) + questionId String + userId String? + content String @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + votes QuestionsQuestionCommentVote[] +} + +model QuestionsQuestionCommentVote { + id String @id @default(cuid()) + questionCommentId String + userId String? + vote Vote + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + comment QuestionsQuestionComment @relation(fields: [questionCommentId], references: [id], onDelete: Cascade) + + @@unique([questionCommentId, userId]) +} + +model QuestionsAnswer { + id String @id @default(cuid()) + questionId String + userId String? + content String @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + votes QuestionsAnswerVote[] + comments QuestionsAnswerComment[] +} + +model QuestionsAnswerVote { + id String @id @default(cuid()) + answerId String + userId String? + vote Vote + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + answer QuestionsAnswer @relation(fields: [answerId], references: [id], onDelete: Cascade) + + @@unique([answerId, userId]) +} + +model QuestionsAnswerComment { + id String @id @default(cuid()) + answerId String + userId String? + content String @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + answer QuestionsAnswer @relation(fields: [answerId], references: [id], onDelete: Cascade) + votes QuestionsAnswerCommentVote[] +} + +model QuestionsAnswerCommentVote { + id String @id @default(cuid()) + answerCommentId String + userId String? + vote Vote + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + comment QuestionsAnswerComment @relation(fields: [answerCommentId], references: [id], onDelete: Cascade) + + @@unique([answerCommentId, userId]) +} + // End of Questions project models. diff --git a/apps/portal/src/server/router/questions-question-router.ts b/apps/portal/src/server/router/questions-question-router.ts new file mode 100644 index 00000000..e69de29b diff --git a/apps/portal/src/types/questions-question.d.ts b/apps/portal/src/types/questions-question.d.ts new file mode 100644 index 00000000..e69de29b From 21e5e0672ad39b2d29effe9f4029161b93010a49 Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Sun, 9 Oct 2022 08:54:57 +0800 Subject: [PATCH 09/11] [ui][radio list] remove disabled prop on radio list level --- apps/storybook/stories/radio-list.stories.tsx | 24 +++++++------------ packages/ui/src/RadioList/RadioList.tsx | 6 ++--- packages/ui/src/RadioList/RadioListContext.ts | 1 - packages/ui/src/RadioList/RadioListItem.tsx | 8 ++++--- 4 files changed, 16 insertions(+), 23 deletions(-) diff --git a/apps/storybook/stories/radio-list.stories.tsx b/apps/storybook/stories/radio-list.stories.tsx index 4217bd13..91ca7b72 100644 --- a/apps/storybook/stories/radio-list.stories.tsx +++ b/apps/storybook/stories/radio-list.stories.tsx @@ -29,7 +29,11 @@ export default { export function Basic({ description, label, -}: Pick, 'description' | 'label'>) { + orientation, +}: Pick< + React.ComponentProps, + 'description' | 'label' | 'orientation' +>) { const items = [ { label: 'Apple', @@ -50,7 +54,8 @@ export function Basic({ defaultValue="apple" description={description} label={label} - name="fruit"> + name="fruit" + orientation={orientation}> {items.map(({ label: itemLabel, value }) => ( ))} @@ -61,6 +66,7 @@ export function Basic({ Basic.args = { description: 'Your favorite fruit', label: 'Choose a fruit', + orientation: 'vertical', }; export function Controlled() { @@ -148,22 +154,10 @@ export function Disabled() { }, ]; - const [value, setValue] = useState('apple'); - return (
    setValue(newValue)}> - {items.map(({ label: itemLabel, value: itemValue }) => ( - - ))} - - - {items.map( diff --git a/packages/ui/src/RadioList/RadioList.tsx b/packages/ui/src/RadioList/RadioList.tsx index 974491f1..6919c819 100644 --- a/packages/ui/src/RadioList/RadioList.tsx +++ b/packages/ui/src/RadioList/RadioList.tsx @@ -11,7 +11,6 @@ type Props = Readonly<{ children: ReadonlyArray>; defaultValue?: T; description?: string; - disabled?: boolean; isLabelHidden?: boolean; label: string; name?: string; @@ -27,10 +26,9 @@ export default function RadioList({ children, defaultValue, description, - disabled, - orientation = 'vertical', isLabelHidden, name, + orientation = 'vertical', label, required, value, @@ -41,7 +39,7 @@ export default function RadioList({ + value={{ defaultValue, name, onChange, value }}>