[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 <jeffsy00@gmail.com>
This commit is contained in:
Ren Weilin
2022-10-08 16:08:12 +08:00
committed by GitHub
parent 6c91ec2077
commit 827550a5fd
10 changed files with 691 additions and 5 deletions

View File

@ -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<typeof TextInput>;
type FormTextInputProps = Omit<TextInputProps, 'onChange'> &
Pick<UseFormRegisterReturn<never>, 'onChange'>;
function FormTextInputWithRef(
props: FormTextInputProps,
ref?: ForwardedRef<HTMLInputElement>,
) {
const { onChange, ...rest } = props;
return (
<TextInput
{...(rest as TextInputProps)}
ref={ref}
onChange={(_, event) => onChange(event)}
/>
);
}
const FormTextInput = forwardRef(FormTextInputWithRef);
export type ContributeQuestionCardProps = {
onSubmit: (data: ContributeQuestionData) => void;
};
export default function ContributeQuestionCard({
onSubmit,
}: ContributeQuestionCardProps) {
const { register, handleSubmit } = useForm<ContributeQuestionData>();
const [isOpen, setOpen] = useState<boolean>(false);
return (
<>
<form
className="flex flex-col items-stretch justify-center gap-2 rounded-md border border-slate-300 p-4"
onSubmit={handleSubmit(onSubmit)}>
<FormTextInput
isLabelHidden={true}
label="Question"
placeholder="Contribute a question"
{...register('questionContent')}
/>
<div className="flex items-end justify-center gap-x-2">
<div className="min-w-[150px] flex-1">
<FormTextInput
label="Company"
startAddOn={BuildingOffice2Icon}
startAddOnType="icon"
{...register('company')}
/>
</div>
<div className="min-w-[150px] flex-1">
<FormTextInput
label="Question type"
startAddOn={QuestionMarkCircleIcon}
startAddOnType="icon"
{...register('questionType')}
/>
</div>
<div className="min-w-[150px] flex-1">
<FormTextInput
label="Date"
startAddOn={CalendarDaysIcon}
startAddOnType="icon"
{...register('date')}
/>
</div>
<Button
label="Contribute"
type="submit"
variant="primary"
onClick={() => setOpen(true)}
/>
</div>
</form>
<ContributeQuestionModal
contributeState={isOpen}
setContributeState={setOpen}></ContributeQuestionModal>
</>
);
}

View File

@ -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<SetStateAction<boolean>>;
};
export default function ContributeQuestionModal({
contributeState,
setContributeState,
}: ContributeQuestionModalProps) {
const [canSubmit, setCanSubmit] = useState<boolean>(false);
const handleCheckSimilarQuestions = (checked: boolean) => {
setCanSubmit(checked);
};
return (
<Transition.Root as={Fragment} show={contributeState}>
<Dialog
as="div"
className="relative z-10"
onClose={() => setContributeState(false)}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
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 transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900">
Question Draft
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
Question Contribution form
</p>
</div>
</div>
</div>
</div>
<div className="bg-primary-50 px-4 py-3 sm:flex sm:flex-row sm:justify-between sm:px-6">
<div className="mb-1 flex">
<Checkbox
checked={canSubmit}
label="I have checked that my question is new"
onChange={handleCheckSimilarQuestions}></Checkbox>
</div>
<div className=" flex gap-x-2">
<button
className="inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
type="button"
onClick={() => setContributeState(false)}>
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-gray-400 sm:ml-3 sm:w-auto sm:text-sm"
disabled={!canSubmit}
type="button"
onClick={() => setContributeState(false)}>
Contribute
</button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}

View File

@ -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 (
<header className="bg-indigo-600">
<nav aria-label="Top" className="max-w-8xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex w-full items-center justify-between border-b border-indigo-500 py-3 lg:border-none">
<div className="flex items-center">
<a className="flex items-center" href="/questions">
<span className="sr-only">TIH Question Bank</span>
<img alt="TIH Logo" className="h-10 w-auto" src="/logo.svg" />
<span className="ml-4 font-bold text-white">
TIH Question Bank
</span>
</a>
<div className="ml-8 hidden space-x-6 lg:block">
{navigation.map((link) => (
<Link
key={link.name}
className="font-sm text-sm text-white hover:text-indigo-50"
href={link.href}>
{link.name}
</Link>
))}
</div>
</div>
<div className="ml-8 space-x-4">
<a
className="inline-block rounded-md border border-transparent bg-indigo-500 py-2 px-4 text-base font-medium text-white hover:bg-opacity-75"
href="#">
Sign in
</a>
</div>
</div>
<div className="flex flex-wrap justify-center space-x-6 py-4 lg:hidden">
{navigation.map((link) => (
<Link
key={link.name}
className="text-base font-medium text-white hover:text-indigo-50"
href={link.href}>
{link.name}
</Link>
))}
</div>
</nav>
</header>
);
}

