New Directory Setup (#2065)
* chore: moved app & space from apps to root * chore: modified workspace configuration * chore: modified dockerfiles for space and web * chore: modified icons for space * feat: updated files for new svg icons supported by next-images * chore: added /spaces base path for next * chore: added compose config for space * chore: updated husky configuration * chore: updated workflows for new configuration * chore: changed app name to web * fix: resolved build errors with web * chore: reset file tracing root for both projects * chore: added nginx config for deploy * fix: eslint and tsconfig settings for space app * husky setup fixes based on new dir * eslint fixes * prettier formatting --------- Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com>
This commit is contained in:
parent
20e36194b4
commit
1e152c666c
1022 changed files with 1475 additions and 1240 deletions
140
web/components/ui/avatar.tsx
Normal file
140
web/components/ui/avatar.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// component
|
||||
import { Icon } from "components/ui";
|
||||
// services
|
||||
import workspaceService from "services/workspace.service";
|
||||
// icons
|
||||
import User from "public/user.png";
|
||||
// types
|
||||
import { IUser, IUserLite } from "types";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type AvatarProps = {
|
||||
user?: Partial<IUser> | Partial<IUserLite> | null;
|
||||
index?: number;
|
||||
height?: string;
|
||||
width?: string;
|
||||
fontSize?: string;
|
||||
};
|
||||
|
||||
export const Avatar: React.FC<AvatarProps> = ({
|
||||
user,
|
||||
index,
|
||||
height = "24px",
|
||||
width = "24px",
|
||||
fontSize = "12px",
|
||||
}) => (
|
||||
<div
|
||||
className={`relative rounded border-[0.5px] ${
|
||||
index && index !== 0 ? "-ml-3.5 border-custom-border-200" : "border-transparent"
|
||||
}`}
|
||||
style={{
|
||||
height: height,
|
||||
width: width,
|
||||
}}
|
||||
>
|
||||
{user && user.avatar && user.avatar !== "" ? (
|
||||
<div
|
||||
className={`rounded border-[0.5px] ${
|
||||
index ? "border-custom-border-200 bg-custom-background-100" : "border-transparent"
|
||||
}`}
|
||||
style={{
|
||||
height: height,
|
||||
width: width,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={user.avatar}
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded"
|
||||
alt={user.display_name}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="grid place-items-center text-xs capitalize text-white rounded bg-gray-700 border-[0.5px] border-custom-border-200"
|
||||
style={{
|
||||
height: height,
|
||||
width: width,
|
||||
fontSize: fontSize,
|
||||
}}
|
||||
>
|
||||
{user?.display_name?.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
type AsigneesListProps = {
|
||||
users?: Partial<IUser[]> | (Partial<IUserLite> | undefined)[] | Partial<IUserLite>[];
|
||||
userIds?: string[];
|
||||
length?: number;
|
||||
showLength?: boolean;
|
||||
};
|
||||
|
||||
export const AssigneesList: React.FC<AsigneesListProps> = ({
|
||||
users,
|
||||
userIds,
|
||||
length = 3,
|
||||
showLength = true,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: people } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
if ((users && users.length === 0) || (userIds && userIds.length === 0))
|
||||
return (
|
||||
<div className="h-5 w-5 rounded border-[0.5px] border-custom-border-200 bg-custom-background-80">
|
||||
<Image src={User} height="100%" width="100%" className="rounded-full" alt="No user" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{users && (
|
||||
<>
|
||||
{users.slice(0, length).map((user, index) => (
|
||||
<Avatar key={user?.id} user={user} index={index} />
|
||||
))}
|
||||
{users.length > length ? (
|
||||
<div className="-ml-3.5 relative h-6 w-6 rounded">
|
||||
<div className="flex items-center rounded bg-custom-background-80 text-xs capitalize h-6 w-6 text-custom-text-200 border-[0.5px] border-custom-border-300">
|
||||
<Icon iconName="add" className="text-xs !leading-3 -mr-0.5" />
|
||||
{users.length - length}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{userIds && (
|
||||
<>
|
||||
{userIds.slice(0, length).map((userId, index) => {
|
||||
const user = people?.find((p) => p.member.id === userId)?.member;
|
||||
|
||||
return <Avatar key={userId} user={user} index={index} />;
|
||||
})}
|
||||
{showLength ? (
|
||||
userIds.length > length ? (
|
||||
<div className="-ml-3.5 relative h-6 w-6 rounded">
|
||||
<div className="flex items-center rounded bg-custom-background-80 text-xs capitalize h-6 w-6 text-custom-text-200 border-[0.5px] border-custom-border-300">
|
||||
<Icon iconName="add" className="text-xs !leading-3 -mr-0.5" />
|
||||
{userIds.length - length}
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
36
web/components/ui/buttons/danger-button.tsx
Normal file
36
web/components/ui/buttons/danger-button.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// types
|
||||
import { ButtonProps } from "./type";
|
||||
|
||||
export const DangerButton: React.FC<ButtonProps> = ({
|
||||
children,
|
||||
className = "",
|
||||
onClick,
|
||||
type = "button",
|
||||
disabled = false,
|
||||
loading = false,
|
||||
size = "sm",
|
||||
outline = false,
|
||||
}) => (
|
||||
<button
|
||||
type={type}
|
||||
className={`${className} border border-red-500 font-medium duration-300 ${
|
||||
size === "sm"
|
||||
? "rounded px-3 py-2 text-xs"
|
||||
: size === "md"
|
||||
? "rounded-md px-3.5 py-2 text-sm"
|
||||
: "rounded-lg px-4 py-2 text-base"
|
||||
} ${
|
||||
disabled
|
||||
? "cursor-not-allowed bg-opacity-70 border-opacity-70 hover:bg-opacity-70 hover:border-opacity-70"
|
||||
: ""
|
||||
} ${
|
||||
outline
|
||||
? "bg-transparent text-red-500 hover:bg-red-500 hover:text-white"
|
||||
: "text-white bg-red-500 hover:border-opacity-90 hover:bg-opacity-90"
|
||||
} ${loading ? "cursor-wait" : ""}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
3
web/components/ui/buttons/index.ts
Normal file
3
web/components/ui/buttons/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./danger-button";
|
||||
export * from "./primary-button";
|
||||
export * from "./secondary-button";
|
||||
32
web/components/ui/buttons/primary-button.tsx
Normal file
32
web/components/ui/buttons/primary-button.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// types
|
||||
import { ButtonProps } from "./type";
|
||||
|
||||
export const PrimaryButton: React.FC<ButtonProps> = ({
|
||||
children,
|
||||
className = "",
|
||||
onClick,
|
||||
type = "button",
|
||||
disabled = false,
|
||||
loading = false,
|
||||
size = "sm",
|
||||
outline = false,
|
||||
}) => (
|
||||
<button
|
||||
type={type}
|
||||
className={`${className} border border-custom-primary font-medium duration-300 ${
|
||||
size === "sm"
|
||||
? "rounded px-3 py-2 text-xs"
|
||||
: size === "md"
|
||||
? "rounded-md px-3.5 py-2 text-sm"
|
||||
: "rounded-lg px-4 py-2 text-base"
|
||||
} ${disabled ? "cursor-not-allowed opacity-70 hover:opacity-70" : ""} ${
|
||||
outline
|
||||
? "bg-transparent text-custom-primary hover:bg-custom-primary hover:text-white"
|
||||
: "text-white bg-custom-primary hover:border-opacity-90 hover:bg-opacity-90"
|
||||
} ${loading ? "cursor-wait" : ""}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
32
web/components/ui/buttons/secondary-button.tsx
Normal file
32
web/components/ui/buttons/secondary-button.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// types
|
||||
import { ButtonProps } from "./type";
|
||||
|
||||
export const SecondaryButton: React.FC<ButtonProps> = ({
|
||||
children,
|
||||
className = "",
|
||||
onClick,
|
||||
type = "button",
|
||||
disabled = false,
|
||||
loading = false,
|
||||
size = "sm",
|
||||
outline = false,
|
||||
}) => (
|
||||
<button
|
||||
type={type}
|
||||
className={`${className} border border-custom-border-200 font-medium duration-300 ${
|
||||
size === "sm"
|
||||
? "rounded px-3 py-2 text-xs"
|
||||
: size === "md"
|
||||
? "rounded-md px-3.5 py-2 text-sm"
|
||||
: "rounded-lg px-4 py-2 text-base"
|
||||
} ${disabled ? "cursor-not-allowed border-custom-border-200 bg-custom-background-90" : ""} ${
|
||||
outline
|
||||
? "bg-transparent hover:bg-custom-background-80"
|
||||
: "bg-custom-background-100 hover:border-opacity-70 hover:bg-opacity-70"
|
||||
} ${loading ? "cursor-wait" : ""}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
10
web/components/ui/buttons/type.d.ts
vendored
Normal file
10
web/components/ui/buttons/type.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export type ButtonProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
type?: "button" | "submit" | "reset";
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
size?: "sm" | "md" | "lg";
|
||||
outline?: boolean;
|
||||
};
|
||||
39
web/components/ui/circular-progress.tsx
Normal file
39
web/components/ui/circular-progress.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
|
||||
export const CircularProgress = ({ progress }: { progress: number }) => {
|
||||
const [circumference, setCircumference] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const radius = 40;
|
||||
const calcCircumference = 2 * Math.PI * radius;
|
||||
setCircumference(calcCircumference);
|
||||
}, []);
|
||||
|
||||
const progressAngle = (progress / 100) * 360 >= 360 ? 359.9 : (progress / 100) * 360;
|
||||
const progressX = 50 + Math.cos((progressAngle - 90) * (Math.PI / 180)) * 40;
|
||||
const progressY = 50 + Math.sin((progressAngle - 90) * (Math.PI / 180)) * 40;
|
||||
|
||||
return (
|
||||
<div className="relative h-5 w-5">
|
||||
<svg className="absolute top-0 left-0" viewBox="0 0 100 100">
|
||||
<circle
|
||||
className="stroke-current"
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
strokeWidth="12"
|
||||
fill="none"
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
/>
|
||||
<path
|
||||
className="fill-current"
|
||||
d={`M50 10
|
||||
A40 40 0 ${progress > 50 ? 1 : 0} 1 ${progressX} ${progressY}
|
||||
L50 50 Z`}
|
||||
strokeWidth="12"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
76
web/components/ui/date.tsx
Normal file
76
web/components/ui/date.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import React from "react";
|
||||
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { CalendarDaysIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
// react-datepicker
|
||||
import DatePicker from "react-datepicker";
|
||||
// import "react-datepicker/dist/react-datepicker.css";
|
||||
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
value: string | null;
|
||||
onChange: (val: string | null) => void;
|
||||
label: string;
|
||||
minDate?: Date;
|
||||
maxDate?: Date;
|
||||
closeOnSelect?: boolean;
|
||||
};
|
||||
|
||||
export const DateSelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
minDate,
|
||||
maxDate,
|
||||
closeOnSelect = true,
|
||||
}) => (
|
||||
<Popover className="relative flex items-center justify-center rounded-lg">
|
||||
{({ close }) => (
|
||||
<>
|
||||
<Popover.Button className="flex cursor-pointer items-center rounded-md border border-custom-border-200 text-xs shadow-sm duration-300 hover:bg-custom-background-80">
|
||||
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-custom-text-200">
|
||||
{value ? (
|
||||
<>
|
||||
<span className="text-custom-text-100">{renderShortDateWithYearFormat(value)}</span>
|
||||
<button onClick={() => onChange(null)}>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CalendarDaysIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>{label}</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute top-10 -left-10 z-20 transform overflow-hidden">
|
||||
<DatePicker
|
||||
selected={value ? new Date(value) : null}
|
||||
onChange={(val) => {
|
||||
if (!val) onChange("");
|
||||
else onChange(renderDateFormat(val));
|
||||
|
||||
if (closeOnSelect) close();
|
||||
}}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
inline
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
71
web/components/ui/datepicker.tsx
Normal file
71
web/components/ui/datepicker.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// react-datepicker
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
// helpers
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
renderAs?: "input" | "button";
|
||||
value: Date | string | null | undefined;
|
||||
onChange: (val: string | null) => void;
|
||||
handleOnOpen?: () => void;
|
||||
handleOnClose?: () => void;
|
||||
placeholder?: string;
|
||||
displayShortForm?: boolean;
|
||||
error?: boolean;
|
||||
noBorder?: boolean;
|
||||
wrapperClassName?: string;
|
||||
className?: string;
|
||||
isClearable?: boolean;
|
||||
disabled?: boolean;
|
||||
maxDate?: Date;
|
||||
minDate?: Date;
|
||||
};
|
||||
|
||||
export const CustomDatePicker: React.FC<Props> = ({
|
||||
renderAs = "button",
|
||||
value,
|
||||
onChange,
|
||||
handleOnOpen,
|
||||
handleOnClose,
|
||||
placeholder = "Select date",
|
||||
displayShortForm = false,
|
||||
error = false,
|
||||
noBorder = false,
|
||||
wrapperClassName = "",
|
||||
className = "",
|
||||
isClearable = true,
|
||||
disabled = false,
|
||||
maxDate,
|
||||
minDate,
|
||||
}) => (
|
||||
<DatePicker
|
||||
placeholderText={placeholder}
|
||||
selected={value ? new Date(value) : null}
|
||||
onChange={(val) => {
|
||||
if (!val) onChange(null);
|
||||
else onChange(renderDateFormat(val));
|
||||
}}
|
||||
onCalendarOpen={handleOnOpen}
|
||||
onCalendarClose={handleOnClose}
|
||||
wrapperClassName={wrapperClassName}
|
||||
className={`${
|
||||
renderAs === "input"
|
||||
? "block px-2 py-2 text-sm focus:outline-none"
|
||||
: renderAs === "button"
|
||||
? `px-2 py-1 text-xs shadow-sm ${
|
||||
disabled ? "" : "hover:bg-custom-background-80"
|
||||
} duration-300`
|
||||
: ""
|
||||
} ${error ? "border-red-500 bg-red-100" : ""} ${
|
||||
disabled ? "cursor-not-allowed" : "cursor-pointer"
|
||||
} ${
|
||||
noBorder ? "" : "border border-custom-border-200"
|
||||
} w-full rounded-md caret-transparent outline-none ${className}`}
|
||||
dateFormat="MMM dd, yyyy"
|
||||
isClearable={isClearable}
|
||||
disabled={disabled}
|
||||
maxDate={maxDate}
|
||||
minDate={minDate}
|
||||
/>
|
||||
);
|
||||
132
web/components/ui/dropdowns/context-menu.tsx
Normal file
132
web/components/ui/dropdowns/context-menu.tsx
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
// hooks
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
|
||||
type Props = {
|
||||
clickEvent: React.MouseEvent | null;
|
||||
children: React.ReactNode;
|
||||
title?: string | JSX.Element;
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const ContextMenu = ({ clickEvent, children, title, isOpen, setIsOpen }: Props) => {
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close the context menu when clicked outside
|
||||
useOutsideClickDetector(contextMenuRef, () => {
|
||||
if (isOpen) setIsOpen(false);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const hideContextMenu = () => {
|
||||
if (isOpen) setIsOpen(false);
|
||||
};
|
||||
|
||||
const escapeKeyEvent = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") hideContextMenu();
|
||||
};
|
||||
|
||||
window.addEventListener("click", hideContextMenu);
|
||||
window.addEventListener("keydown", escapeKeyEvent);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("click", hideContextMenu);
|
||||
window.removeEventListener("keydown", escapeKeyEvent);
|
||||
};
|
||||
}, [isOpen, setIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const contextMenu = contextMenuRef.current;
|
||||
|
||||
if (contextMenu && isOpen) {
|
||||
const contextMenuWidth = contextMenu.clientWidth;
|
||||
const contextMenuHeight = contextMenu.clientHeight;
|
||||
|
||||
const clickX = clickEvent?.pageX || 0;
|
||||
const clickY = clickEvent?.pageY || 0;
|
||||
|
||||
let top = clickY;
|
||||
// check if there's enough space at the bottom, otherwise show at the top
|
||||
if (clickY + contextMenuHeight > window.innerHeight) top = clickY - contextMenuHeight;
|
||||
|
||||
// check if there's enough space on the right, otherwise show on the left
|
||||
let left = clickX;
|
||||
if (clickX + contextMenuWidth > window.innerWidth) left = clickX - contextMenuWidth;
|
||||
|
||||
contextMenu.style.top = `${top}px`;
|
||||
contextMenu.style.left = `${left}px`;
|
||||
}
|
||||
}, [clickEvent, isOpen]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed z-50 top-0 left-0 h-full w-full ${
|
||||
isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
ref={contextMenuRef}
|
||||
className={`fixed z-50 flex min-w-[8rem] flex-col items-stretch gap-1 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs shadow-lg`}
|
||||
>
|
||||
{title && (
|
||||
<h4 className="border-b border-custom-border-200 px-1 py-1 pb-2 text-[0.8rem] font-medium">
|
||||
{title}
|
||||
</h4>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type MenuItemProps = {
|
||||
children: JSX.Element | string;
|
||||
renderAs?: "button" | "a";
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
Icon?: any;
|
||||
};
|
||||
|
||||
const MenuItem: React.FC<MenuItemProps> = ({
|
||||
children,
|
||||
renderAs,
|
||||
href = "",
|
||||
onClick,
|
||||
className = "",
|
||||
Icon,
|
||||
}) => (
|
||||
<>
|
||||
{renderAs === "a" ? (
|
||||
<Link href={href}>
|
||||
<a
|
||||
className={`${className} flex w-full items-center gap-2 rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80`}
|
||||
>
|
||||
<>
|
||||
{Icon && <Icon />}
|
||||
{children}
|
||||
</>
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={`${className} flex w-full items-center gap-2 rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<>
|
||||
{Icon && <Icon height={12} width={12} />}
|
||||
{children}
|
||||
</>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
ContextMenu.Item = MenuItem;
|
||||
|
||||
export { ContextMenu };
|
||||
166
web/components/ui/dropdowns/custom-menu.tsx
Normal file
166
web/components/ui/dropdowns/custom-menu.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
// headless ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { DropdownProps } from "components/ui";
|
||||
// icons
|
||||
import { ExpandMoreOutlined, MoreHorizOutlined } from "@mui/icons-material";
|
||||
|
||||
export type CustomMenuProps = DropdownProps & {
|
||||
children: React.ReactNode;
|
||||
ellipsis?: boolean;
|
||||
noBorder?: boolean;
|
||||
verticalEllipsis?: boolean;
|
||||
menuButtonOnClick?: (...args: any) => void;
|
||||
};
|
||||
|
||||
const CustomMenu = ({
|
||||
buttonClassName = "",
|
||||
children,
|
||||
className = "",
|
||||
customButton,
|
||||
disabled = false,
|
||||
ellipsis = false,
|
||||
label,
|
||||
maxHeight = "md",
|
||||
noBorder = false,
|
||||
noChevron = false,
|
||||
optionsClassName = "",
|
||||
position = "right",
|
||||
selfPositioned = false,
|
||||
verticalEllipsis = false,
|
||||
verticalPosition = "bottom",
|
||||
width = "auto",
|
||||
menuButtonOnClick,
|
||||
}: CustomMenuProps) => (
|
||||
<Menu as="div" className={`${selfPositioned ? "" : "relative"} w-min text-left ${className}`}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{customButton ? (
|
||||
<Menu.Button as="button" type="button" onClick={menuButtonOnClick}>
|
||||
{customButton}
|
||||
</Menu.Button>
|
||||
) : (
|
||||
<>
|
||||
{ellipsis || verticalEllipsis ? (
|
||||
<Menu.Button
|
||||
type="button"
|
||||
onClick={menuButtonOnClick}
|
||||
disabled={disabled}
|
||||
className={`relative grid place-items-center rounded p-1 text-custom-text-200 hover:text-custom-text-100 outline-none ${
|
||||
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
<MoreHorizOutlined
|
||||
fontSize="small"
|
||||
className={verticalEllipsis ? "rotate-90" : ""}
|
||||
/>
|
||||
</Menu.Button>
|
||||
) : (
|
||||
<Menu.Button
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 rounded-md px-2.5 py-1 text-xs whitespace-nowrap duration-300 ${
|
||||
open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200"
|
||||
} ${
|
||||
noBorder ? "" : "border border-custom-border-300 shadow-sm focus:outline-none"
|
||||
} ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && (
|
||||
<ExpandMoreOutlined
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</Menu.Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className={`absolute z-10 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 p-1 text-xs shadow-lg focus:outline-none bg-custom-background-90 ${
|
||||
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
||||
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
|
||||
>
|
||||
<div className="py-1">{children}</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
type MenuItemProps = {
|
||||
children: React.ReactNode;
|
||||
renderAs?: "button" | "a";
|
||||
href?: string;
|
||||
onClick?: (args?: any) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const MenuItem: React.FC<MenuItemProps> = ({
|
||||
children,
|
||||
renderAs,
|
||||
href,
|
||||
onClick,
|
||||
className = "",
|
||||
}) => (
|
||||
<Menu.Item as="div">
|
||||
{({ active, close }) =>
|
||||
renderAs === "a" ? (
|
||||
<Link href={href ?? ""}>
|
||||
<a
|
||||
className={`inline-block w-full select-none truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80 ${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${className}`}
|
||||
onClick={close}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={`w-full select-none truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80 ${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${className}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</Menu.Item>
|
||||
);
|
||||
|
||||
CustomMenu.MenuItem = MenuItem;
|
||||
|
||||
export { CustomMenu };
|
||||
186
web/components/ui/dropdowns/custom-search-select.tsx
Normal file
186
web/components/ui/dropdowns/custom-search-select.tsx
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import React, { useState } from "react";
|
||||
|
||||
// headless ui
|
||||
import { Combobox, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { DropdownProps } from "./types";
|
||||
|
||||
export type CustomSearchSelectProps = DropdownProps & {
|
||||
footerOption?: JSX.Element;
|
||||
onChange: any;
|
||||
options:
|
||||
| {
|
||||
value: any;
|
||||
query: string;
|
||||
content: React.ReactNode;
|
||||
}[]
|
||||
| undefined;
|
||||
} & (
|
||||
| { multiple?: false; value: any } // if multiple is false, value can be anything
|
||||
| {
|
||||
multiple?: true;
|
||||
value: any[] | null; // if multiple is true, value should be an array
|
||||
}
|
||||
);
|
||||
|
||||
export const CustomSearchSelect = ({
|
||||
buttonClassName = "",
|
||||
className = "",
|
||||
customButton,
|
||||
disabled = false,
|
||||
footerOption,
|
||||
input = false,
|
||||
label,
|
||||
maxHeight = "md",
|
||||
multiple = false,
|
||||
noChevron = false,
|
||||
onChange,
|
||||
options,
|
||||
onOpen,
|
||||
optionsClassName = "",
|
||||
position = "left",
|
||||
selfPositioned = false,
|
||||
value,
|
||||
verticalPosition = "bottom",
|
||||
width = "auto",
|
||||
}: CustomSearchSelectProps) => {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? options
|
||||
: options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const props: any = {
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
};
|
||||
|
||||
if (multiple) props.multiple = true;
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`${selfPositioned ? "" : "relative"} flex-shrink-0 text-left ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open && onOpen) onOpen();
|
||||
|
||||
return (
|
||||
<>
|
||||
{customButton ? (
|
||||
<Combobox.Button as="div">{customButton}</Combobox.Button>
|
||||
) : (
|
||||
<Combobox.Button
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
|
||||
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
||||
} ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</Combobox.Button>
|
||||
)}
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Combobox.Options
|
||||
className={`absolute z-10 min-w-[10rem] border border-custom-border-300 p-2 rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
|
||||
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
||||
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
||||
width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width
|
||||
} ${optionsClassName}`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded-sm border-[0.6px] border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3 w-3 text-custom-text-200" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Type to search..."
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`mt-2 space-y-1 ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} overflow-y-scroll`}
|
||||
>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active || selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ active, selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{multiple ? (
|
||||
<div
|
||||
className={`flex items-center justify-center rounded border border-custom-border-400 p-0.5 ${
|
||||
active || selected ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
>
|
||||
<CheckIcon
|
||||
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<CheckIcon
|
||||
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
{footerOption}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
118
web/components/ui/dropdowns/custom-select.tsx
Normal file
118
web/components/ui/dropdowns/custom-select.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import React from "react";
|
||||
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { DropdownProps } from "./types";
|
||||
|
||||
export type CustomSelectProps = DropdownProps & {
|
||||
children: React.ReactNode;
|
||||
value: any;
|
||||
onChange: any;
|
||||
};
|
||||
|
||||
const CustomSelect = ({
|
||||
buttonClassName = "",
|
||||
children,
|
||||
className = "",
|
||||
customButton,
|
||||
disabled = false,
|
||||
input = false,
|
||||
label,
|
||||
maxHeight = "md",
|
||||
noChevron = false,
|
||||
onChange,
|
||||
optionsClassName = "",
|
||||
position = "left",
|
||||
selfPositioned = false,
|
||||
value,
|
||||
verticalPosition = "bottom",
|
||||
width = "auto",
|
||||
}: CustomSelectProps) => (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className={`${selfPositioned ? "" : "relative"} flex-shrink-0 text-left ${className}`}
|
||||
disabled={disabled}
|
||||
>
|
||||
<>
|
||||
{customButton ? (
|
||||
<Listbox.Button as={React.Fragment}>{customButton}</Listbox.Button>
|
||||
) : (
|
||||
<Listbox.Button
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full rounded-md border border-custom-border-300 shadow-sm duration-300 focus:outline-none ${
|
||||
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
||||
} ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
|
||||
</Listbox.Button>
|
||||
)}
|
||||
</>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Listbox.Options
|
||||
className={`absolute z-10 border border-custom-border-300 mt-1 origin-top-right overflow-y-auto rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
|
||||
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
||||
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
|
||||
>
|
||||
<div className="space-y-1 p-2">{children}</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</Listbox>
|
||||
);
|
||||
|
||||
type OptionProps = {
|
||||
children: React.ReactNode;
|
||||
value: any;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Option: React.FC<OptionProps> = ({ children, value, className }) => (
|
||||
<Listbox.Option
|
||||
value={value}
|
||||
className={({ active, selected }) =>
|
||||
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active || selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"} ${className}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">{children}</div>
|
||||
{selected && <CheckIcon className="h-4 w-4 flex-shrink-0" />}
|
||||
</div>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
);
|
||||
|
||||
CustomSelect.Option = Option;
|
||||
|
||||
export { CustomSelect };
|
||||
5
web/components/ui/dropdowns/index.ts
Normal file
5
web/components/ui/dropdowns/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export * from "./context-menu";
|
||||
export * from "./custom-menu";
|
||||
export * from "./custom-search-select";
|
||||
export * from "./custom-select";
|
||||
export * from "./types.d";
|
||||
16
web/components/ui/dropdowns/types.d.ts
vendored
Normal file
16
web/components/ui/dropdowns/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export type DropdownProps = {
|
||||
buttonClassName?: string;
|
||||
className?: string;
|
||||
customButton?: JSX.Element;
|
||||
disabled?: boolean;
|
||||
input?: boolean;
|
||||
label?: string | JSX.Element;
|
||||
maxHeight?: "sm" | "rg" | "md" | "lg";
|
||||
noChevron?: boolean;
|
||||
onOpen?: () => void;
|
||||
optionsClassName?: string;
|
||||
position?: "right" | "left";
|
||||
selfPositioned?: boolean;
|
||||
verticalPosition?: "top" | "bottom";
|
||||
width?: "auto" | string;
|
||||
};
|
||||
82
web/components/ui/empty-space.tsx
Normal file
82
web/components/ui/empty-space.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// 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?: any;
|
||||
link?: { text: string; href: string };
|
||||
};
|
||||
|
||||
const EmptySpace: React.FC<EmptySpaceProps> = ({ title, description, children, Icon, link }) => (
|
||||
<>
|
||||
<div className="max-w-lg">
|
||||
{Icon ? (
|
||||
<div className="mb-4">
|
||||
<Icon className="h-14 w-14 text-custom-text-200" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<h2 className="text-lg font-medium text-custom-text-100">{title}</h2>
|
||||
<div className="mt-1 text-sm text-custom-text-200">{description}</div>
|
||||
<ul
|
||||
role="list"
|
||||
className="mt-6 divide-y divide-custom-border-200 border-t border-b border-custom-border-200"
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
{link ? (
|
||||
<div className="mt-6 flex">
|
||||
<Link href={link.href}>
|
||||
<a className="text-sm font-medium text-custom-primary hover:text-custom-primary">
|
||||
{link.text}
|
||||
<span aria-hidden="true"> →</span>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
type EmptySpaceItemProps = {
|
||||
title: string;
|
||||
description?: React.ReactNode | string;
|
||||
Icon: any;
|
||||
action: () => void;
|
||||
};
|
||||
|
||||
const EmptySpaceItem: React.FC<EmptySpaceItemProps> = ({ title, description, Icon, action }) => (
|
||||
<>
|
||||
<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 h-10 w-10 items-center justify-center rounded-lg bg-custom-primary">
|
||||
<Icon className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 text-custom-text-200">
|
||||
<div className="text-sm font-medium group-hover:text-custom-text-100">{title}</div>
|
||||
{description ? <div className="text-sm">{description}</div> : null}
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<ChevronRightIcon
|
||||
className="h-5 w-5 text-custom-text-200 group-hover:text-custom-text-100"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
|
||||
export { EmptySpace, EmptySpaceItem };
|
||||
49
web/components/ui/empty-state.tsx
Normal file
49
web/components/ui/empty-state.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import React from "react";
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
// ui
|
||||
import { PrimaryButton } from "components/ui";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: React.ReactNode;
|
||||
image: any;
|
||||
primaryButton?: {
|
||||
icon?: any;
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
secondaryButton?: React.ReactNode;
|
||||
isFullScreen?: boolean;
|
||||
};
|
||||
|
||||
export const EmptyState: React.FC<Props> = ({
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
isFullScreen = true,
|
||||
}) => (
|
||||
<div
|
||||
className={`h-full w-full mx-auto grid place-items-center p-8 ${
|
||||
isFullScreen ? "md:w-4/5 lg:w-3/5" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="text-center flex flex-col items-center w-full">
|
||||
<Image src={image} className="w-52 sm:w-60" alt={primaryButton?.text} />
|
||||
<h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">{title}</h6>
|
||||
{description && <p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>}
|
||||
<div className="flex items-center gap-4">
|
||||
{primaryButton && (
|
||||
<PrimaryButton className="flex items-center gap-1.5" onClick={primaryButton.onClick}>
|
||||
{primaryButton.icon}
|
||||
{primaryButton.text}
|
||||
</PrimaryButton>
|
||||
)}
|
||||
{secondaryButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
51
web/components/ui/graphs/bar-graph.tsx
Normal file
51
web/components/ui/graphs/bar-graph.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// nivo
|
||||
import { ResponsiveBar, BarSvgProps } from "@nivo/bar";
|
||||
// helpers
|
||||
import { generateYAxisTickValues } from "helpers/graph.helper";
|
||||
// types
|
||||
import { TGraph } from "./types";
|
||||
// constants
|
||||
import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph";
|
||||
|
||||
type Props = {
|
||||
indexBy: string;
|
||||
keys: string[];
|
||||
customYAxisTickValues?: number[];
|
||||
};
|
||||
|
||||
export const BarGraph: React.FC<Props & TGraph & Omit<BarSvgProps<any>, "height" | "width">> = ({
|
||||
indexBy,
|
||||
keys,
|
||||
customYAxisTickValues,
|
||||
height = "400px",
|
||||
width = "100%",
|
||||
margin,
|
||||
theme,
|
||||
...rest
|
||||
}) => (
|
||||
<div style={{ height, width }}>
|
||||
<ResponsiveBar
|
||||
indexBy={indexBy}
|
||||
keys={keys}
|
||||
margin={{ ...DEFAULT_MARGIN, ...(margin ?? {}) }}
|
||||
padding={rest.padding ?? rest.data.length > 7 ? 0.8 : 0.9}
|
||||
axisLeft={{
|
||||
tickSize: 0,
|
||||
tickPadding: 10,
|
||||
tickValues: customYAxisTickValues
|
||||
? generateYAxisTickValues(customYAxisTickValues)
|
||||
: undefined,
|
||||
}}
|
||||
axisBottom={{
|
||||
tickSize: 0,
|
||||
tickPadding: 10,
|
||||
tickRotation: rest.data.length > 7 ? -45 : 0,
|
||||
}}
|
||||
labelTextColor={{ from: "color", modifiers: [["darker", 1.6]] }}
|
||||
theme={{ ...CHARTS_THEME, ...(theme ?? {}) }}
|
||||
animate={true}
|
||||
enableLabel={rest.enableLabel ?? false}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
34
web/components/ui/graphs/calendar-graph.tsx
Normal file
34
web/components/ui/graphs/calendar-graph.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// nivo
|
||||
import { ResponsiveCalendar, CalendarSvgProps } from "@nivo/calendar";
|
||||
// types
|
||||
import { TGraph } from "./types";
|
||||
// constants
|
||||
import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph";
|
||||
|
||||
export const CalendarGraph: React.FC<TGraph & Omit<CalendarSvgProps, "height" | "width">> = ({
|
||||
height = "400px",
|
||||
width = "100%",
|
||||
margin,
|
||||
theme,
|
||||
...rest
|
||||
}) => (
|
||||
<div style={{ height, width }}>
|
||||
<ResponsiveCalendar
|
||||
margin={{ ...DEFAULT_MARGIN, ...(margin ?? {}) }}
|
||||
colors={
|
||||
rest.colors ?? [
|
||||
"rgba(var(--color-primary-100), 0.2)",
|
||||
"rgba(var(--color-primary-100), 0.4)",
|
||||
"rgba(var(--color-primary-100), 0.8)",
|
||||
"rgba(var(--color-primary-100), 1)",
|
||||
]
|
||||
}
|
||||
emptyColor={rest.emptyColor ?? "rgb(var(--color-background-80))"}
|
||||
dayBorderColor={rest.dayBorderColor ?? "transparent"}
|
||||
daySpacing={rest.daySpacing ?? 5}
|
||||
monthBorderColor={rest.monthBorderColor ?? "rgb(var(--color-background-100))"}
|
||||
theme={{ ...CHARTS_THEME, ...(theme ?? {}) }}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
5
web/components/ui/graphs/index.ts
Normal file
5
web/components/ui/graphs/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export * from "./bar-graph";
|
||||
export * from "./calendar-graph";
|
||||
export * from "./line-graph";
|
||||
export * from "./pie-graph";
|
||||
export * from "./scatter-plot-graph";
|
||||
37
web/components/ui/graphs/line-graph.tsx
Normal file
37
web/components/ui/graphs/line-graph.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// nivo
|
||||
import { ResponsiveLine, LineSvgProps } from "@nivo/line";
|
||||
// helpers
|
||||
import { generateYAxisTickValues } from "helpers/graph.helper";
|
||||
// types
|
||||
import { TGraph } from "./types";
|
||||
// constants
|
||||
import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph";
|
||||
|
||||
type Props = {
|
||||
customYAxisTickValues?: number[];
|
||||
};
|
||||
|
||||
export const LineGraph: React.FC<Props & TGraph & LineSvgProps> = ({
|
||||
customYAxisTickValues,
|
||||
height = "400px",
|
||||
width = "100%",
|
||||
margin,
|
||||
theme,
|
||||
...rest
|
||||
}) => (
|
||||
<div style={{ height, width }}>
|
||||
<ResponsiveLine
|
||||
margin={{ ...DEFAULT_MARGIN, ...(margin ?? {}) }}
|
||||
axisLeft={{
|
||||
tickSize: 0,
|
||||
tickPadding: 10,
|
||||
tickValues: customYAxisTickValues
|
||||
? generateYAxisTickValues(customYAxisTickValues)
|
||||
: undefined,
|
||||
}}
|
||||
theme={{ ...CHARTS_THEME, ...(theme ?? {}) }}
|
||||
animate={true}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
23
web/components/ui/graphs/pie-graph.tsx
Normal file
23
web/components/ui/graphs/pie-graph.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// nivo
|
||||
import { PieSvgProps, ResponsivePie } from "@nivo/pie";
|
||||
// types
|
||||
import { TGraph } from "./types";
|
||||
// constants
|
||||
import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph";
|
||||
|
||||
export const PieGraph: React.FC<TGraph & Omit<PieSvgProps<any>, "height" | "width">> = ({
|
||||
height = "400px",
|
||||
width = "100%",
|
||||
margin,
|
||||
theme,
|
||||
...rest
|
||||
}) => (
|
||||
<div style={{ height, width }}>
|
||||
<ResponsivePie
|
||||
margin={{ ...DEFAULT_MARGIN, ...(margin ?? {}) }}
|
||||
theme={{ ...CHARTS_THEME, ...(theme ?? {}) }}
|
||||
animate={true}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
19
web/components/ui/graphs/scatter-plot-graph.tsx
Normal file
19
web/components/ui/graphs/scatter-plot-graph.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// nivo
|
||||
import { ResponsiveScatterPlot, ScatterPlotSvgProps } from "@nivo/scatterplot";
|
||||
// types
|
||||
import { TGraph } from "./types";
|
||||
// constants
|
||||
import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph";
|
||||
|
||||
export const ScatterPlotGraph: React.FC<
|
||||
TGraph & Omit<ScatterPlotSvgProps<any>, "height" | "width">
|
||||
> = ({ height = "400px", width = "100%", margin, theme, ...rest }) => (
|
||||
<div style={{ height, width }}>
|
||||
<ResponsiveScatterPlot
|
||||
margin={{ ...DEFAULT_MARGIN, ...(margin ?? {}) }}
|
||||
animate={true}
|
||||
theme={{ ...CHARTS_THEME, ...(theme ?? {}) }}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
8
web/components/ui/graphs/types.d.ts
vendored
Normal file
8
web/components/ui/graphs/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Theme, Margin } from "@nivo/core";
|
||||
|
||||
export type TGraph = {
|
||||
height?: string;
|
||||
width?: string;
|
||||
margin?: Partial<Margin>;
|
||||
theme?: Theme;
|
||||
};
|
||||
2991
web/components/ui/icon-name-type.d.ts
vendored
Normal file
2991
web/components/ui/icon-name-type.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
12
web/components/ui/icon.tsx
Normal file
12
web/components/ui/icon.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
iconName: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Icon: React.FC<Props> = ({ iconName, className = "" }) => (
|
||||
<span className={`material-symbols-rounded text-sm leading-5 font-light ${className}`}>
|
||||
{iconName}
|
||||
</span>
|
||||
);
|
||||
26
web/components/ui/index.ts
Normal file
26
web/components/ui/index.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export * from "./buttons";
|
||||
export * from "./dropdowns";
|
||||
export * from "./graphs";
|
||||
export * from "./input";
|
||||
export * from "./text-area";
|
||||
export * from "./avatar";
|
||||
export * from "./date";
|
||||
export * from "./datepicker";
|
||||
export * from "./empty-space";
|
||||
export * from "./empty-state";
|
||||
export * from "./icon";
|
||||
export * from "./labels-list";
|
||||
export * from "./linear-progress-indicator";
|
||||
export * from "./loader";
|
||||
export * from "./multi-level-dropdown";
|
||||
export * from "./multi-level-select";
|
||||
export * from "./progress-bar";
|
||||
export * from "./spinner";
|
||||
export * from "./tooltip";
|
||||
export * from "./toggle-switch";
|
||||
export * from "./markdown-to-component";
|
||||
export * from "./product-updates-modal";
|
||||
export * from "./integration-and-import-export-banner";
|
||||
export * from "./range-datepicker";
|
||||
export * from "./circular-progress";
|
||||
export * from "./profile-empty-state";
|
||||
52
web/components/ui/input/index.tsx
Normal file
52
web/components/ui/input/index.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import * as React from "react";
|
||||
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
|
||||
export const Input: React.FC<Props> = ({
|
||||
label,
|
||||
value,
|
||||
name,
|
||||
register,
|
||||
validations,
|
||||
error,
|
||||
mode = "primary",
|
||||
onChange,
|
||||
className = "",
|
||||
type,
|
||||
id,
|
||||
size = "rg",
|
||||
fullWidth = true,
|
||||
...rest
|
||||
}) => (
|
||||
<>
|
||||
{label && (
|
||||
<label htmlFor={id} className="text-custom-text-200 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={`block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${
|
||||
mode === "primary"
|
||||
? "rounded-md border border-custom-border-200"
|
||||
: mode === "transparent"
|
||||
? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary"
|
||||
: mode === "trueTransparent"
|
||||
? "rounded border-none bg-transparent ring-0"
|
||||
: ""
|
||||
} ${error ? "border-red-500" : ""} ${error && mode === "primary" ? "bg-red-500/20" : ""} ${
|
||||
fullWidth ? "w-full" : ""
|
||||
} ${size === "rg" ? "px-3 py-2" : size === "lg" ? "p-3" : ""} ${className}`}
|
||||
{...rest}
|
||||
/>
|
||||
{error?.message && <div className="text-sm text-red-500">{error.message}</div>}
|
||||
</>
|
||||
);
|
||||
15
web/components/ui/input/types.d.ts
vendored
Normal file
15
web/components/ui/input/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import * as React from "react";
|
||||
import type { UseFormRegister, RegisterOptions } from "react-hook-form";
|
||||
|
||||
export interface Props extends React.ComponentPropsWithoutRef<"input"> {
|
||||
label?: string;
|
||||
name?: string;
|
||||
value?: string | number | readonly string[];
|
||||
mode?: "primary" | "transparent" | "trueTransparent" | "secondary" | "disabled";
|
||||
register?: UseFormRegister<any>;
|
||||
validations?: RegisterOptions;
|
||||
error?: any;
|
||||
className?: string;
|
||||
size?: "rg" | "lg";
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
18
web/components/ui/integration-and-import-export-banner.tsx
Normal file
18
web/components/ui/integration-and-import-export-banner.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { ExclamationIcon } from "components/icons";
|
||||
|
||||
type Props = {
|
||||
bannerName: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName, description }) => (
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<h3 className="text-2xl font-semibold">{bannerName}</h3>
|
||||
{description && (
|
||||
<div className="flex items-center gap-3 rounded-[10px] border border-custom-primary/75 bg-custom-primary/5 p-4 text-sm text-custom-text-100">
|
||||
<ExclamationIcon height={24} width={24} className="fill-current text-custom-text-100" />
|
||||
<p className="leading-5">{description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
34
web/components/ui/labels-list.tsx
Normal file
34
web/components/ui/labels-list.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import React from "react";
|
||||
// ui
|
||||
import { Tooltip } from "components/ui";
|
||||
// types
|
||||
import { IIssueLabels } from "types";
|
||||
|
||||
type IssueLabelsListProps = {
|
||||
labels?: (IIssueLabels | undefined)[];
|
||||
length?: number;
|
||||
showLength?: boolean;
|
||||
};
|
||||
|
||||
export const IssueLabelsList: React.FC<IssueLabelsListProps> = ({
|
||||
labels,
|
||||
length = 5,
|
||||
showLength = true,
|
||||
}) => (
|
||||
<>
|
||||
{labels && (
|
||||
<>
|
||||
<Tooltip
|
||||
position="top"
|
||||
tooltipHeading="Labels"
|
||||
tooltipContent={labels.map((l) => l?.name).join(", ")}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 text-custom-text-200 rounded shadow-sm border border-custom-border-300">
|
||||
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
|
||||
{`${labels.length} Labels`}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
38
web/components/ui/linear-progress-indicator.tsx
Normal file
38
web/components/ui/linear-progress-indicator.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React from "react";
|
||||
import { Tooltip } from "./tooltip";
|
||||
|
||||
type Props = {
|
||||
data: any;
|
||||
noTooltip?: boolean;
|
||||
};
|
||||
|
||||
export const LinearProgressIndicator: React.FC<Props> = ({ data, noTooltip = false }) => {
|
||||
const total = data.reduce((acc: any, cur: any) => acc + cur.value, 0);
|
||||
let progress = 0;
|
||||
|
||||
const bars = data.map((item: any) => {
|
||||
const width = `${(item.value / total) * 100}%`;
|
||||
const style = {
|
||||
width,
|
||||
backgroundColor: item.color,
|
||||
};
|
||||
progress += item.value;
|
||||
if (noTooltip) return <div style={style} />;
|
||||
else
|
||||
return (
|
||||
<Tooltip key={item.id} tooltipContent={`${item.name} ${Math.round(item.value)}%`}>
|
||||
<div style={style} />
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-1 w-full items-center justify-between gap-1">
|
||||
{total === 0 ? (
|
||||
<div className="flex h-full w-full gap-1 bg-neutral-500">{bars}</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full gap-1">{bars}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
25
web/components/ui/loader.tsx
Normal file
25
web/components/ui/loader.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Loader = ({ children, className = "" }: Props) => (
|
||||
<div className={`${className} animate-pulse`} role="status">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
type ItemProps = {
|
||||
height?: string;
|
||||
width?: string;
|
||||
};
|
||||
|
||||
const Item: React.FC<ItemProps> = ({ height = "auto", width = "auto" }) => (
|
||||
<div className="rounded-md bg-custom-background-80" style={{ height: height, width: width }} />
|
||||
);
|
||||
|
||||
Loader.Item = Item;
|
||||
|
||||
export { Loader };
|
||||
77
web/components/ui/markdown-to-component.tsx
Normal file
77
web/components/ui/markdown-to-component.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import React from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
interface CustomComponentProps {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
type CustomComponent = React.ComponentType<CustomComponentProps>;
|
||||
|
||||
interface Props {
|
||||
markdown: string;
|
||||
components?: {
|
||||
a?: CustomComponent;
|
||||
blockquote?: CustomComponent;
|
||||
code?: CustomComponent;
|
||||
del?: CustomComponent;
|
||||
em?: CustomComponent;
|
||||
heading?: CustomComponent;
|
||||
hr?: CustomComponent;
|
||||
image?: CustomComponent;
|
||||
inlineCode?: CustomComponent;
|
||||
link?: CustomComponent;
|
||||
list?: CustomComponent;
|
||||
listItem?: CustomComponent;
|
||||
paragraph?: CustomComponent;
|
||||
strong?: CustomComponent;
|
||||
table?: CustomComponent;
|
||||
tableCell?: CustomComponent;
|
||||
tableHead?: CustomComponent;
|
||||
tableRow?: CustomComponent;
|
||||
};
|
||||
options?: any;
|
||||
}
|
||||
|
||||
const HeadingPrimary: CustomComponent = ({ children }) => (
|
||||
<h1 className="text-lg font-semibold text-custom-text-100">{children}</h1>
|
||||
);
|
||||
|
||||
const HeadingSecondary: CustomComponent = ({ children }) => (
|
||||
<h3 className="text-base font-semibold text-custom-text-100">{children}</h3>
|
||||
);
|
||||
|
||||
const Paragraph: CustomComponent = ({ children }) => (
|
||||
<p className="text-sm text-custom-text-200">{children}</p>
|
||||
);
|
||||
|
||||
const OrderedList: CustomComponent = ({ children }) => (
|
||||
<ol className="ml-8 mb-4 list-decimal text-sm text-custom-text-200">{children}</ol>
|
||||
);
|
||||
|
||||
const UnorderedList: CustomComponent = ({ children }) => (
|
||||
<ul className="ml-8 mb-4 list-disc text-sm text-custom-text-200">{children}</ul>
|
||||
);
|
||||
|
||||
const Link: CustomComponent = ({ href, children }) => (
|
||||
<a href={href} className="underline hover:no-underline" target="_blank" rel="noopener noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
|
||||
export const MarkdownRenderer: React.FC<Props> = ({ markdown, options = {} }) => {
|
||||
const customComponents = {
|
||||
h1: HeadingPrimary,
|
||||
h3: HeadingSecondary,
|
||||
p: Paragraph,
|
||||
ol: OrderedList,
|
||||
ul: UnorderedList,
|
||||
a: Link,
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactMarkdown components={customComponents} {...options}>
|
||||
{markdown}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
};
|
||||
175
web/components/ui/multi-level-dropdown.tsx
Normal file
175
web/components/ui/multi-level-dropdown.tsx
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import { Fragment, useState } from "react";
|
||||
|
||||
// headless ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Loader } from "components/ui";
|
||||
// icons
|
||||
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
type MultiLevelDropdownProps = {
|
||||
label: string;
|
||||
options: {
|
||||
id: string;
|
||||
children?: {
|
||||
id: string;
|
||||
label: string | JSX.Element;
|
||||
value: any;
|
||||
selected?: boolean;
|
||||
element?: JSX.Element;
|
||||
}[];
|
||||
hasChildren: boolean;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
selected?: boolean;
|
||||
value: any;
|
||||
}[];
|
||||
onSelect: (value: any) => void;
|
||||
direction?: "left" | "right";
|
||||
height?: "sm" | "md" | "rg" | "lg";
|
||||
};
|
||||
|
||||
export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
||||
label,
|
||||
options,
|
||||
onSelect,
|
||||
direction = "right",
|
||||
height = "md",
|
||||
}) => {
|
||||
const [openChildFor, setOpenChildFor] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Menu.Button
|
||||
onClick={() => setOpenChildFor(null)}
|
||||
className={`group flex items-center justify-between gap-2 rounded-md border border-custom-border-200 px-3 py-1.5 text-xs shadow-sm duration-300 focus:outline-none hover:text-custom-text-100 hover:bg-custom-background-90 ${
|
||||
open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
static
|
||||
className="absolute right-0 z-10 mt-1 w-36 origin-top-right select-none rounded-md bg-custom-background-90 border border-custom-border-300 text-xs shadow-lg focus:outline-none"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<div className="relative p-1" key={option.id}>
|
||||
<Menu.Item
|
||||
as="button"
|
||||
onClick={(e: any) => {
|
||||
if (option.hasChildren) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (option.onClick) option.onClick();
|
||||
|
||||
if (openChildFor === option.id) setOpenChildFor(null);
|
||||
else setOpenChildFor(option.id);
|
||||
} else onSelect(option.value);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
{({ active }) => (
|
||||
<>
|
||||
<div
|
||||
className={`${
|
||||
active || option.selected ? "bg-custom-background-80" : ""
|
||||
} flex items-center gap-1 rounded px-1 py-1.5 text-custom-text-200 ${
|
||||
direction === "right" ? "justify-between" : ""
|
||||
}`}
|
||||
>
|
||||
{direction === "left" && option.hasChildren && (
|
||||
<ChevronLeftIcon className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
<span>{option.label}</span>
|
||||
{direction === "right" && option.hasChildren && (
|
||||
<ChevronRightIcon className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Menu.Item>
|
||||
{option.hasChildren && option.id === openChildFor && (
|
||||
<div
|
||||
className={`absolute top-0 min-w-36 whitespace-nowrap origin-top-right select-none overflow-y-scroll rounded-md bg-custom-background-90 border border-custom-border-300 shadow-lg focus:outline-none ${
|
||||
direction === "left"
|
||||
? "right-full -translate-x-1"
|
||||
: "left-full translate-x-1"
|
||||
} ${
|
||||
height === "sm"
|
||||
? "max-h-28"
|
||||
: height === "md"
|
||||
? "max-h-44"
|
||||
: height === "rg"
|
||||
? "max-h-56"
|
||||
: height === "lg"
|
||||
? "max-h-80"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{option.children ? (
|
||||
<div className="space-y-1 p-1">
|
||||
{option.children.length === 0 ? (
|
||||
<p className="text-custom-text-200 text-center px-1 py-1.5">
|
||||
No {option.label} found
|
||||
</p> //if no children found, show this message.
|
||||
) : (
|
||||
option.children.map((child) => {
|
||||
if (child.element) return child.element;
|
||||
else
|
||||
return (
|
||||
<button
|
||||
key={child.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(child.value)}
|
||||
className={`${
|
||||
child.selected ? "bg-custom-background-80" : ""
|
||||
} flex w-full items-center justify-between break-words rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80`}
|
||||
>
|
||||
{child.label}{" "}
|
||||
<CheckIcon
|
||||
className={`h-3.5 w-3.5 opacity-0 ${
|
||||
child.selected ? "opacity-100" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="p-1 space-y-2">
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
155
web/components/ui/multi-level-select.tsx
Normal file
155
web/components/ui/multi-level-select.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import React, { useState } from "react";
|
||||
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
type TSelectOption = {
|
||||
id: string;
|
||||
label: string;
|
||||
value: any;
|
||||
children?:
|
||||
| (TSelectOption & {
|
||||
children?: null;
|
||||
})[]
|
||||
| null;
|
||||
};
|
||||
|
||||
type TMultipleSelectProps = {
|
||||
options: TSelectOption[];
|
||||
selected: TSelectOption | null;
|
||||
setSelected: (value: any) => void;
|
||||
label: string;
|
||||
direction?: "left" | "right";
|
||||
};
|
||||
|
||||
export const MultiLevelSelect: React.FC<TMultipleSelectProps> = ({
|
||||
options,
|
||||
selected,
|
||||
setSelected,
|
||||
label,
|
||||
direction = "right",
|
||||
}) => {
|
||||
const [openChildFor, setOpenChildFor] = useState<TSelectOption | null>(null);
|
||||
|
||||
return (
|
||||
<div className="fixed top-16 w-72">
|
||||
<Listbox
|
||||
value={selected}
|
||||
onChange={(value) => {
|
||||
if (value?.children === null) {
|
||||
setSelected(value);
|
||||
setOpenChildFor(null);
|
||||
} else setOpenChildFor(value);
|
||||
}}
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative mt-1">
|
||||
<Listbox.Button
|
||||
onClick={() => setOpenChildFor(null)}
|
||||
className="relative w-full cursor-default rounded-lg bg-custom-background-80 py-2 pl-3 pr-10 text-left shadow-md sm:text-sm"
|
||||
>
|
||||
<span className="block truncate">{selected?.label ?? label}</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon className="h-5 w-5 text-custom-text-200" aria-hidden="true" />
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
show={open}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options
|
||||
static
|
||||
className="absolute mt-1 max-h-60 w-full rounded-md bg-custom-background-80 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<Listbox.Option
|
||||
key={option.id}
|
||||
className={
|
||||
"relative cursor-default select-none py-2 pl-10 pr-4 hover:bg-custom-background-90 hover:text-custom-text-100"
|
||||
}
|
||||
onClick={(e: any) => {
|
||||
if (option.children !== null) {
|
||||
e.preventDefault();
|
||||
setOpenChildFor(option);
|
||||
}
|
||||
if (option.id === openChildFor?.id) {
|
||||
e.preventDefault();
|
||||
setOpenChildFor(null);
|
||||
}
|
||||
}}
|
||||
value={option}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{openChildFor?.id === option.id && (
|
||||
<div
|
||||
className={`absolute h-auto max-h-72 w-72 rounded-lg border border-custom-border-200 bg-custom-background-80 ${
|
||||
direction === "right"
|
||||
? "left-full translate-x-2 rounded-tl-none shadow-md"
|
||||
: "right-full -translate-x-2 rounded-tr-none shadow-md"
|
||||
}`}
|
||||
>
|
||||
{option.children?.map((child) => (
|
||||
<Listbox.Option
|
||||
key={child.id}
|
||||
className={
|
||||
"relative cursor-default select-none py-2 pl-10 pr-4 hover:bg-custom-background-90 hover:text-custom-text-100"
|
||||
}
|
||||
as="div"
|
||||
value={child}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={`block truncate ${
|
||||
selected ? "font-medium" : "font-normal"
|
||||
}`}
|
||||
>
|
||||
{child.label}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-custom-text-200">
|
||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
|
||||
<div
|
||||
className={`absolute h-0 w-0 border-t-8 border-custom-border-200 ${
|
||||
direction === "right"
|
||||
? "top-0 left-0 -translate-x-2 border-r-8 border-b-8 border-b-transparent border-t-transparent border-l-transparent"
|
||||
: "top-0 right-0 translate-x-2 border-l-8 border-b-8 border-b-transparent border-t-transparent border-r-transparent"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className={`block truncate ${selected ? "font-medium" : "font-normal"}`}
|
||||
>
|
||||
{option.label}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-custom-text-200">
|
||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
99
web/components/ui/product-updates-modal.tsx
Normal file
99
web/components/ui/product-updates-modal.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import React from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// component
|
||||
import { MarkdownRenderer, Spinner } from "components/ui";
|
||||
// icons
|
||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||
// services
|
||||
import workspaceService from "services/workspace.service";
|
||||
// helper
|
||||
import { renderLongDateFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const ProductUpdatesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
const { data: updates } = useSWR("PRODUCT_UPDATES", () => workspaceService.getProductUpdates());
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={setIsOpen}>
|
||||
<Transition.Child
|
||||
as={React.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-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="grid place-items-center min-h-full text-center p-4">
|
||||
<Transition.Child
|
||||
as={React.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-custom-background-100 border border-custom-border-100 text-left shadow-xl transition-all grid place-items-center sm:w-full sm:max-w-2xl">
|
||||
<div className="max-h-[90vh] overflow-y-auto p-5">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="flex w-full flex-col gap-y-4 text-center sm:text-left">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="flex justify-between text-lg font-medium leading-6 text-custom-text-100"
|
||||
>
|
||||
<span>Product Updates</span>
|
||||
<span>
|
||||
<button type="button" onClick={() => setIsOpen(false)}>
|
||||
<XMarkIcon
|
||||
className="h-6 w-6 text-custom-text-200 hover:text-custom-text-100"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</Dialog.Title>
|
||||
{updates && updates.length > 0 ? (
|
||||
updates.map((item, index) => (
|
||||
<React.Fragment key={item.id}>
|
||||
<div className="flex items-center gap-3 text-xs text-custom-text-200">
|
||||
<span className="flex items-center rounded-full border border-custom-border-200 bg-custom-background-90 px-3 py-1.5 text-xs">
|
||||
{item.tag_name}
|
||||
</span>
|
||||
<span>{renderLongDateFormat(item.published_at)}</span>
|
||||
{index === 0 && (
|
||||
<span className="flex items-center rounded-full border border-custom-border-200 bg-custom-primary px-3 py-1.5 text-xs text-white">
|
||||
New
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<MarkdownRenderer markdown={item.body} />
|
||||
</React.Fragment>
|
||||
))
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
Loading...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
21
web/components/ui/profile-empty-state.tsx
Normal file
21
web/components/ui/profile-empty-state.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from "react";
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: React.ReactNode;
|
||||
image: any;
|
||||
};
|
||||
|
||||
export const ProfileEmptyState: React.FC<Props> = ({ title, description, image }) => (
|
||||
<div className={`h-full w-full mx-auto grid place-items-center p-8 `}>
|
||||
<div className="text-center flex flex-col items-center w-full">
|
||||
<div className="flex items-center justify-center h-14 w-14 rounded-full bg-custom-background-90">
|
||||
<Image src={image} width={32} alt={title} />
|
||||
</div>
|
||||
<h6 className="text-base font-semibold mt-3.5 mb-3">{title}</h6>
|
||||
{description && <p className="text-sm text-custom-text-300">{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
69
web/components/ui/progress-bar.tsx
Normal file
69
web/components/ui/progress-bar.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
maxValue?: number;
|
||||
value?: number;
|
||||
radius?: number;
|
||||
strokeWidth?: number;
|
||||
activeStrokeColor?: string;
|
||||
inactiveStrokeColor?: string;
|
||||
};
|
||||
|
||||
export const ProgressBar: React.FC<Props> = ({
|
||||
maxValue = 0,
|
||||
value = 0,
|
||||
radius = 8,
|
||||
strokeWidth = 2,
|
||||
activeStrokeColor = "#3e98c7",
|
||||
inactiveStrokeColor = "#ddd",
|
||||
}) => {
|
||||
// PIE Calc Fn
|
||||
const generatePie = (value: any) => {
|
||||
const x = radius - Math.cos((2 * Math.PI) / (100 / value)) * radius;
|
||||
const y = radius + Math.sin((2 * Math.PI) / (100 / value)) * radius;
|
||||
const long = value <= 50 ? 0 : 1;
|
||||
const d = `M${radius} ${radius} L${radius} ${0} A${radius} ${radius} 0 ${long} 1 ${y} ${x} Z`;
|
||||
|
||||
return d;
|
||||
};
|
||||
|
||||
// ---- PIE Area Calc --------
|
||||
const calculatePieValue = (numberOfBars: any) => {
|
||||
const angle = 360 / numberOfBars;
|
||||
const pieValue = Math.floor(angle / 4);
|
||||
return pieValue < 1 ? 1 : Math.floor(angle / 4);
|
||||
};
|
||||
|
||||
// ---- PIE Render Fn --------
|
||||
const renderPie = (i: any) => {
|
||||
const DIRECTION = -1;
|
||||
// Rotation Calc
|
||||
const primaryRotationAngle = (maxValue - 1) * (360 / maxValue);
|
||||
const rotationAngle =
|
||||
-1 * DIRECTION * primaryRotationAngle + i * DIRECTION * primaryRotationAngle;
|
||||
const rotationTransformation = `rotate(${rotationAngle}, ${radius}, ${radius})`;
|
||||
const pieValue = calculatePieValue(maxValue);
|
||||
const dValue = generatePie(pieValue);
|
||||
const fillColor = value > 0 && i <= value ? activeStrokeColor : inactiveStrokeColor;
|
||||
|
||||
return (
|
||||
<path
|
||||
style={{ opacity: i === 0 ? 0 : 1 }}
|
||||
key={i}
|
||||
d={dValue}
|
||||
fill={fillColor}
|
||||
transform={rotationTransformation}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// combining the Pies
|
||||
const renderOuterCircle = () => [...Array(maxValue + 1)].map((e, i) => renderPie(i));
|
||||
|
||||
return (
|
||||
<svg width={radius * 2} height={radius * 2}>
|
||||
{renderOuterCircle()}
|
||||
<circle r={radius - strokeWidth} cx={radius} cy={radius} className="progress-bar" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
65
web/components/ui/range-datepicker.tsx
Normal file
65
web/components/ui/range-datepicker.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// react-datepicker
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
// helpers
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
renderAs?: "input" | "button";
|
||||
value: Date | string | null | undefined;
|
||||
onChange: (val: string | null) => void;
|
||||
error?: boolean;
|
||||
className?: string;
|
||||
isClearable?: boolean;
|
||||
disabled?: boolean;
|
||||
startDate: string | null;
|
||||
endDate: string | null;
|
||||
selectsStart?: boolean;
|
||||
selectsEnd?: boolean;
|
||||
minDate?: Date | null | undefined;
|
||||
maxDate?: Date | null | undefined;
|
||||
};
|
||||
|
||||
export const CustomRangeDatePicker: React.FC<Props> = ({
|
||||
renderAs = "button",
|
||||
value,
|
||||
onChange,
|
||||
error = false,
|
||||
className = "",
|
||||
disabled = false,
|
||||
startDate,
|
||||
endDate,
|
||||
selectsStart = false,
|
||||
selectsEnd = false,
|
||||
minDate = null,
|
||||
maxDate = null,
|
||||
}) => (
|
||||
<DatePicker
|
||||
selected={value ? new Date(value) : null}
|
||||
onChange={(val) => {
|
||||
if (!val) onChange(null);
|
||||
else onChange(renderDateFormat(val));
|
||||
}}
|
||||
className={`${
|
||||
renderAs === "input"
|
||||
? "block px-3 py-2 text-sm focus:outline-none"
|
||||
: renderAs === "button"
|
||||
? `px-3 py-1 text-xs shadow-sm ${
|
||||
disabled ? "" : "hover:bg-custom-background-80"
|
||||
} duration-300 focus:border-custom-primary focus:outline-none focus:ring-1 focus:ring-custom-primary`
|
||||
: ""
|
||||
} ${error ? "border-red-500 bg-red-100" : ""} ${
|
||||
disabled ? "cursor-not-allowed" : "cursor-pointer"
|
||||
} w-full rounded-md border border-custom-border-200 bg-transparent caret-transparent ${className}`}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
disabled={disabled}
|
||||
selectsStart={selectsStart}
|
||||
selectsEnd={selectsEnd}
|
||||
startDate={startDate ? new Date(startDate) : new Date()}
|
||||
endDate={endDate ? new Date(endDate) : new Date()}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
shouldCloseOnSelect
|
||||
inline
|
||||
/>
|
||||
);
|
||||
23
web/components/ui/spinner.tsx
Normal file
23
web/components/ui/spinner.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import * as React from "react";
|
||||
|
||||
export const Spinner: React.FC = () => (
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="mr-2 h-8 w-8 animate-spin fill-blue-600 text-custom-text-200"
|
||||
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
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
);
|
||||
85
web/components/ui/text-area/index.tsx
Normal file
85
web/components/ui/text-area/index.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import React, { useState, useRef, useEffect } from "react";
|
||||
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
|
||||
// Updates the height of a <textarea> when the value changes.
|
||||
const useAutoSizeTextArea = (textAreaRef: HTMLTextAreaElement | null, value: any) => {
|
||||
useEffect(() => {
|
||||
if (textAreaRef) {
|
||||
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
|
||||
textAreaRef.style.height = "0px";
|
||||
const scrollHeight = textAreaRef.scrollHeight;
|
||||
|
||||
// We then set the height directly, outside of the render loop
|
||||
// Trying to set this with state or a ref will product an incorrect value.
|
||||
textAreaRef.style.height = scrollHeight + "px";
|
||||
}
|
||||
}, [textAreaRef, value]);
|
||||
};
|
||||
|
||||
export const TextArea: React.FC<Props> = ({
|
||||
id,
|
||||
label,
|
||||
className = "",
|
||||
value,
|
||||
placeholder,
|
||||
name,
|
||||
register,
|
||||
mode = "primary",
|
||||
rows,
|
||||
cols,
|
||||
disabled,
|
||||
error,
|
||||
validations,
|
||||
noPadding = false,
|
||||
onChange,
|
||||
...rest
|
||||
}) => {
|
||||
const [textareaValue, setTextareaValue] = useState(value ?? "");
|
||||
|
||||
const textAreaRef = useRef<any>(null);
|
||||
|
||||
useAutoSizeTextArea(textAreaRef.current, textareaValue);
|
||||
|
||||
return (
|
||||
<>
|
||||
{label && (
|
||||
<label htmlFor={id} className="mb-2 text-custom-text-200">
|
||||
{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={`no-scrollbar w-full bg-transparent placeholder-custom-text-400 ${
|
||||
noPadding ? "" : "px-3 py-2"
|
||||
} outline-none ${
|
||||
mode === "primary"
|
||||
? "rounded-md border border-custom-border-200"
|
||||
: mode === "transparent"
|
||||
? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-theme"
|
||||
: ""
|
||||
} ${error ? "border-red-500" : ""} ${
|
||||
error && mode === "primary" ? "bg-red-100" : ""
|
||||
} ${className}`}
|
||||
{...rest}
|
||||
/>
|
||||
{error?.message && <div className="text-sm text-red-500">{error.message}</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
13
web/components/ui/text-area/types.d.ts
vendored
Normal file
13
web/components/ui/text-area/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<"textarea"> {
|
||||
label?: string;
|
||||
value?: string | number | readonly string[];
|
||||
name: string;
|
||||
register?: UseFormRegister<any>;
|
||||
mode?: "primary" | "transparent" | "secondary" | "disabled";
|
||||
validations?: RegisterOptions;
|
||||
error?: FieldError | Merge<FieldError, FieldErrorsImpl<any>>;
|
||||
noPadding?: boolean;
|
||||
}
|
||||
43
web/components/ui/toggle-switch.tsx
Normal file
43
web/components/ui/toggle-switch.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { Switch } from "@headlessui/react";
|
||||
|
||||
type Props = {
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
label?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ToggleSwitch: React.FC<Props> = (props) => {
|
||||
const { value, onChange, label, size = "sm", disabled, className } = props;
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={value}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
className={`relative flex-shrink-0 inline-flex ${
|
||||
size === "sm" ? "h-3.5 w-6" : size === "md" ? "h-4 w-7" : "h-6 w-11"
|
||||
} flex-shrink-0 cursor-pointer rounded-full border-2 border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
value ? "bg-green-500" : "bg-custom-background-80"
|
||||
} ${className || ""}`}
|
||||
>
|
||||
<span className="sr-only">{label}</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block ${
|
||||
size === "sm" ? "h-2.5 w-2.5" : size === "md" ? "h-3 w-3" : "h-5 w-5"
|
||||
} transform rounded-full shadow ring-0 transition duration-200 ease-in-out ${
|
||||
value
|
||||
? (size === "sm"
|
||||
? "translate-x-2.5"
|
||||
: size === "md"
|
||||
? "translate-x-3"
|
||||
: "translate-x-5") + " bg-white"
|
||||
: "translate-x-0 bg-custom-background-90"
|
||||
}`}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
77
web/components/ui/tooltip.tsx
Normal file
77
web/components/ui/tooltip.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import React from "react";
|
||||
|
||||
// next-themes
|
||||
import { useTheme } from "next-themes";
|
||||
// tooltip2
|
||||
import { Tooltip2 } from "@blueprintjs/popover2";
|
||||
|
||||
type Props = {
|
||||
tooltipHeading?: string;
|
||||
tooltipContent: string | React.ReactNode;
|
||||
position?:
|
||||
| "top"
|
||||
| "right"
|
||||
| "bottom"
|
||||
| "left"
|
||||
| "auto"
|
||||
| "auto-end"
|
||||
| "auto-start"
|
||||
| "bottom-left"
|
||||
| "bottom-right"
|
||||
| "left-bottom"
|
||||
| "left-top"
|
||||
| "right-bottom"
|
||||
| "right-top"
|
||||
| "top-left"
|
||||
| "top-right";
|
||||
children: JSX.Element;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
openDelay?: number;
|
||||
closeDelay?: number;
|
||||
};
|
||||
|
||||
export const Tooltip: React.FC<Props> = ({
|
||||
tooltipHeading,
|
||||
tooltipContent,
|
||||
position = "top",
|
||||
children,
|
||||
disabled = false,
|
||||
className = "",
|
||||
openDelay = 200,
|
||||
closeDelay,
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<Tooltip2
|
||||
disabled={disabled}
|
||||
hoverOpenDelay={openDelay}
|
||||
hoverCloseDelay={closeDelay}
|
||||
content={
|
||||
<div
|
||||
className={`relative z-50 max-w-xs gap-1 rounded-md p-2 text-xs shadow-md ${
|
||||
theme === "custom"
|
||||
? "bg-custom-background-100 text-custom-text-200"
|
||||
: "bg-black text-gray-400"
|
||||
} break-words overflow-hidden ${className}`}
|
||||
>
|
||||
{tooltipHeading && (
|
||||
<h5
|
||||
className={`font-medium ${
|
||||
theme === "custom" ? "text-custom-text-100" : "text-white"
|
||||
}`}
|
||||
>
|
||||
{tooltipHeading}
|
||||
</h5>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
}
|
||||
position={position}
|
||||
renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) =>
|
||||
React.cloneElement(children, { ref: eleReference, ...tooltipProps, ...children.props })
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue