Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions web/core/components/empty-state/detailed-empty-state-root.tsx
Original file line number Diff line number Diff line change
@@ -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;
}) => (
<Button
variant={variant}
size={size}
onClick={config.onClick}
prependIcon={config.prependIcon}
appendIcon={config.appendIcon}
disabled={config.disabled}
>
{config.text}
</Button>
);

export const DetailedEmptyState: React.FC<Props> = observer((props) => {
const {
title,
description,
size = "lg",
primaryButton,
secondaryButton,
customPrimaryButton,
customSecondaryButton,
assetPath,
} = props;

const hasButtons = primaryButton || secondaryButton || customPrimaryButton || customSecondaryButton;

return (
<div className="flex items-center justify-center min-h-full min-w-full overflow-y-auto py-10 md:px-20 px-5">
<div className={cn("flex flex-col gap-5", sizeClasses[size])}>
<div className="flex flex-col gap-1.5 flex-shrink">
<h3 className={cn("text-xl font-semibold", { "font-medium": !description })}>{title}</h3>
{description && <p className="text-sm">{description}</p>}
</div>

{assetPath && (
<Image src={assetPath} alt={title} width={384} height={250} layout="responsive" lazyBoundary="100%" />
)}
Comment on lines +81 to +83
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Update Image component implementation.

The current implementation has several issues:

  1. The layout prop is deprecated in Next.js Image component
  2. The alt text could be more descriptive
  3. Missing loading state handling
  4. No error boundary for image loading failures
-{assetPath && (
-  <Image src={assetPath} alt={title} width={384} height={250} layout="responsive" lazyBoundary="100%" />
-)}
+{assetPath && (
+  <div className="relative w-full aspect-[1.536] max-w-[384px] mx-auto">
+    <Image
+      src={assetPath}
+      alt={`Empty state illustration for ${title}`}
+      fill
+      className="object-contain"
+      sizes="(max-width: 384px) 100vw, 384px"
+      priority={false}
+      onError={(e) => {
+        console.error('Failed to load empty state image:', e);
+      }}
+    />
+  </div>
+)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{assetPath && (
<Image src={assetPath} alt={title} width={384} height={250} layout="responsive" lazyBoundary="100%" />
)}
{assetPath && (
<div className="relative w-full aspect-[1.536] max-w-[384px] mx-auto">
<Image
src={assetPath}
alt={`Empty state illustration for ${title}`}
fill
className="object-contain"
sizes="(max-width: 384px) 100vw, 384px"
priority={false}
onError={(e) => {
console.error('Failed to load empty state image:', e);
}}
/>
</div>
)}


{hasButtons && (
<div className="relative flex items-center justify-center gap-2 flex-shrink-0 w-full">
{/* primary button */}
{customPrimaryButton ??
(primaryButton?.text && <CustomButton config={primaryButton} variant="primary" size={size} />)}
{/* secondary button */}
{customSecondaryButton ??
(secondaryButton?.text && (
<CustomButton config={secondaryButton} variant="neutral-primary" size={size} />
))}
</div>
)}
</div>
</div>
);
});
3 changes: 3 additions & 0 deletions web/core/components/empty-state/index.ts
Original file line number Diff line number Diff line change
@@ -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";
24 changes: 24 additions & 0 deletions web/core/components/empty-state/section-empty-state-root.tsx
Original file line number Diff line number Diff line change
@@ -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> = (props) => {
const { title, description, icon, actionElement } = props;
return (
<div className="flex flex-col gap-4 items-center justify-center rounded-md border border-custom-border-200 p-10">
<div className="flex flex-col items-center gap-2">
<div className="flex items-center justify-center size-8 bg-custom-background-80 rounded">{icon}</div>
<span className="text-sm font-medium">{title}</span>
{description && <span className="text-xs text-custom-text-300">{description}</span>}
</div>
{actionElement && <>{actionElement}</>}
</div>
Comment on lines +15 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance accessibility attributes.

Add appropriate ARIA attributes to improve accessibility for screen readers.

-    <div className="flex flex-col gap-4 items-center justify-center rounded-md border border-custom-border-200 p-10">
+    <div 
+      role="region" 
+      aria-label={title}
+      className="flex flex-col gap-4 items-center justify-center rounded-md border border-custom-border-200 p-10"
+    >
       <div className="flex flex-col items-center gap-2">
-        <div className="flex items-center justify-center size-8 bg-custom-background-80 rounded">{icon}</div>
+        <div className="flex items-center justify-center size-8 bg-custom-background-80 rounded" aria-hidden="true">{icon}</div>
-        <span className="text-sm font-medium">{title}</span>
+        <h2 className="text-sm font-medium">{title}</h2>
-        {description && <span className="text-xs text-custom-text-300">{description}</span>}
+        {description && <p className="text-xs text-custom-text-300">{description}</p>}
       </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="flex flex-col gap-4 items-center justify-center rounded-md border border-custom-border-200 p-10">
<div className="flex flex-col items-center gap-2">
<div className="flex items-center justify-center size-8 bg-custom-background-80 rounded">{icon}</div>
<span className="text-sm font-medium">{title}</span>
{description && <span className="text-xs text-custom-text-300">{description}</span>}
</div>
{actionElement && <>{actionElement}</>}
</div>
<div
role="region"
aria-label={title}
className="flex flex-col gap-4 items-center justify-center rounded-md border border-custom-border-200 p-10"
>
<div className="flex flex-col items-center gap-2">
<div className="flex items-center justify-center size-8 bg-custom-background-80 rounded" aria-hidden="true">{icon}</div>
<h2 className="text-sm font-medium">{title}</h2>
{description && <p className="text-xs text-custom-text-300">{description}</p>}
</div>
{actionElement && <>{actionElement}</>}
</div>

);
};
62 changes: 62 additions & 0 deletions web/core/components/empty-state/simple-empty-state-root.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="text-center flex flex-col gap-2.5 items-center">
{assetPath && (
<div className={sizeConfig[size].container}>
<Image
src={assetPath}
alt={title}
height={sizeConfig[size].dimensions}
width={sizeConfig[size].dimensions}
layout="responsive"
lazyBoundary="100%"
/>
</div>
)}
Comment on lines +44 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Update Image component implementation.

The current Image component implementation uses deprecated props and lacks error handling.

       {assetPath && (
         <div className={sizeConfig[size].container}>
           <Image
             src={assetPath}
             alt={title}
             height={sizeConfig[size].dimensions}
             width={sizeConfig[size].dimensions}
-            layout="responsive"
+            style={{ width: '100%', height: 'auto' }}
             lazyBoundary="100%"
+            loading="lazy"
+            onError={(e) => {
+              console.error('Failed to load image:', assetPath);
+              e.currentTarget.style.display = 'none';
+            }}
           />
         </div>
       )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{assetPath && (
<div className={sizeConfig[size].container}>
<Image
src={assetPath}
alt={title}
height={sizeConfig[size].dimensions}
width={sizeConfig[size].dimensions}
layout="responsive"
lazyBoundary="100%"
/>
</div>
)}
{assetPath && (
<div className={sizeConfig[size].container}>
<Image
src={assetPath}
alt={title}
height={sizeConfig[size].dimensions}
width={sizeConfig[size].dimensions}
style={{ width: '100%', height: 'auto' }}
lazyBoundary="100%"
loading="lazy"
onError={(e) => {
console.error('Failed to load image:', assetPath);
e.currentTarget.style.display = 'none';
}}
/>
</div>
)}


<h3 className={getTitleClassName(!!description)}>{title}</h3>

{description && <p className="text-base font-medium text-custom-text-400 whitespace-pre-line">{description}</p>}
</div>
);
Comment on lines +42 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance accessibility attributes.

Add appropriate ARIA attributes and semantic HTML elements to improve accessibility.

-    <div className="text-center flex flex-col gap-2.5 items-center">
+    <div 
+      role="region" 
+      aria-label={title}
+      className="text-center flex flex-col gap-2.5 items-center"
+    >
       {assetPath && (
-        <div className={sizeConfig[size].container}>
+        <div className={sizeConfig[size].container} aria-hidden="true">
           <Image />
         </div>
       )}

-      <h3 className={getTitleClassName(!!description)}>{title}</h3>
+      <h2 className={getTitleClassName(!!description)}>{title}</h2>

-      {description && <p className="text-base font-medium text-custom-text-400 whitespace-pre-line">{description}</p>}
+      {description && (
+        <p 
+          className="text-base font-medium text-custom-text-400 whitespace-pre-line"
+          aria-describedby={`${title}-description`}
+          id={`${title}-description`}
+        >
+          {description}
+        </p>
+      )}
     </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<div className="text-center flex flex-col gap-2.5 items-center">
{assetPath && (
<div className={sizeConfig[size].container}>
<Image
src={assetPath}
alt={title}
height={sizeConfig[size].dimensions}
width={sizeConfig[size].dimensions}
layout="responsive"
lazyBoundary="100%"
/>
</div>
)}
<h3 className={getTitleClassName(!!description)}>{title}</h3>
{description && <p className="text-base font-medium text-custom-text-400 whitespace-pre-line">{description}</p>}
</div>
);
return (
<div
role="region"
aria-label={title}
className="text-center flex flex-col gap-2.5 items-center"
>
{assetPath && (
<div className={sizeConfig[size].container} aria-hidden="true">
<Image
src={assetPath}
alt={title}
height={sizeConfig[size].dimensions}
width={sizeConfig[size].dimensions}
layout="responsive"
lazyBoundary="100%"
/>
</div>
)}
<h2 className={getTitleClassName(!!description)}>{title}</h2>
{description && (
<p
className="text-base font-medium text-custom-text-400 whitespace-pre-line"
aria-describedby={`${title}-description`}
id={`${title}-description`}
>
{description}
</p>
)}
</div>
);

});
17 changes: 17 additions & 0 deletions web/core/hooks/use-resolved-asset-path.tsx
Original file line number Diff line number Diff line change
@@ -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}`;
};
Loading