[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>pull/328/head
parent
6c91ec2077
commit
827550a5fd
After Width: | Height: | Size: 4.8 KiB |
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in new issue