View File

@ -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 (
<article className="flex gap-2 rounded-md border border-slate-300 p-4">
<div className="flex flex-col items-center">
<Button
icon={ChevronUpIcon}
isLabelHidden={true}
label="Upvote"
variant="tertiary"
/>
<p>{upvoteCount}</p>
<Button
icon={ChevronDownIcon}
isLabelHidden={true}
label="Downvote"
variant="tertiary"
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-slate-500">
<Badge label="Technical" variant="primary" />
<p className="text-xs">
{timestamp} · {location} · {role}
</p>
</div>
<p className="line-clamp-2 text-ellipsis">{content}</p>
<div className="flex gap-2">
<Button
addonPosition="start"
icon={ChatBubbleBottomCenterTextIcon}
label={`${answerCount} answers`}
size="sm"
variant="tertiary"
/>
<Button
addonPosition="start"
icon={EyeIcon}
label={`${similarCount} received this`}
size="sm"
variant="tertiary"
/>
</div>
</div>
</article>
);
}

View File

@ -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<SortOptions extends Array<SortOption>> = {
onSortChange?: (sortValue: SortOptions[number]['value']) => void;
sortOptions: SortOptions;
sortValue: SortOptions[number]['value'];
};
export default function QuestionSearchBar<
SortOptions extends Array<SortOption>,
>({
onSortChange,
sortOptions,
sortValue,
}: QuestionSearchBarProps<SortOptions>) {
return (
<div className="flex items-center gap-2">
<div className="flex-1 pt-1">
<TextInput
isLabelHidden={true}
label="Search by content"
placeholder="Search by content"
startAddOn={MagnifyingGlassIcon}
startAddOnType="icon"
/>
</div>
<span className="pl-3 pr-1 pt-1 text-sm">Sort by:</span>
<Select
display="inline"
label=""
options={sortOptions}
value={sortValue}
onChange={onSortChange}></Select>
</div>
);
}

View File

@ -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<FilterOptions>;
} & (
| {
searchPlaceholder: string;
showAll?: never;
}
| {
searchPlaceholder?: never;
showAll: true;
}
);
export default function FilterSection({
label,
options,
searchPlaceholder,
showAll,
onOptionChange,
}: FilterSectionProps) {
return (
<div className="mx-2">
<Collapsible defaultOpen={true} label={label}>
<div className="-mx-2 flex flex-col items-stretch gap-2">
{!showAll && (
<TextInput
isLabelHidden={true}
label={label}
placeholder={searchPlaceholder}
startAddOn={MagnifyingGlassIcon}
startAddOnType="icon"
/>
)}
<div className="mx-1">
{options.map((option) => (
<Checkbox
key={option.value}
{...option}
onChange={(checked) => {
onOptionChange(option.value, checked);
}}
/>
))}
</div>
</div>
</Collapsible>
</div>
);
}

View File

@ -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 (
<div className="flex items-center">
<input
checked={checked}
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300"
id={id}
type="checkbox"
onChange={(event) => onChange(event.target.checked)}
/>
<label className="ml-3 min-w-0 flex-1 text-gray-700" htmlFor={id}>
{label}
</label>
</div>
);
}