mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-29 21:23:14 +08:00
[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:
39
apps/portal/public/logo.svg
Normal file
39
apps/portal/public/logo.svg
Normal file
@ -0,0 +1,39 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 582.39 458.49">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1{fill:#5252ee}.cls-2{fill:#7f7ff0}.cls-3{fill:#fff}.cls-4{fill:#363636}.cls-5{fill:#ff4585}.cls-6{fill:#00e5a0}
|
||||
</style>
|
||||
</defs>
|
||||
<ellipse cx="291.19" cy="428.46" fill="#eef2f5" rx="291.19" ry="30.03"/>
|
||||
<rect width="346.05" height="180.28" x="197.89" y="168.35" class="cls-1" rx="30" ry="30" transform="rotate(-90 370.92 258.49)"/>
|
||||
<rect width="346.05" height="180.28" x="189.19" y="168.35" class="cls-2" rx="30" ry="30" transform="rotate(-90 362.22 258.49)"/>
|
||||
<path d="M257.71 419.94V97.04h152.1a28.18 28.18 0 0128.18 28.18v266.54a28.18 28.18 0 01-28.18 28.18h-152.1z" class="cls-3"/>
|
||||
<path d="M206.96 431.51v-346h151.11a29.17 29.17 0 0129.17 29.08v287.75a29.17 29.17 0 01-29.17 29.17H206.96z" class="cls-1"/>
|
||||
<rect width="346.05" height="244.06" x="80.86" y="136.46" class="cls-2" rx="33.94" ry="33.94" transform="rotate(-90 253.89 258.49)"/>
|
||||
<path fill="#ffcb2c" d="M131.87 397.57V119.41a33.94 33.94 0 0133.94-33.94h5.06v346h-5.07a33.94 33.94 0 01-33.93-33.9z"/>
|
||||
<g opacity=".2">
|
||||
<circle cx="148.62" cy="375.73" r="11.84"/>
|
||||
<circle cx="148.62" cy="329.54" r="11.84"/>
|
||||
<circle cx="148.62" cy="283.35" r="11.84"/>
|
||||
<circle cx="148.62" cy="237.16" r="11.84"/>
|
||||
<circle cx="148.62" cy="190.96" r="11.84"/>
|
||||
<circle cx="148.62" cy="144.77" r="11.84"/>
|
||||
</g>
|
||||
<rect width="11.93" height="27.44" x="129.79" y="362.01" class="cls-4" rx="4.5" ry="4.5" transform="rotate(-90 135.755 375.735)"/>
|
||||
<rect width="11.93" height="27.44" x="129.79" y="315.82" class="cls-4" rx="4.5" ry="4.5" transform="rotate(-90 135.75 329.54)"/>
|
||||
<rect width="11.93" height="27.44" x="129.79" y="269.63" class="cls-4" rx="4.5" ry="4.5" transform="rotate(-90 135.75 283.35)"/>
|
||||
<rect width="11.93" height="27.44" x="129.79" y="223.44" class="cls-4" rx="4.5" ry="4.5" transform="rotate(-90 135.75 237.16)"/>
|
||||
<rect width="11.93" height="27.44" x="129.79" y="177.24" class="cls-4" rx="4.5" ry="4.5" transform="rotate(-90 135.755 190.965)"/>
|
||||
<rect width="11.93" height="27.44" x="129.79" y="131.05" class="cls-4" rx="4.5" ry="4.5" transform="rotate(-90 135.75 144.77)"/>
|
||||
<path d="M396.07 97.04h2.47v189.3h-2.47zm0 233.12h2.47v90.57h-2.47zm15.44-107.97h2.47v197.75h-2.47zm16.06 10.68h-2.47V101.54l2.47 1.79v129.54zm-2.47 58.71h2.47v45.59h-2.47zm-13.59-163h2.47v45.59h-2.47z" opacity=".1"/>
|
||||
<path d="M237.95 271a5 5 0 01-3.54-1.46L211.9 247a5 5 0 010-7.07l22.51-22.51a5 5 0 017.07 7.07l-19 19 19 19a5 5 0 01-3.54 8.54zm64.82 0a5 5 0 01-3.54-8.54l19-19-19-19a5 5 0 017.07-7.07l22.51 22.51a5 5 0 010 7.07l-22.51 22.51a5 5 0 01-3.53 1.52zm-41.58 14.87a5 5 0 01-4.84-6.27l19.72-74.85a5 5 0 119.67 2.55L266 282.14a5 5 0 01-4.81 3.73z" class="cls-3"/>
|
||||
<path fill="#ffe400" d="M92.72 186.5l5 13.1A2.32 2.32 0 0099 201l13.1 5a2.32 2.32 0 010 4.34l-13.1 5a2.32 2.32 0 00-1.35 1.35l-5 13.1a2.32 2.32 0 01-4.34 0l-5-13.1a2.32 2.32 0 00-1.35-1.35l-13.1-5a2.32 2.32 0 010-4.34l13.1-5a2.32 2.32 0 001.35-1.35l5-13.1a2.32 2.32 0 014.41-.05zm386.36 79.39l5.38 14.22a2.52 2.52 0 001.47 1.47l14.22 5.42a2.52 2.52 0 010 4.72l-14.22 5.38a2.52 2.52 0 00-1.47 1.47l-5.38 14.22a2.52 2.52 0 01-4.72 0L469 298.52a2.52 2.52 0 00-1.47-1.47l-14.22-5.38a2.52 2.52 0 010-4.72l14.22-5.38a2.52 2.52 0 001.47-1.47l5.38-14.22a2.52 2.52 0 014.7.01zM258.14 18l5.67 15a2.66 2.66 0 001.55 1.55l15 5.67a2.66 2.66 0 010 5l-15 5.67a2.66 2.66 0 00-1.55 1.55l-5.67 15a2.66 2.66 0 01-5 0l-5.67-15a2.66 2.66 0 00-1.55-1.55l-15-5.67a2.66 2.66 0 010-5l15-5.67a2.66 2.66 0 001.55-1.55l5.67-15a2.66 2.66 0 015 0zm-49.69 344.87l5.67 15a2.66 2.66 0 001.55 1.55l15 5.67a2.66 2.66 0 010 5l-15 5.67a2.66 2.66 0 00-1.55 1.55l-5.67 15a2.66 2.66 0 01-5 0l-5.67-15a2.66 2.66 0 00-1.55-1.55l-15-5.67a2.66 2.66 0 010-5l15-5.67a2.66 2.66 0 001.55-1.55l5.67-15a2.66 2.66 0 015 0z"/>
|
||||
<rect width="90.16" height="59.5" x="480.14" y="173.05" class="cls-5" rx="11.44" ry="11.44" transform="rotate(30.42 525.27 202.83)"/>
|
||||
<path d="M505.53 242.61a4.27 4.27 0 01-5.83-3.43l-1.49-11.87-1.1-8.77a4.27 4.27 0 016.4-4.21l10.94 6.42 10.94 6.42a4.27 4.27 0 01-.56 7.64l-8.19 3.31z" class="cls-5"/>
|
||||
<circle cx="507.01" cy="194.63" r="5.78" class="cls-3" transform="rotate(-59.76 507.024 194.63)"/>
|
||||
<circle cx="524.1" cy="204.6" r="5.78" class="cls-3" transform="rotate(-59.76 524.112 204.598)"/>
|
||||
<circle cx="541.31" cy="214.63" r="5.78" class="cls-3" transform="rotate(-59.76 541.326 214.627)"/>
|
||||
<rect width="86.2" height="56.89" x="69.02" y="11.6" class="cls-6" rx="10.94" ry="10.94" transform="rotate(-24.07 112.12 40.037)"/>
|
||||
<path d="M132.16 77.47a4.08 4.08 0 01-5.9 2.64l-10.06-5.43-7.43-4a4.08 4.08 0 01.27-7.32l11.07-4.95 11.07-4.95a4.08 4.08 0 015.63 4.68l-2 8.21z" class="cls-6"/>
|
||||
<path d="M115.94 53.34l-17.08-5.28 2.41-7.83 9.67 2.99 7.81-21.18 7.68 2.84-10.49 28.46z" class="cls-3"/>
|
||||
</svg>
|
After Width: | Height: | Size: 4.8 KiB |
102
apps/portal/src/components/questions/ContributeQuestionCard.tsx
Normal file
102
apps/portal/src/components/questions/ContributeQuestionCard.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
56
apps/portal/src/components/questions/NavBar.tsx
Normal file
56
apps/portal/src/components/questions/NavBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
42
apps/portal/src/components/questions/QuestionSearchBar.tsx
Normal file
42
apps/portal/src/components/questions/QuestionSearchBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
25
apps/portal/src/components/questions/ui-patch/Checkbox.tsx
Normal file
25
apps/portal/src/components/questions/ui-patch/Checkbox.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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<Omit<FilterOptions, 'checked'>>;
|
||||
|
||||
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<Array<string>>([]);
|
||||
const [selectedQuestionTypes, setSelectedQuestionTypes] = useState<
|
||||
Array<string>
|
||||
>([]);
|
||||
const [selectedQuestionAges, setSelectedQuestionAges] = useState<
|
||||
Array<string>
|
||||
>([]);
|
||||
const [selectedLocations, setSelectedLocations] = useState<Array<string>>([]);
|
||||
|
||||
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 (
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<QuestionBankTitle />
|
||||
<main className="flex flex-1 flex-col items-stretch overflow-y-auto">
|
||||
<div className="pb-4">
|
||||
<NavBar></NavBar>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<section className="w-[300px] border-r px-4">
|
||||
<h2 className="text-xl font-semibold">Filter by</h2>
|
||||
<div className="divide-y divide-slate-200">
|
||||
<FilterSection
|
||||
label="Company"
|
||||
options={companyFilterOptions}
|
||||
searchPlaceholder="Add company filter"
|
||||
onOptionChange={(optionValue, checked) => {
|
||||
if (checked) {
|
||||
setSelectedCompanies((prev) => [...prev, optionValue]);
|
||||
} else {
|
||||
setSelectedCompanies((prev) =>
|
||||
prev.filter((company) => company !== optionValue),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FilterSection
|
||||
label="Question types"
|
||||
options={questionTypeFilterOptions}
|
||||
showAll={true}
|
||||
onOptionChange={(optionValue, checked) => {
|
||||
if (checked) {
|
||||
setSelectedQuestionTypes((prev) => [...prev, optionValue]);
|
||||
} else {
|
||||
setSelectedQuestionTypes((prev) =>
|
||||
prev.filter((questionType) => questionType !== optionValue),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FilterSection
|
||||
label="Question age"
|
||||
options={questionAgeFilterOptions}
|
||||
showAll={true}
|
||||
onOptionChange={(optionValue, checked) => {
|
||||
if (checked) {
|
||||
setSelectedQuestionAges((prev) => [...prev, optionValue]);
|
||||
} else {
|
||||
setSelectedQuestionAges((prev) =>
|
||||
prev.filter((questionAge) => questionAge !== optionValue),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FilterSection
|
||||
label="Location"
|
||||
options={locationFilterOptions}
|
||||
searchPlaceholder="Add location filter"
|
||||
onOptionChange={(optionValue, checked) => {
|
||||
if (checked) {
|
||||
setSelectedLocations((prev) => [...prev, optionValue]);
|
||||
} else {
|
||||
setSelectedLocations((prev) =>
|
||||
prev.filter((location) => location !== optionValue),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<div className="flex flex-1 justify-center">
|
||||
<div className="flex max-w-3xl flex-1 gap-x-4">
|
||||
<div className="flex flex-1 flex-col items-stretch justify-start gap-4">
|
||||
<ContributeQuestionCard
|
||||
onSubmit={(data) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(data);
|
||||
}}
|
||||
/>
|
||||
<QuestionSearchBar
|
||||
sortOptions={[
|
||||
{
|
||||
label: 'Most recent',
|
||||
value: 'most-recent',
|
||||
},
|
||||
{
|
||||
label: 'Most upvotes',
|
||||
value: 'most-upvotes',
|
||||
},
|
||||
]}
|
||||
sortValue="most-recent"
|
||||
/>
|
||||
<QuestionOverviewCard
|
||||
answerCount={0}
|
||||
content="Given an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums and an integer target, return indices of the two numbers such that they add up. Given an array of integers nums and"
|
||||
location="Menlo Park, CA"
|
||||
role="Senior Engineering Manager"
|
||||
similarCount={0}
|
||||
timestamp="Last month"
|
||||
upvoteCount={0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
@ -23,7 +23,7 @@ export default function Collapsible({ children, defaultOpen, label }: Props) {
|
||||
/>
|
||||
<span className="flex-1">{label}</span>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel className="pt-1 pb-2 text-sm text-gray-500">
|
||||
<Disclosure.Panel className="w-full pt-1 pb-2 text-sm text-gray-500">
|
||||
{children}
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
|
Reference in New Issue
Block a user