diff --git a/web/core/components/empty-state/detailed-empty-state-root.tsx b/web/core/components/empty-state/detailed-empty-state-root.tsx new file mode 100644 index 00000000000..b90503682a6 --- /dev/null +++ b/web/core/components/empty-state/detailed-empty-state-root.tsx @@ -0,0 +1,100 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react"; +import Image from "next/image"; +// ui +import { Button } from "@plane/ui/src/button"; +// utils +import { cn } from "@plane/utils"; + +type EmptyStateSize = "sm" | "md" | "lg"; + +type ButtonConfig = { + text: string; + prependIcon?: React.ReactNode; + appendIcon?: React.ReactNode; + onClick?: () => void; + disabled?: boolean; +}; + +type Props = { + title: string; + description?: string; + assetPath?: string; + size?: EmptyStateSize; + primaryButton?: ButtonConfig; + secondaryButton?: ButtonConfig; + customPrimaryButton?: React.ReactNode; + customSecondaryButton?: React.ReactNode; +}; + +const sizeClasses = { + sm: "md:min-w-[24rem] max-w-[45rem]", + md: "md:min-w-[28rem] max-w-[50rem]", + lg: "md:min-w-[30rem] max-w-[60rem]", +} as const; + +const CustomButton = ({ + config, + variant, + size, +}: { + config: ButtonConfig; + variant: "primary" | "neutral-primary"; + size: EmptyStateSize; +}) => ( + +); + +export const DetailedEmptyState: React.FC = observer((props) => { + const { + title, + description, + size = "lg", + primaryButton, + secondaryButton, + customPrimaryButton, + customSecondaryButton, + assetPath, + } = props; + + const hasButtons = primaryButton || secondaryButton || customPrimaryButton || customSecondaryButton; + + return ( +
+
+
+

{title}

+ {description &&

{description}

} +
+ + {assetPath && ( + {title} + )} + + {hasButtons && ( +
+ {/* primary button */} + {customPrimaryButton ?? + (primaryButton?.text && )} + {/* secondary button */} + {customSecondaryButton ?? + (secondaryButton?.text && ( + + ))} +
+ )} +
+
+ ); +}); diff --git a/web/core/components/empty-state/index.ts b/web/core/components/empty-state/index.ts index 57e63c1bb95..afa892f2712 100644 --- a/web/core/components/empty-state/index.ts +++ b/web/core/components/empty-state/index.ts @@ -1,3 +1,6 @@ export * from "./empty-state"; export * from "./helper"; export * from "./comic-box-button"; +export * from "./detailed-empty-state-root"; +export * from "./simple-empty-state-root"; +export * from "./section-empty-state-root"; diff --git a/web/core/components/empty-state/section-empty-state-root.tsx b/web/core/components/empty-state/section-empty-state-root.tsx new file mode 100644 index 00000000000..67c458a2075 --- /dev/null +++ b/web/core/components/empty-state/section-empty-state-root.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { FC } from "react"; + +type Props = { + icon: React.ReactNode; + title: string; + description?: string; + actionElement?: React.ReactNode; +}; + +export const SectionEmptyState: FC = (props) => { + const { title, description, icon, actionElement } = props; + return ( +
+
+
{icon}
+ {title} + {description && {description}} +
+ {actionElement && <>{actionElement}} +
+ ); +}; diff --git a/web/core/components/empty-state/simple-empty-state-root.tsx b/web/core/components/empty-state/simple-empty-state-root.tsx new file mode 100644 index 00000000000..d0ab95076a7 --- /dev/null +++ b/web/core/components/empty-state/simple-empty-state-root.tsx @@ -0,0 +1,62 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react"; +import Image from "next/image"; +// utils +import { cn } from "@plane/utils"; + +type EmptyStateSize = "sm" | "md" | "lg"; + +type Props = { + title: string; + description?: string; + assetPath?: string; + size?: EmptyStateSize; +}; + +const sizeConfig = { + sm: { + container: "size-20", + dimensions: 78, + }, + md: { + container: "size-24", + dimensions: 80, + }, + lg: { + container: "size-28", + dimensions: 96, + }, +} as const; + +const getTitleClassName = (hasDescription: boolean) => + cn("font-medium whitespace-pre-line", { + "text-sm text-custom-text-400": !hasDescription, + "text-lg text-custom-text-300": hasDescription, + }); + +export const SimpleEmptyState = observer((props: Props) => { + const { title, description, size = "sm", assetPath } = props; + + return ( +
+ {assetPath && ( +
+ {title} +
+ )} + +

{title}

+ + {description &&

{description}

} +
+ ); +}); diff --git a/web/core/hooks/use-resolved-asset-path.tsx b/web/core/hooks/use-resolved-asset-path.tsx new file mode 100644 index 00000000000..f5543d2006e --- /dev/null +++ b/web/core/hooks/use-resolved-asset-path.tsx @@ -0,0 +1,17 @@ +import { useTheme } from "next-themes"; + +type AssetPathConfig = { + basePath: string; + additionalPath?: string; + extension?: string; +}; + +export const useResolvedAssetPath = ({ basePath, additionalPath = "", extension = "webp" }: AssetPathConfig) => { + // hooks + const { resolvedTheme } = useTheme(); + + // resolved theme + const theme = resolvedTheme === "light" ? "light" : "dark"; + + return `${additionalPath && additionalPath !== "" ? `${basePath}${additionalPath}` : basePath}-${theme}.${extension}`; +};