commit
fe52d9d55a
@ -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";
|
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>
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
@ -0,0 +1,99 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import type { ComponentMeta } from '@storybook/react';
|
||||||
|
import { CheckboxInput } from '@tih/ui';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
argTypes: {
|
||||||
|
defaultValue: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
component: CheckboxInput,
|
||||||
|
title: 'CheckboxInput',
|
||||||
|
} as ComponentMeta<typeof CheckboxInput>;
|
||||||
|
|
||||||
|
export function Basic({
|
||||||
|
defaultValue,
|
||||||
|
description,
|
||||||
|
disabled,
|
||||||
|
label,
|
||||||
|
}: Pick<
|
||||||
|
React.ComponentProps<typeof CheckboxInput>,
|
||||||
|
'defaultValue' | 'description' | 'disabled' | 'label'
|
||||||
|
>) {
|
||||||
|
return (
|
||||||
|
<CheckboxInput
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
description={description}
|
||||||
|
disabled={disabled}
|
||||||
|
label={label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Basic.args = {
|
||||||
|
description: 'I will be responsible for any mistakes',
|
||||||
|
disabled: false,
|
||||||
|
label: 'I have read the terms and conditions',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Controlled() {
|
||||||
|
const [value, setValue] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckboxInput
|
||||||
|
label="I have read the terms and conditions"
|
||||||
|
value={value}
|
||||||
|
onChange={(newValue: boolean) => {
|
||||||
|
setValue(newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Disabled() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<CheckboxInput
|
||||||
|
defaultValue={true}
|
||||||
|
label="I have read the terms and conditions"
|
||||||
|
/>
|
||||||
|
<CheckboxInput
|
||||||
|
defaultValue={false}
|
||||||
|
label="I have read the terms and conditions"
|
||||||
|
/>
|
||||||
|
<CheckboxInput
|
||||||
|
defaultValue={true}
|
||||||
|
disabled={true}
|
||||||
|
label="I have read the terms and conditions"
|
||||||
|
/>
|
||||||
|
<CheckboxInput
|
||||||
|
defaultValue={false}
|
||||||
|
disabled={true}
|
||||||
|
label="I have read the terms and conditions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ItemDescriptions() {
|
||||||
|
return (
|
||||||
|
<CheckboxInput
|
||||||
|
defaultValue={false}
|
||||||
|
description="I will be responsible for any mistakes"
|
||||||
|
label="I have read the terms and conditions"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,259 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import type { ComponentMeta } from '@storybook/react';
|
||||||
|
import type { CheckboxListOrientation } from '@tih/ui';
|
||||||
|
import { HorizontalDivider } from '@tih/ui';
|
||||||
|
import { CheckboxInput, CheckboxList } from '@tih/ui';
|
||||||
|
|
||||||
|
const CheckboxListOrientations: ReadonlyArray<CheckboxListOrientation> = [
|
||||||
|
'horizontal',
|
||||||
|
'vertical',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
argTypes: {
|
||||||
|
description: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
orientation: {
|
||||||
|
control: { type: 'select' },
|
||||||
|
options: CheckboxListOrientations,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
component: CheckboxList,
|
||||||
|
title: 'CheckboxList',
|
||||||
|
} as ComponentMeta<typeof CheckboxList>;
|
||||||
|
|
||||||
|
export function Basic({
|
||||||
|
description,
|
||||||
|
label,
|
||||||
|
orientation,
|
||||||
|
}: Pick<
|
||||||
|
React.ComponentProps<typeof CheckboxList>,
|
||||||
|
'description' | 'label' | 'orientation'
|
||||||
|
>) {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: 'Apple',
|
||||||
|
name: 'apple',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Banana',
|
||||||
|
name: 'banana',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Orange',
|
||||||
|
name: 'orange',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckboxList
|
||||||
|
description={description}
|
||||||
|
label={label}
|
||||||
|
orientation={orientation}>
|
||||||
|
{items.map(({ label: itemLabel, name, value: itemValue }) => (
|
||||||
|
<CheckboxInput
|
||||||
|
key={itemLabel}
|
||||||
|
defaultValue={itemValue}
|
||||||
|
label={itemLabel}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CheckboxList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Basic.args = {
|
||||||
|
description: 'Selected fruits will be served after dinner',
|
||||||
|
label: 'Select your favorite fruits',
|
||||||
|
orientation: 'vertical',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Controlled() {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: 'Apple',
|
||||||
|
value: 'apple',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Banana',
|
||||||
|
value: 'banana',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Orange',
|
||||||
|
value: 'orange',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const [values, setValues] = useState(new Set(['apple']));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckboxList
|
||||||
|
description="You will be served it during dinner"
|
||||||
|
label="Choose a fruit">
|
||||||
|
{items.map(({ label: itemLabel, value: itemValue }) => (
|
||||||
|
<CheckboxInput
|
||||||
|
key={itemLabel}
|
||||||
|
label={itemLabel}
|
||||||
|
value={values.has(itemValue)}
|
||||||
|
onChange={(newValue: boolean) => {
|
||||||
|
if (newValue) {
|
||||||
|
setValues(new Set([...Array.from(values), itemValue]));
|
||||||
|
} else {
|
||||||
|
setValues(
|
||||||
|
new Set(Array.from(values).filter((v) => v !== itemValue)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CheckboxList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 [values, setValues] = useState(new Set(['apple', 'banana']));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<CheckboxList label="Choose a fruit (some fruits disabled)">
|
||||||
|
{items.map(({ disabled, label: itemLabel, value: itemValue }) => (
|
||||||
|
<CheckboxInput
|
||||||
|
key={itemLabel}
|
||||||
|
disabled={disabled}
|
||||||
|
label={itemLabel}
|
||||||
|
value={values.has(itemValue)}
|
||||||
|
onChange={(newValue: boolean) => {
|
||||||
|
if (newValue) {
|
||||||
|
setValues(new Set([...Array.from(values), itemValue]));
|
||||||
|
} else {
|
||||||
|
setValues(
|
||||||
|
new Set(Array.from(values).filter((v) => v !== itemValue)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CheckboxList>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 [values, setValues] = useState(new Set(['apple', 'banana']));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<CheckboxList label="Choose a fruit (some fruits disabled)">
|
||||||
|
{items.map(({ description, label: itemLabel, value: itemValue }) => (
|
||||||
|
<CheckboxInput
|
||||||
|
key={itemLabel}
|
||||||
|
description={description}
|
||||||
|
label={itemLabel}
|
||||||
|
value={values.has(itemValue)}
|
||||||
|
onChange={(newValue: boolean) => {
|
||||||
|
if (newValue) {
|
||||||
|
setValues(new Set([...Array.from(values), itemValue]));
|
||||||
|
} else {
|
||||||
|
setValues(
|
||||||
|
new Set(Array.from(values).filter((v) => v !== itemValue)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CheckboxList>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Orientation() {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: 'Apple',
|
||||||
|
name: 'apple',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Banana',
|
||||||
|
name: 'banana',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Orange',
|
||||||
|
name: 'orange',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<CheckboxList label="Choose a fruit" orientation="vertical">
|
||||||
|
{items.map(({ label: itemLabel, name, value: itemValue }) => (
|
||||||
|
<CheckboxInput
|
||||||
|
key={itemLabel}
|
||||||
|
defaultValue={itemValue}
|
||||||
|
label={itemLabel}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CheckboxList>
|
||||||
|
<HorizontalDivider />
|
||||||
|
<CheckboxList label="Choose a fruit" orientation="horizontal">
|
||||||
|
{items.map(({ label: itemLabel, name, value: itemValue }) => (
|
||||||
|
<CheckboxInput
|
||||||
|
key={itemLabel}
|
||||||
|
defaultValue={itemValue}
|
||||||
|
label={itemLabel}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CheckboxList>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,256 @@
|
|||||||
|
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<RadioListOrientation> = [
|
||||||
|
'horizontal',
|
||||||
|
'vertical',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
argTypes: {
|
||||||
|
description: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
orientation: {
|
||||||
|
control: { type: 'select' },
|
||||||
|
options: RadioListOrientations,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
component: RadioList,
|
||||||
|
title: 'RadioList',
|
||||||
|
} as ComponentMeta<typeof RadioList>;
|
||||||
|
|
||||||
|
export function Basic({
|
||||||
|
description,
|
||||||
|
label,
|
||||||
|
orientation,
|
||||||
|
}: Pick<
|
||||||
|
React.ComponentProps<typeof RadioList>,
|
||||||
|
'description' | 'label' | 'orientation'
|
||||||
|
>) {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: 'Apple',
|
||||||
|
value: 'apple',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Banana',
|
||||||
|
value: 'banana',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Orange',
|
||||||
|
value: 'orange',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioList
|
||||||
|
defaultValue="apple"
|
||||||
|
description={description}
|
||||||
|
label={label}
|
||||||
|
name="fruit"
|
||||||
|
orientation={orientation}>
|
||||||
|
{items.map(({ label: itemLabel, value }) => (
|
||||||
|
<RadioList.Item key={itemLabel} label={itemLabel} value={value} />
|
||||||
|
))}
|
||||||
|
</RadioList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Basic.args = {
|
||||||
|
description: 'Your favorite fruit',
|
||||||
|
label: 'Choose a fruit',
|
||||||
|
orientation: 'vertical',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Controlled() {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: 'Apple',
|
||||||
|
value: 'apple',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Banana',
|
||||||
|
value: 'banana',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Orange',
|
||||||
|
value: 'orange',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const [value, setValue] = useState('apple');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioList
|
||||||
|
description="You will be served it during dinner"
|
||||||
|
label="Choose a fruit"
|
||||||
|
value={value}
|
||||||
|
onChange={(newValue: string) => setValue(newValue)}>
|
||||||
|
{items.map(({ label: itemLabel, value: itemValue }) => (
|
||||||
|
<RadioList.Item key={itemLabel} label={itemLabel} value={itemValue} />
|
||||||
|
))}
|
||||||
|
</RadioList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Required() {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: 'Apple',
|
||||||
|
value: 'apple',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Banana',
|
||||||
|
value: 'banana',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Orange',
|
||||||
|
value: 'orange',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const [value, setValue] = useState('apple');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioList
|
||||||
|
description="You will be served it during dinner"
|
||||||
|
label="Choose a fruit"
|
||||||
|
required={true}
|
||||||
|
value={value}
|
||||||
|
onChange={(newValue: string) => setValue(newValue)}>
|
||||||
|
{items.map(({ label: itemLabel, value: itemValue }) => (
|
||||||
|
<RadioList.Item key={itemLabel} label={itemLabel} value={itemValue} />
|
||||||
|
))}
|
||||||
|
</RadioList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<RadioList
|
||||||
|
defaultValue="apple"
|
||||||
|
label="Choose a fruit (some fruits disabled)"
|
||||||
|
name="fruit-5">
|
||||||
|
{items.map(
|
||||||
|
({ description, label: itemLabel, value: itemValue, disabled }) => (
|
||||||
|
<RadioList.Item
|
||||||
|
key={itemLabel}
|
||||||
|
description={description}
|
||||||
|
disabled={disabled}
|
||||||
|
label={itemLabel}
|
||||||
|
value={itemValue}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</RadioList>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<RadioList
|
||||||
|
label="Choose a fruit"
|
||||||
|
value={value}
|
||||||
|
onChange={(newValue: string) => setValue(newValue)}>
|
||||||
|
{items.map(({ description, label: itemLabel, value: itemValue }) => (
|
||||||
|
<RadioList.Item
|
||||||
|
key={itemValue}
|
||||||
|
description={description}
|
||||||
|
label={itemLabel}
|
||||||
|
value={itemValue}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RadioList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Orientation() {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: 'Apple',
|
||||||
|
value: 'apple',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Banana',
|
||||||
|
value: 'banana',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Orange',
|
||||||
|
value: 'orange',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<RadioList
|
||||||
|
defaultValue="apple"
|
||||||
|
label="Choose a fruit"
|
||||||
|
name="fruit-2"
|
||||||
|
orientation="vertical">
|
||||||
|
{items.map(({ label: itemLabel, value: itemValue }) => (
|
||||||
|
<RadioList.Item key={itemLabel} label={itemLabel} value={itemValue} />
|
||||||
|
))}
|
||||||
|
</RadioList>
|
||||||
|
<HorizontalDivider />
|
||||||
|
<RadioList
|
||||||
|
defaultValue="banana"
|
||||||
|
label="Choose a fruit"
|
||||||
|
name="fruit-3"
|
||||||
|
orientation="horizontal">
|
||||||
|
{items.map(({ label: itemLabel, value: itemValue }) => (
|
||||||
|
<RadioList.Item key={itemLabel} label={itemLabel} value={itemValue} />
|
||||||
|
))}
|
||||||
|
</RadioList>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import type { ChangeEvent } from 'react';
|
||||||
|
import { useId } from 'react';
|
||||||
|
|
||||||
|
type Props = Readonly<{
|
||||||
|
defaultValue?: boolean;
|
||||||
|
description?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
label: string;
|
||||||
|
name?: string;
|
||||||
|
onChange?: (
|
||||||
|
value: boolean,
|
||||||
|
event: ChangeEvent<HTMLInputElement>,
|
||||||
|
) => undefined | void;
|
||||||
|
value?: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function CheckboxInput({
|
||||||
|
defaultValue,
|
||||||
|
description,
|
||||||
|
disabled = false,
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: Props) {
|
||||||
|
const id = useId();
|
||||||
|
const descriptionId = useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'relative flex',
|
||||||
|
// Vertically center only when there's no description.
|
||||||
|
description == null && 'items-center',
|
||||||
|
)}>
|
||||||
|
<div className="flex h-5 items-center">
|
||||||
|
<input
|
||||||
|
aria-describedby={description != null ? descriptionId : undefined}
|
||||||
|
checked={value}
|
||||||
|
className={clsx(
|
||||||
|
'h-4 w-4 rounded border-slate-300',
|
||||||
|
disabled
|
||||||
|
? 'bg-slate-100 text-slate-400'
|
||||||
|
: 'text-primary-600 focus:ring-primary-500',
|
||||||
|
)}
|
||||||
|
defaultChecked={defaultValue}
|
||||||
|
disabled={disabled}
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
type="checkbox"
|
||||||
|
onChange={
|
||||||
|
onChange != null
|
||||||
|
? (event) => {
|
||||||
|
onChange?.(event.target.checked, event);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 text-sm">
|
||||||
|
<label
|
||||||
|
className={clsx(
|
||||||
|
'block font-medium',
|
||||||
|
disabled ? 'text-slate-400' : 'text-slate-700',
|
||||||
|
)}
|
||||||
|
htmlFor={id}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{description && (
|
||||||
|
<p
|
||||||
|
className={clsx(
|
||||||
|
'text-xs',
|
||||||
|
disabled ? 'text-slate-400' : 'text-slate-500',
|
||||||
|
)}
|
||||||
|
id={descriptionId}>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import { useId } from 'react';
|
||||||
|
|
||||||
|
import type CheckboxInput from '../CheckboxInput/CheckboxInput';
|
||||||
|
|
||||||
|
export type CheckboxListOrientation = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
|
type Props = Readonly<{
|
||||||
|
children: ReadonlyArray<React.ReactElement<typeof CheckboxInput>>;
|
||||||
|
description?: string;
|
||||||
|
isLabelHidden?: boolean;
|
||||||
|
label: string;
|
||||||
|
orientation?: CheckboxListOrientation;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function CheckboxList({
|
||||||
|
children,
|
||||||
|
description,
|
||||||
|
isLabelHidden,
|
||||||
|
label,
|
||||||
|
orientation = 'vertical',
|
||||||
|
}: Props) {
|
||||||
|
const labelId = useId();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={clsx(isLabelHidden ? 'sr-only' : 'mb-2')}>
|
||||||
|
<label className="text-sm font-medium text-gray-900" id={labelId}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{description && (
|
||||||
|
<p className="text-xs leading-5 text-gray-500">{description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-labelledby={labelId}
|
||||||
|
className={clsx(
|
||||||
|
'space-y-2',
|
||||||
|
orientation === 'horizontal' &&
|
||||||
|
'sm:flex sm:items-center sm:space-y-0 sm:space-x-10',
|
||||||
|
)}
|
||||||
|
role="group">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import type { ChangeEvent } from 'react';
|
||||||
|
import { useId } from 'react';
|
||||||
|
|
||||||
|
import { RadioListContext } from './RadioListContext';
|
||||||
|
import RadioListItem from './RadioListItem';
|
||||||
|
|
||||||
|
export type RadioListOrientation = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
|
type Props<T> = Readonly<{
|
||||||
|
children: ReadonlyArray<React.ReactElement<typeof RadioListItem>>;
|
||||||
|
defaultValue?: T;
|
||||||
|
description?: string;
|
||||||
|
isLabelHidden?: boolean;
|
||||||
|
label: string;
|
||||||
|
name?: string;
|
||||||
|
onChange?: (value: T, event: ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
orientation?: RadioListOrientation;
|
||||||
|
required?: boolean;
|
||||||
|
value?: T;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
RadioList.Item = RadioListItem;
|
||||||
|
|
||||||
|
export default function RadioList<T>({
|
||||||
|
children,
|
||||||
|
defaultValue,
|
||||||
|
description,
|
||||||
|
isLabelHidden,
|
||||||
|
name,
|
||||||
|
orientation = 'vertical',
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: Props<T>) {
|
||||||
|
const labelId = useId();
|
||||||
|
return (
|
||||||
|
<RadioListContext.Provider
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore TODO: Figure out how to type the onChange.
|
||||||
|
value={{ defaultValue, name, onChange, value }}>
|
||||||
|
<div>
|
||||||
|
<div className={clsx(isLabelHidden ? 'sr-only' : 'mb-2')}>
|
||||||
|
<label className="text-sm font-medium text-gray-900" id={labelId}>
|
||||||
|
{label}
|
||||||
|
{required && (
|
||||||
|
<span aria-hidden="true" className="text-danger-500">
|
||||||
|
{' '}
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
{description && (
|
||||||
|
<p className="text-xs leading-5 text-gray-500">{description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-labelledby={labelId}
|
||||||
|
aria-required={required != null ? required : undefined}
|
||||||
|
className={clsx(
|
||||||
|
'space-y-2',
|
||||||
|
orientation === 'horizontal' &&
|
||||||
|
'sm:flex sm:items-center sm:space-y-0 sm:space-x-10',
|
||||||
|
)}
|
||||||
|
role="radiogroup">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RadioListContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import type { ChangeEvent } from 'react';
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
type RadioListContextValue<T = unknown> = {
|
||||||
|
defaultValue?: T;
|
||||||
|
name?: string;
|
||||||
|
onChange?: (
|
||||||
|
value: T,
|
||||||
|
event: ChangeEvent<HTMLInputElement>,
|
||||||
|
) => undefined | void;
|
||||||
|
value?: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RadioListContext =
|
||||||
|
createContext<RadioListContextValue<unknown> | null>(null);
|
||||||
|
export function useRadioListContext<T>(): RadioListContextValue<T> | null {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore TODO: Figure out how to type useContext with generics.
|
||||||
|
return useContext<T>(RadioListContext);
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import { useId } from 'react';
|
||||||
|
|
||||||
|
import { useRadioListContext } from './RadioListContext';
|
||||||
|
|
||||||
|
type Props<T> = Readonly<{
|
||||||
|
description?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
label: string;
|
||||||
|
value: T;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function RadioListItem<T>({
|
||||||
|
description,
|
||||||
|
disabled = false,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}: Props<T>) {
|
||||||
|
const id = useId();
|
||||||
|
const descriptionId = useId();
|
||||||
|
const context = useRadioListContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'relative flex',
|
||||||
|
// Vertically center only when there's no description.
|
||||||
|
description == null && 'items-center',
|
||||||
|
)}>
|
||||||
|
<div className="flex h-5 items-center">
|
||||||
|
<input
|
||||||
|
aria-describedby={description != null ? descriptionId : undefined}
|
||||||
|
checked={
|
||||||
|
context?.value != null ? value === context?.value : undefined
|
||||||
|
}
|
||||||
|
className={clsx(
|
||||||
|
'text-primary-600 focus:ring-primary-500 h-4 w-4 border-slate-300',
|
||||||
|
disabled && 'bg-slate-100',
|
||||||
|
)}
|
||||||
|
defaultChecked={
|
||||||
|
context?.defaultValue != null
|
||||||
|
? value === context?.defaultValue
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
id={id}
|
||||||
|
name={context?.name}
|
||||||
|
type="radio"
|
||||||
|
onChange={
|
||||||
|
context?.onChange != null
|
||||||
|
? (event) => {
|
||||||
|
context?.onChange?.(value, event);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 text-sm">
|
||||||
|
<label
|
||||||
|
className={clsx(
|
||||||
|
'block font-medium',
|
||||||
|
disabled ? 'text-slate-400' : 'text-slate-700',
|
||||||
|
)}
|
||||||
|
htmlFor={id}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{description && (
|
||||||
|
<p
|
||||||
|
className={clsx(
|
||||||
|
'text-xs',
|
||||||
|
disabled ? 'text-slate-400' : 'text-slate-500',
|
||||||
|
)}
|
||||||
|
id={descriptionId}>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in new issue