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:
sriram veeraghanta 2023-09-03 18:50:30 +05:30 committed by GitHub
parent 20e36194b4
commit 1e152c666c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
1022 changed files with 1475 additions and 1240 deletions

View 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
) : (
""
)}
</>
)}
</>
);
};

View 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>
);

View file

@ -0,0 +1,3 @@
export * from "./danger-button";
export * from "./primary-button";
export * from "./secondary-button";

View 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>
);

View 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
View 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;
};

View 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>
);
};

View 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>
);

View 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}
/>
);

View 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 };

View 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 };

View 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>
);
};

View 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 };

View 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
View 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;
};

View 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"> &rarr;</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 };

View 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>
);

View 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>
);

View 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>
);

View 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";

View 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>
);

View 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>
);

View 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
View 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

File diff suppressed because it is too large Load diff

View 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>
);

View 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";

View 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
View 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;
}

View 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>
);

View 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>
</>
)}
</>
);

View 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>
);
};

View 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 };

View 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>
);
};

View 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>
</>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);

View 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>
);
};

View 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
/>
);

View 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>
);

View 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
View 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;
}

View 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>
);
};

View 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 })
}
/>
);
};