build: merge with latest changes update dockerfile to work with pnpm
This commit is contained in:
parent
45fe4b89db
commit
7ef9ea07f0
161 changed files with 67 additions and 48 deletions
59
apps/app/ui/Breadcrumbs/index.tsx
Normal file
59
apps/app/ui/Breadcrumbs/index.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { ArrowLeftIcon, HomeIcon } from "@heroicons/react/24/outline";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
type BreadcrumbsProps = {
|
||||
children: any;
|
||||
};
|
||||
|
||||
const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ children }: BreadcrumbsProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-3 ml-1">
|
||||
<div
|
||||
className="bg-indigo-50 hover:bg-indigo-100 duration-300 px-3 py-1 rounded-tl-lg rounded-tr-md rounded-br-lg rounded-bl-md skew-x-[-20deg] text-sm text-center grid place-items-center cursor-pointer"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<p className="skew-x-[20deg]">
|
||||
<ArrowLeftIcon className="h-3 w-3" />
|
||||
</p>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type BreadcrumbItemProps = {
|
||||
title: string;
|
||||
link?: string;
|
||||
icon?: any;
|
||||
};
|
||||
|
||||
const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) => {
|
||||
return (
|
||||
<>
|
||||
{link ? (
|
||||
<Link href={link}>
|
||||
<a className="bg-indigo-50 hover:bg-indigo-100 duration-300 px-4 py-1 rounded-tl-lg rounded-tr-md rounded-br-lg rounded-bl-md skew-x-[-20deg] text-sm text-center">
|
||||
<p className={`skew-x-[20deg] ${icon ? "flex items-center gap-2" : ""}`}>
|
||||
{icon ?? null}
|
||||
{title}
|
||||
</p>
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="bg-indigo-50 px-4 py-1 rounded-tl-lg rounded-tr-md rounded-br-lg rounded-bl-md skew-x-[-20deg] text-sm text-center">
|
||||
<p className={`skew-x-[20deg] ${icon ? "flex items-center gap-2" : ""}`}>
|
||||
{icon}
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { Breadcrumbs, BreadcrumbItem };
|
||||
64
apps/app/ui/Button/index.tsx
Normal file
64
apps/app/ui/Button/index.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
onClick?: () => void;
|
||||
children: React.ReactNode;
|
||||
type?: "button" | "submit" | "reset";
|
||||
className?: string;
|
||||
theme?: "primary" | "secondary" | "danger";
|
||||
size?: "sm" | "rg" | "md" | "lg";
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
// commons
|
||||
import { classNames } from "constants/common";
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, Props>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
onClick,
|
||||
type = "button",
|
||||
size = "sm",
|
||||
className,
|
||||
theme = "primary",
|
||||
disabled = false,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
className={classNames(
|
||||
"inline-flex items-center rounded justify-center font-medium",
|
||||
theme === "primary"
|
||||
? `${
|
||||
disabled ? "opacity-70" : "bg-theme hover:bg-indigo-700"
|
||||
} text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 border border-transparent`
|
||||
: theme === "secondary"
|
||||
? "border border-gray-300 bg-white"
|
||||
: `${
|
||||
disabled ? "opacity-70" : "bg-red-600 hover:bg-red-700"
|
||||
} text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 border border-transparent`,
|
||||
size === "sm"
|
||||
? "p-2 text-xs"
|
||||
: size === "md"
|
||||
? "px-3 py-2 text-base"
|
||||
: size === "lg"
|
||||
? "px-4 py-2 text-base"
|
||||
: "px-2.5 py-2 text-sm",
|
||||
className || ""
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = "Button";
|
||||
|
||||
export default Button;
|
||||
163
apps/app/ui/CustomListbox/index.tsx
Normal file
163
apps/app/ui/CustomListbox/index.tsx
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import React from "react";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { CheckIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
import { Props } from "./types";
|
||||
|
||||
const CustomListbox: React.FC<Props> = ({
|
||||
title,
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
multiple,
|
||||
icon,
|
||||
width,
|
||||
footerOption,
|
||||
optionsFontsize,
|
||||
className,
|
||||
label,
|
||||
}) => {
|
||||
return (
|
||||
<Listbox value={value} onChange={onChange} multiple={multiple}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{label && (
|
||||
<Listbox.Label>
|
||||
<div className="text-gray-500 mb-2">{label}</div>
|
||||
</Listbox.Label>
|
||||
)}
|
||||
<div className="relative">
|
||||
<Listbox.Button
|
||||
className={`flex items-center gap-1 hover:bg-gray-100 relative border rounded-md shadow-sm cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm duration-300 ${
|
||||
width === "sm"
|
||||
? "w-32"
|
||||
: width === "md"
|
||||
? "w-48"
|
||||
: width === "lg"
|
||||
? "w-64"
|
||||
: width === "xl"
|
||||
? "w-80"
|
||||
: width === "2xl"
|
||||
? "w-96"
|
||||
: width === "w-full"
|
||||
? "w-full"
|
||||
: ""
|
||||
}
|
||||
${className || "px-2 py-1"}`}
|
||||
>
|
||||
{icon ?? null}
|
||||
<span className="block truncate">
|
||||
{Array.isArray(value)
|
||||
? value.map((v) => options?.find((o) => o.value === v)?.display).join(", ") ||
|
||||
`${title}`
|
||||
: options?.find((o) => o.value === value)?.display || `${title}`}
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options
|
||||
className={`absolute mt-1 bg-white shadow-lg max-h-32 overflow-auto ${
|
||||
width === "sm"
|
||||
? "w-32"
|
||||
: width === "md"
|
||||
? "w-48"
|
||||
: width === "lg"
|
||||
? "w-64"
|
||||
: width === "xl"
|
||||
? "w-80"
|
||||
: width === "2xl"
|
||||
? "w-96"
|
||||
: width === "w-full"
|
||||
? "w-full"
|
||||
: ""
|
||||
} ${
|
||||
optionsFontsize === "sm"
|
||||
? "text-xs"
|
||||
: optionsFontsize === "md"
|
||||
? "text-base"
|
||||
: optionsFontsize === "lg"
|
||||
? "text-lg"
|
||||
: optionsFontsize === "xl"
|
||||
? "text-xl"
|
||||
: optionsFontsize === "2xl"
|
||||
? "text-2xl"
|
||||
: ""
|
||||
} ${
|
||||
className || ""
|
||||
} rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none z-10`}
|
||||
>
|
||||
<div className="p-1">
|
||||
{options ? (
|
||||
options.length > 0 ? (
|
||||
options.map((option) => (
|
||||
<Listbox.Option
|
||||
key={option.value}
|
||||
className={({ active }) =>
|
||||
`${
|
||||
active ? "text-white bg-theme" : "text-gray-900"
|
||||
} cursor-pointer select-none relative p-2 rounded-md`
|
||||
}
|
||||
value={option.value}
|
||||
>
|
||||
{({ selected, active }) => (
|
||||
<>
|
||||
<span
|
||||
className={`${
|
||||
selected ||
|
||||
(Array.isArray(value)
|
||||
? value.includes(option.value)
|
||||
: value === option.value)
|
||||
? "font-semibold"
|
||||
: "font-normal"
|
||||
} block truncate`}
|
||||
>
|
||||
{option.display}
|
||||
</span>
|
||||
|
||||
{selected ||
|
||||
(Array.isArray(value)
|
||||
? value.includes(option.value)
|
||||
: value === option.value) ? (
|
||||
<span
|
||||
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
||||
active ||
|
||||
(Array.isArray(value)
|
||||
? value.includes(option.value)
|
||||
: value === option.value)
|
||||
? "text-white"
|
||||
: "text-indigo-600"
|
||||
}`}
|
||||
>
|
||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 text-center">No options</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 text-center">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
{footerOption ?? null}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomListbox;
|
||||
13
apps/app/ui/CustomListbox/types.d.ts
vendored
Normal file
13
apps/app/ui/CustomListbox/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export type Props = {
|
||||
title: string;
|
||||
label?: string;
|
||||
options?: Array<{ display: string; value: any }>;
|
||||
icon?: JSX.Element;
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
multiple?: boolean;
|
||||
width?: "sm" | "md" | "lg" | "xl" | "2xl" | "w-full";
|
||||
optionsFontsize?: "sm" | "md" | "lg" | "xl" | "2xl";
|
||||
className?: string;
|
||||
footerOption?: JSX.Element;
|
||||
};
|
||||
102
apps/app/ui/EmptySpace/index.tsx
Normal file
102
apps/app/ui/EmptySpace/index.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// next
|
||||
import Link from "next/link";
|
||||
// react
|
||||
import React from "react";
|
||||
// icons
|
||||
import { ChevronRightIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type EmptySpaceProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
children: any;
|
||||
Icon?: (
|
||||
props: React.SVGProps<SVGSVGElement> & {
|
||||
title?: string | undefined;
|
||||
titleId?: string | undefined;
|
||||
}
|
||||
) => JSX.Element;
|
||||
link?: { text: string; href: string };
|
||||
};
|
||||
|
||||
const EmptySpace: React.FC<EmptySpaceProps> = ({ title, description, children, Icon, link }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-lg">
|
||||
{Icon ? (
|
||||
<div className="mb-4">
|
||||
<Icon className="h-14 w-14 text-gray-400" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<h2 className="text-lg font-medium text-gray-900">{title}</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">{description}</p>
|
||||
<ul role="list" className="mt-6 divide-y divide-gray-200 border-t border-b border-gray-200">
|
||||
{children}
|
||||
</ul>
|
||||
{link ? (
|
||||
<div className="mt-6 flex">
|
||||
<Link href={link.href}>
|
||||
<a className="text-sm font-medium text-indigo-600 hover:text-indigo-500">
|
||||
{link.text}
|
||||
<span aria-hidden="true"> →</span>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type EmptySpaceItemProps = {
|
||||
title: string;
|
||||
description?: React.ReactNode | string;
|
||||
bgColor?: string;
|
||||
Icon: (
|
||||
props: React.SVGProps<SVGSVGElement> & {
|
||||
title?: string | undefined;
|
||||
titleId?: string | undefined;
|
||||
}
|
||||
) => JSX.Element;
|
||||
action: () => void;
|
||||
};
|
||||
|
||||
const EmptySpaceItem: React.FC<EmptySpaceItemProps> = ({
|
||||
title,
|
||||
description,
|
||||
bgColor = "blue",
|
||||
Icon,
|
||||
action,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<li className="cursor-pointer" onClick={action}>
|
||||
<div
|
||||
className={`group relative flex ${
|
||||
description ? "items-start" : "items-center"
|
||||
} space-x-3 py-4`}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<span
|
||||
className={`inline-flex items-center justify-center h-10 w-10 rounded-lg bg-theme`}
|
||||
>
|
||||
<Icon className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-gray-900">{title}</div>
|
||||
{description ? <p className="text-sm text-gray-500">{description}</p> : null}
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<ChevronRightIcon
|
||||
className="h-5 w-5 text-gray-400 group-hover:text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { EmptySpace, EmptySpaceItem };
|
||||
40
apps/app/ui/HeaderButton/index.tsx
Normal file
40
apps/app/ui/HeaderButton/index.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
type HeaderButtonProps = {
|
||||
Icon: (
|
||||
props: React.SVGProps<SVGSVGElement> & {
|
||||
title?: string | undefined;
|
||||
titleId?: string | undefined;
|
||||
}
|
||||
) => JSX.Element;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
position?: "normal" | "reverse";
|
||||
};
|
||||
|
||||
const HeaderButton = ({
|
||||
Icon,
|
||||
label,
|
||||
disabled = false,
|
||||
onClick,
|
||||
className = "",
|
||||
position = "normal",
|
||||
}: HeaderButtonProps) => {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={`bg-theme text-white border border-indigo-600 text-xs flex items-center gap-x-1 p-2 rounded-md font-medium whitespace-nowrap outline-none ${
|
||||
position === "reverse" && "flex-row-reverse"
|
||||
} ${className}`}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderButton;
|
||||
54
apps/app/ui/Input/index.tsx
Normal file
54
apps/app/ui/Input/index.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import React from "react";
|
||||
// common
|
||||
import { classNames } from "constants/common";
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
|
||||
const Input: React.FC<Props> = ({
|
||||
label,
|
||||
value,
|
||||
name,
|
||||
register,
|
||||
validations,
|
||||
error,
|
||||
mode = "primary",
|
||||
onChange,
|
||||
className,
|
||||
type,
|
||||
id,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{label && (
|
||||
<label htmlFor={id} className="text-gray-500 mb-2">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
type={type}
|
||||
id={id}
|
||||
value={value}
|
||||
{...(register && register(name, validations))}
|
||||
onChange={(e) => {
|
||||
register && register(name).onChange(e);
|
||||
onChange && onChange(e);
|
||||
}}
|
||||
className={classNames(
|
||||
"mt-1 block w-full px-3 py-2 text-base focus:outline-none sm:text-sm rounded-md bg-transparent",
|
||||
mode === "primary" ? "border border-gray-300 rounded-md" : "",
|
||||
mode === "transparent"
|
||||
? "bg-transparent border-none transition-all ring-0 focus:ring-1 focus:ring-indigo-500 rounded"
|
||||
: "",
|
||||
error ? "border-red-500" : "",
|
||||
error && mode === "primary" ? "bg-red-100" : "",
|
||||
className ?? ""
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
{error?.message && <div className="text-red-500 text-sm">{error.message}</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Input;
|
||||
13
apps/app/ui/Input/types.d.ts
vendored
Normal file
13
apps/app/ui/Input/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import React from "react";
|
||||
import type { UseFormRegister, RegisterOptions, FieldError } from "react-hook-form";
|
||||
|
||||
export interface Props extends React.ComponentPropsWithoutRef<"input"> {
|
||||
label?: string;
|
||||
name: string;
|
||||
value?: string | number | readonly string[];
|
||||
mode?: "primary" | "transparent" | "secondary" | "disabled";
|
||||
register?: UseFormRegister<any>;
|
||||
validations?: RegisterOptions;
|
||||
error?: FieldError;
|
||||
className?: string;
|
||||
}
|
||||
118
apps/app/ui/Modal/index.tsx
Normal file
118
apps/app/ui/Modal/index.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { Fragment, ReactNode } from "react";
|
||||
// Headless ui imports
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// Design components
|
||||
import Button from "ui/Button";
|
||||
// Icons
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type ModalProps = {
|
||||
isModal: boolean;
|
||||
setModal: Function;
|
||||
size?: "xs" | "rg" | "lg" | "xl";
|
||||
position?: "top" | "center" | "bottom";
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
buttons?: ReactNode;
|
||||
onClose?: Function;
|
||||
closeButton?: string;
|
||||
continueButton?: string;
|
||||
};
|
||||
|
||||
const Modal = (props: ModalProps) => {
|
||||
const closeModal = () => {
|
||||
props.setModal(false);
|
||||
props.onClose ? props.onClose() : () => {};
|
||||
};
|
||||
|
||||
const width: string =
|
||||
props.size === "xs"
|
||||
? "w-4/12"
|
||||
: props.size === "rg"
|
||||
? "w-6/12"
|
||||
: props.size === "lg"
|
||||
? "w-9/12"
|
||||
: props.size === "xl"
|
||||
? "w-full"
|
||||
: "w-auto";
|
||||
|
||||
const position: string =
|
||||
props.position === "top"
|
||||
? "content-start justify-items-center"
|
||||
: props.position === "center"
|
||||
? "place-items-center"
|
||||
: props.position === "bottom"
|
||||
? "content-end justify-items-center"
|
||||
: "place-items-center";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition appear show={props.isModal} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<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-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0">
|
||||
<div className={`grid h-full ${position} p-4 text-center`}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel
|
||||
className={`transform rounded-2xl ${width} bg-white p-8 text-left max-h-full shadow-xl transition-all`}
|
||||
>
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900 relative"
|
||||
>
|
||||
<div
|
||||
className="absolute top-[-1rem] right-[-1rem] cursor-pointer"
|
||||
onClick={closeModal}
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<div>{props.title}</div>
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">{props.children}</div>
|
||||
<div className="mt-4">
|
||||
<div className={`flex gap-2 justify-end`}>
|
||||
<Button theme="secondary" onClick={closeModal}>
|
||||
{props.closeButton}
|
||||
</Button>
|
||||
<Button onClick={closeModal}>
|
||||
{props.continueButton}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.defaultProps = {
|
||||
size: "rg",
|
||||
position: "center",
|
||||
closeButton: "Close",
|
||||
continueButton: "Continue",
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
151
apps/app/ui/SearchListbox/index.tsx
Normal file
151
apps/app/ui/SearchListbox/index.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import React, { useState } from "react";
|
||||
// headless ui
|
||||
import { Transition, Combobox } from "@headlessui/react";
|
||||
// common
|
||||
import { classNames } from "constants/common";
|
||||
// types
|
||||
import type { Props } from "./types";
|
||||
|
||||
const SearchListbox: React.FC<Props> = ({
|
||||
title,
|
||||
options,
|
||||
onChange,
|
||||
value,
|
||||
multiple: canSelectMultiple,
|
||||
icon,
|
||||
width = "sm",
|
||||
optionsFontsize,
|
||||
buttonClassName,
|
||||
optionsClassName,
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? options
|
||||
: options?.filter((option) => option.display.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const props: any = {
|
||||
value,
|
||||
onChange,
|
||||
};
|
||||
|
||||
if (canSelectMultiple) {
|
||||
props.value = props.value ?? [];
|
||||
props.onChange = (value: string[]) => {
|
||||
onChange(value);
|
||||
};
|
||||
props.multiple = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<Combobox as="div" {...props} className="flex-shrink-0">
|
||||
{({ open }: any) => (
|
||||
<>
|
||||
<Combobox.Label className="sr-only"> {title} </Combobox.Label>
|
||||
<div className="relative">
|
||||
<Combobox.Button
|
||||
className={`flex items-center gap-1 hover:bg-gray-100 relative border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm duration-300 ${
|
||||
width === "sm"
|
||||
? "w-32"
|
||||
: width === "md"
|
||||
? "w-48"
|
||||
: width === "lg"
|
||||
? "w-64"
|
||||
: width === "xl"
|
||||
? "w-80"
|
||||
: width === "2xl"
|
||||
? "w-96"
|
||||
: ""
|
||||
} ${buttonClassName || ""}`}
|
||||
>
|
||||
{icon ?? null}
|
||||
<span
|
||||
className={classNames(
|
||||
value === null || value === undefined ? "" : "text-gray-900",
|
||||
"hidden truncate sm:ml-2 sm:block"
|
||||
)}
|
||||
>
|
||||
{Array.isArray(value)
|
||||
? value
|
||||
.map((v) => options?.find((option) => option.value === v)?.display)
|
||||
.join(", ") || title
|
||||
: options?.find((option) => option.value === value)?.display || title}
|
||||
</span>
|
||||
</Combobox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Combobox.Options
|
||||
className={`absolute mt-1 bg-white shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none max-h-32 overflow-auto z-10 ${
|
||||
width === "xs"
|
||||
? "w-20"
|
||||
: width === "sm"
|
||||
? "w-32"
|
||||
: width === "md"
|
||||
? "w-48"
|
||||
: width === "lg"
|
||||
? "w-64"
|
||||
: width === "xl"
|
||||
? "w-80"
|
||||
: width === "2xl"
|
||||
? "w-96"
|
||||
: ""
|
||||
}} ${
|
||||
optionsFontsize === "sm"
|
||||
? "text-xs"
|
||||
: optionsFontsize === "md"
|
||||
? "text-base"
|
||||
: optionsFontsize === "lg"
|
||||
? "text-lg"
|
||||
: optionsFontsize === "xl"
|
||||
? "text-xl"
|
||||
: optionsFontsize === "2xl"
|
||||
? "text-2xl"
|
||||
: ""
|
||||
} ${optionsClassName || ""}`}
|
||||
>
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent border-b p-2 mb-1 focus:outline-none sm:text-sm"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
<div className="p-1">
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
className={({ active }) =>
|
||||
`${
|
||||
active ? "text-white bg-theme" : "text-gray-900"
|
||||
} cursor-pointer select-none truncate font-medium relative p-2 rounded-md`
|
||||
}
|
||||
value={option.value}
|
||||
>
|
||||
{option.element ?? option.display}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">No {title.toLowerCase()} found</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchListbox;
|
||||
14
apps/app/ui/SearchListbox/types.d.ts
vendored
Normal file
14
apps/app/ui/SearchListbox/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
type Value = any;
|
||||
|
||||
export type Props = {
|
||||
title: string;
|
||||
multiple?: boolean;
|
||||
options?: Array<{ display: string; element?: JSX.Element; value: Value }>;
|
||||
onChange: (value: Value) => void;
|
||||
value: Value;
|
||||
icon?: JSX.Element;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
width?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
||||
optionsFontsize?: "sm" | "md" | "lg" | "xl" | "2xl";
|
||||
};
|
||||
43
apps/app/ui/Select/index.tsx
Normal file
43
apps/app/ui/Select/index.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React from "react";
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
|
||||
const Select: React.FC<Props> = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
className,
|
||||
name,
|
||||
register,
|
||||
disabled,
|
||||
validations,
|
||||
error,
|
||||
options,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{label && (
|
||||
<label htmlFor={id} className="text-gray-500 mb-2">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<select
|
||||
id={id}
|
||||
name={name}
|
||||
value={value}
|
||||
{...(register && register(name, validations))}
|
||||
disabled={disabled}
|
||||
className="mt-1 block w-full px-3 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md bg-transparent"
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<option value={option.value} key={index}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{error?.message && <div className="text-red-500 text-sm">{error.message}</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
||||
19
apps/app/ui/Select/types.d.ts
vendored
Normal file
19
apps/app/ui/Select/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type {
|
||||
UseFormRegister,
|
||||
RegisterOptions,
|
||||
FieldError,
|
||||
} from "react-hook-form";
|
||||
|
||||
export type Props = {
|
||||
label?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
value?: string | number | readonly string[];
|
||||
className?: string;
|
||||
register?: UseFormRegister<any>;
|
||||
disabled?: boolean;
|
||||
validations?: RegisterOptions;
|
||||
error?: FieldError;
|
||||
autoComplete?: "on" | "off";
|
||||
options: { label: string; value: any }[];
|
||||
};
|
||||
25
apps/app/ui/Spinner/index.tsx
Normal file
25
apps/app/ui/Spinner/index.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
const Spinner: React.FC = () => {
|
||||
return (
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="mr-2 w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
></path>
|
||||
</svg>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
73
apps/app/ui/TextArea/index.tsx
Normal file
73
apps/app/ui/TextArea/index.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import React, { useState, useRef } from "react";
|
||||
// commons
|
||||
import { classNames } from "constants/common";
|
||||
// hooks
|
||||
import useAutosizeTextArea from "lib/hooks/useAutosizeTextArea";
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
|
||||
const TextArea: React.FC<Props> = ({
|
||||
id,
|
||||
label,
|
||||
className,
|
||||
value,
|
||||
placeholder,
|
||||
name,
|
||||
register,
|
||||
mode = "primary",
|
||||
rows,
|
||||
cols,
|
||||
disabled,
|
||||
error,
|
||||
validations,
|
||||
onChange,
|
||||
...rest
|
||||
}) => {
|
||||
const [textareaValue, setTextareaValue] = useState(value ?? "");
|
||||
|
||||
const textAreaRef = useRef<any>(null);
|
||||
|
||||
useAutosizeTextArea(textAreaRef.current, textareaValue);
|
||||
|
||||
return (
|
||||
<>
|
||||
{label && (
|
||||
<label htmlFor={id} className="text-gray-500 mb-2">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<textarea
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
rows={rows}
|
||||
cols={cols}
|
||||
disabled={disabled}
|
||||
{...(register && register(name, validations))}
|
||||
ref={(e) => {
|
||||
textAreaRef.current = e;
|
||||
if (register) register(name).ref(e);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
register && register(name).onChange(e);
|
||||
onChange && onChange(e);
|
||||
setTextareaValue(e.target.value);
|
||||
}}
|
||||
className={classNames(
|
||||
"w-full outline-none px-3 py-2 bg-transparent",
|
||||
mode === "primary" ? "border border-gray-300 rounded-md" : "",
|
||||
mode === "transparent"
|
||||
? "bg-transparent border-none transition-all ring-0 focus:ring-1 focus:ring-indigo-600 rounded"
|
||||
: "",
|
||||
error ? "border-red-500" : "",
|
||||
error && mode === "primary" ? "bg-red-100" : "",
|
||||
className ?? ""
|
||||
)}
|
||||
{...rest}
|
||||
></textarea>
|
||||
{error?.message && <div className="text-red-500 text-sm">{error.message}</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextArea;
|
||||
12
apps/app/ui/TextArea/types.d.ts
vendored
Normal file
12
apps/app/ui/TextArea/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
import type { UseFormRegister, RegisterOptions, FieldError } from "react-hook-form";
|
||||
|
||||
export interface Props extends React.ComponentPropsWithoutRef<"textarea"> {
|
||||
label?: string;
|
||||
value?: string | number | readonly string[];
|
||||
name: string;
|
||||
register?: UseFormRegister<any>;
|
||||
mode?: "primary" | "transparent" | "secondary" | "disabled";
|
||||
validations?: RegisterOptions;
|
||||
error?: FieldError;
|
||||
}
|
||||
41
apps/app/ui/Tooltip/index.tsx
Normal file
41
apps/app/ui/Tooltip/index.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
position?: "top" | "bottom" | "left" | "right";
|
||||
};
|
||||
|
||||
const Tooltip: React.FC<Props> = ({ children, content, position = "top" }) => {
|
||||
return (
|
||||
<div className="relative group">
|
||||
<div
|
||||
className={`fixed pointer-events-none transition-opacity opacity-0 group-hover:opacity-100 bg-black text-white px-3 py-1 rounded ${
|
||||
position === "right"
|
||||
? "left-14"
|
||||
: position === "left"
|
||||
? "right-14"
|
||||
: position === "top"
|
||||
? "bottom-14"
|
||||
: "top-14"
|
||||
}`}
|
||||
>
|
||||
<p className="truncate text-xs">{content}</p>
|
||||
<span
|
||||
className={`absolute w-2 h-2 bg-black ${
|
||||
position === "top"
|
||||
? "top-full left-1/2 transform -translate-y-1/2 -translate-x-1/2 rotate-45"
|
||||
: position === "bottom"
|
||||
? "bottom-full left-1/2 transform translate-y-1/2 -translate-x-1/2 rotate-45"
|
||||
: position === "left"
|
||||
? "left-full top-1/2 transform translate-x-1/2 -translate-y-1/2 rotate-45"
|
||||
: "right-full top-1/2 transform translate-x-1/2 -translate-y-1/2 rotate-45"
|
||||
}`}
|
||||
></span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
11
apps/app/ui/index.ts
Normal file
11
apps/app/ui/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export { default as Button } from "./Button";
|
||||
export { default as Input } from "./Input";
|
||||
export { default as Select } from "./Select";
|
||||
export { default as TextArea } from "./TextArea";
|
||||
export { default as CustomListbox } from "./CustomListbox";
|
||||
export { default as Spinner } from "./Spinner";
|
||||
export { default as Tooltip } from "./Tooltip";
|
||||
export { default as SearchListbox } from "./SearchListbox";
|
||||
export { default as HeaderButton } from "./HeaderButton";
|
||||
export * from "./Breadcrumbs";
|
||||
export * from "./EmptySpace";
|
||||
Loading…
Add table
Add a link
Reference in a new issue