Skip to content

Commit 4432be1

Browse files
[WEB-3166] chore: global empty state components (#6414)
* chore: detailed and simple empty state component added * chore: section empty state component added * chore: asset path helper hook added
1 parent 20893c6 commit 4432be1

File tree

5 files changed

+206
-0
lines changed

5 files changed

+206
-0
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"use client";
2+
3+
import React from "react";
4+
import { observer } from "mobx-react";
5+
import Image from "next/image";
6+
// ui
7+
import { Button } from "@plane/ui/src/button";
8+
// utils
9+
import { cn } from "@plane/utils";
10+
11+
type EmptyStateSize = "sm" | "md" | "lg";
12+
13+
type ButtonConfig = {
14+
text: string;
15+
prependIcon?: React.ReactNode;
16+
appendIcon?: React.ReactNode;
17+
onClick?: () => void;
18+
disabled?: boolean;
19+
};
20+
21+
type Props = {
22+
title: string;
23+
description?: string;
24+
assetPath?: string;
25+
size?: EmptyStateSize;
26+
primaryButton?: ButtonConfig;
27+
secondaryButton?: ButtonConfig;
28+
customPrimaryButton?: React.ReactNode;
29+
customSecondaryButton?: React.ReactNode;
30+
};
31+
32+
const sizeClasses = {
33+
sm: "md:min-w-[24rem] max-w-[45rem]",
34+
md: "md:min-w-[28rem] max-w-[50rem]",
35+
lg: "md:min-w-[30rem] max-w-[60rem]",
36+
} as const;
37+
38+
const CustomButton = ({
39+
config,
40+
variant,
41+
size,
42+
}: {
43+
config: ButtonConfig;
44+
variant: "primary" | "neutral-primary";
45+
size: EmptyStateSize;
46+
}) => (
47+
<Button
48+
variant={variant}
49+
size={size}
50+
onClick={config.onClick}
51+
prependIcon={config.prependIcon}
52+
appendIcon={config.appendIcon}
53+
disabled={config.disabled}
54+
>
55+
{config.text}
56+
</Button>
57+
);
58+
59+
export const DetailedEmptyState: React.FC<Props> = observer((props) => {
60+
const {
61+
title,
62+
description,
63+
size = "lg",
64+
primaryButton,
65+
secondaryButton,
66+
customPrimaryButton,
67+
customSecondaryButton,
68+
assetPath,
69+
} = props;
70+
71+
const hasButtons = primaryButton || secondaryButton || customPrimaryButton || customSecondaryButton;
72+
73+
return (
74+
<div className="flex items-center justify-center min-h-full min-w-full overflow-y-auto py-10 md:px-20 px-5">
75+
<div className={cn("flex flex-col gap-5", sizeClasses[size])}>
76+
<div className="flex flex-col gap-1.5 flex-shrink">
77+
<h3 className={cn("text-xl font-semibold", { "font-medium": !description })}>{title}</h3>
78+
{description && <p className="text-sm">{description}</p>}
79+
</div>
80+
81+
{assetPath && (
82+
<Image src={assetPath} alt={title} width={384} height={250} layout="responsive" lazyBoundary="100%" />
83+
)}
84+
85+
{hasButtons && (
86+
<div className="relative flex items-center justify-center gap-2 flex-shrink-0 w-full">
87+
{/* primary button */}
88+
{customPrimaryButton ??
89+
(primaryButton?.text && <CustomButton config={primaryButton} variant="primary" size={size} />)}
90+
{/* secondary button */}
91+
{customSecondaryButton ??
92+
(secondaryButton?.text && (
93+
<CustomButton config={secondaryButton} variant="neutral-primary" size={size} />
94+
))}
95+
</div>
96+
)}
97+
</div>
98+
</div>
99+
);
100+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
export * from "./empty-state";
22
export * from "./helper";
33
export * from "./comic-box-button";
4+
export * from "./detailed-empty-state-root";
5+
export * from "./simple-empty-state-root";
6+
export * from "./section-empty-state-root";
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"use client";
2+
3+
import { FC } from "react";
4+
5+
type Props = {
6+
icon: React.ReactNode;
7+
title: string;
8+
description?: string;
9+
actionElement?: React.ReactNode;
10+
};
11+
12+
export const SectionEmptyState: FC<Props> = (props) => {
13+
const { title, description, icon, actionElement } = props;
14+
return (
15+
<div className="flex flex-col gap-4 items-center justify-center rounded-md border border-custom-border-200 p-10">
16+
<div className="flex flex-col items-center gap-2">
17+
<div className="flex items-center justify-center size-8 bg-custom-background-80 rounded">{icon}</div>
18+
<span className="text-sm font-medium">{title}</span>
19+
{description && <span className="text-xs text-custom-text-300">{description}</span>}
20+
</div>
21+
{actionElement && <>{actionElement}</>}
22+
</div>
23+
);
24+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"use client";
2+
3+
import React from "react";
4+
import { observer } from "mobx-react";
5+
import Image from "next/image";
6+
// utils
7+
import { cn } from "@plane/utils";
8+
9+
type EmptyStateSize = "sm" | "md" | "lg";
10+
11+
type Props = {
12+
title: string;
13+
description?: string;
14+
assetPath?: string;
15+
size?: EmptyStateSize;
16+
};
17+
18+
const sizeConfig = {
19+
sm: {
20+
container: "size-20",
21+
dimensions: 78,
22+
},
23+
md: {
24+
container: "size-24",
25+
dimensions: 80,
26+
},
27+
lg: {
28+
container: "size-28",
29+
dimensions: 96,
30+
},
31+
} as const;
32+
33+
const getTitleClassName = (hasDescription: boolean) =>
34+
cn("font-medium whitespace-pre-line", {
35+
"text-sm text-custom-text-400": !hasDescription,
36+
"text-lg text-custom-text-300": hasDescription,
37+
});
38+
39+
export const SimpleEmptyState = observer((props: Props) => {
40+
const { title, description, size = "sm", assetPath } = props;
41+
42+
return (
43+
<div className="text-center flex flex-col gap-2.5 items-center">
44+
{assetPath && (
45+
<div className={sizeConfig[size].container}>
46+
<Image
47+
src={assetPath}
48+
alt={title}
49+
height={sizeConfig[size].dimensions}
50+
width={sizeConfig[size].dimensions}
51+
layout="responsive"
52+
lazyBoundary="100%"
53+
/>
54+
</div>
55+
)}
56+
57+
<h3 className={getTitleClassName(!!description)}>{title}</h3>
58+
59+
{description && <p className="text-base font-medium text-custom-text-400 whitespace-pre-line">{description}</p>}
60+
</div>
61+
);
62+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useTheme } from "next-themes";
2+
3+
type AssetPathConfig = {
4+
basePath: string;
5+
additionalPath?: string;
6+
extension?: string;
7+
};
8+
9+
export const useResolvedAssetPath = ({ basePath, additionalPath = "", extension = "webp" }: AssetPathConfig) => {
10+
// hooks
11+
const { resolvedTheme } = useTheme();
12+
13+
// resolved theme
14+
const theme = resolvedTheme === "light" ? "light" : "dark";
15+
16+
return `${additionalPath && additionalPath !== "" ? `${basePath}${additionalPath}` : basePath}-${theme}.${extension}`;
17+
};

0 commit comments

Comments
 (0)