[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