style: new empty states (#1497)

* fix: custom colors opacity

* chore: update text colors for dark mode

* fix: dropdown text colors, datepicker bg color

* chore: update text colors

* chore: updated primary bg color

* style: new empty states added

* refactor: empty state for issues

* style: empty state for estimates

* chore: update labels, estimates and integrations empty states

* fix: custom analytics sidebar
This commit is contained in:
Aaryan Khandelwal 2023-07-12 11:45:45 +05:30 committed by GitHub
parent 82ff786666
commit 4a2057c0b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 757 additions and 792 deletions

View file

@ -61,7 +61,7 @@ export const CustomAnalytics: React.FC<Props> = ({
<AnalyticsSelectBar
control={control}
setValue={setValue}
projects={projects}
projects={projects ?? []}
params={params}
fullScreen={fullScreen}
isProjectLevel={isProjectLevel}

View file

@ -24,13 +24,13 @@ import { ContrastIcon, LayerDiagonalIcon } from "components/icons";
// helpers
import { renderShortDate } from "helpers/date-time.helper";
import { renderEmoji } from "helpers/emoji.helper";
import { truncateText } from "helpers/string.helper";
// types
import {
IAnalyticsParams,
IAnalyticsResponse,
ICurrentUserResponse,
IExportAnalyticsFormData,
IProject,
IWorkspace,
} from "types";
// fetch-keys
@ -179,7 +179,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
};
const selectedProjects =
params.project && params.project.length > 0 ? params.project : projects.map((p) => p.id);
params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id);
return (
<div
@ -207,7 +207,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
</div>
)}
</div>
<div className="h-full overflow-hidden">
<div className="h-full w-full overflow-hidden">
{fullScreen ? (
<>
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
@ -215,61 +215,62 @@ export const AnalyticsSidebar: React.FC<Props> = ({
<h4 className="font-medium">Selected Projects</h4>
<div className="space-y-6 mt-4 h-full overflow-y-auto">
{selectedProjects.map((projectId) => {
const project: IProject = projects.find((p) => p.id === projectId);
const project = projects?.find((p) => p.id === projectId);
return (
<div key={project.id}>
<div className="text-sm flex items-center gap-1">
{project.emoji ? (
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">
{renderEmoji(project.emoji)}
</span>
) : project.icon_prop ? (
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
<span
style={{ color: project.icon_prop.color }}
className="material-symbols-rounded text-lg"
>
{project.icon_prop.name}
if (project)
return (
<div key={project.id} className="w-full">
<div className="text-sm flex items-center gap-1">
{project.emoji ? (
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">
{renderEmoji(project.emoji)}
</span>
</div>
) : (
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{project?.name.charAt(0)}
</span>
)}
<h5 className="flex items-center gap-1">
<p className="break-words">{project.name}</p>
<span className="text-custom-text-200 text-xs ml-1">
({project.identifier})
</span>
</h5>
</div>
<div className="mt-4 space-y-3 pl-2">
<div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2">
<UserGroupIcon className="h-4 w-4 text-custom-text-200" />
<h6>Total members</h6>
</div>
<span className="text-custom-text-200">{project.total_members}</span>
) : project.icon_prop ? (
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
<span
style={{ color: project.icon_prop.color }}
className="material-symbols-rounded text-lg"
>
{project.icon_prop.name}
</span>
</div>
) : (
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{project?.name.charAt(0)}
</span>
)}
<h5 className="flex items-center gap-1">
<p className="break-words">{truncateText(project.name, 20)}</p>
<span className="text-custom-text-200 text-xs ml-1">
({project.identifier})
</span>
</h5>
</div>
<div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2">
<ContrastIcon height={16} width={16} />
<h6>Total cycles</h6>
<div className="mt-4 space-y-3 pl-2 w-full">
<div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2">
<UserGroupIcon className="h-4 w-4 text-custom-text-200" />
<h6>Total members</h6>
</div>
<span className="text-custom-text-200">{project.total_members}</span>
</div>
<span className="text-custom-text-200">{project.total_cycles}</span>
</div>
<div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2">
<UserGroupIcon className="h-4 w-4 text-custom-text-200" />
<h6>Total modules</h6>
<div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2">
<ContrastIcon height={16} width={16} />
<h6>Total cycles</h6>
</div>
<span className="text-custom-text-200">{project.total_cycles}</span>
</div>
<div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2">
<UserGroupIcon className="h-4 w-4 text-custom-text-200" />
<h6>Total modules</h6>
</div>
<span className="text-custom-text-200">{project.total_modules}</span>
</div>
<span className="text-custom-text-200">{project.total_modules}</span>
</div>
</div>
</div>
);
);
})}
</div>
</div>

View file

@ -29,19 +29,13 @@ import {
} from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { CreateUpdateViewModal } from "components/views";
import { CycleIssuesGanttChartView, TransferIssues, TransferIssuesModal } from "components/cycles";
import { IssueGanttChartView } from "components/issues/gantt-chart";
import { TransferIssues, TransferIssuesModal } from "components/cycles";
// ui
import { EmptySpace, EmptySpaceItem, EmptyState, PrimaryButton, Spinner } from "components/ui";
import { EmptyState, PrimaryButton, Spinner } from "components/ui";
// icons
import {
ListBulletIcon,
PlusIcon,
RectangleStackIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
// images
import emptyIssue from "public/empty-state/empty-issue.svg";
import emptyIssue from "public/empty-state/issue.svg";
// helpers
import { getStatesList } from "helpers/state.helper";
import { orderArrayBy } from "helpers/array.helper";
@ -56,7 +50,6 @@ import {
PROJECT_ISSUES_LIST_WITH_PARAMS,
STATES_LIST,
} from "constants/fetch-keys";
import { ModuleIssuesGanttChartView } from "components/modules";
type Props = {
type?: "issue" | "cycle" | "module";
@ -107,7 +100,7 @@ export const IssuesView: React.FC<Props> = ({
groupByProperty: selectedGroup,
orderBy,
filters,
isNotEmpty,
isEmpty,
setFilters,
params,
} = useIssuesView();
@ -517,7 +510,7 @@ export const IssuesView: React.FC<Props> = ({
)}
</StrictModeDroppable>
{groupedByIssues ? (
isNotEmpty ? (
!isEmpty || issueView === "kanban" || issueView === "calendar" ? (
<>
{isCompleted && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
{issueView === "list" ? (
@ -584,46 +577,20 @@ export const IssuesView: React.FC<Props> = ({
issueView === "gantt_chart" && <GanttChartView />
)}
</>
) : type === "issue" ? (
<EmptyState
type="issue"
title="Create New Issue"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
imgURL={emptyIssue}
/>
) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
<EmptySpace
title="You don't have any issue yet."
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
Icon={RectangleStackIcon}
>
<EmptySpaceItem
title="Create a new issue"
description={
<span>
Use <pre className="inline rounded bg-custom-background-80 px-2 py-1">C</pre>{" "}
shortcut to create a new issue
</span>
}
Icon={PlusIcon}
action={() => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
}}
/>
{openIssuesListModal && (
<EmptySpaceItem
title="Add an existing issue"
description="Open list"
Icon={ListBulletIcon}
action={openIssuesListModal}
/>
)}
</EmptySpace>
</div>
<EmptyState
title="Project issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
buttonText="New Issue"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
}}
/>
)
) : (
<div className="flex h-full w-full items-center justify-center">

View file

@ -19,8 +19,10 @@ import {
} from "components/cycles";
// ui
import { EmptyState, Loader } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// images
import emptyCycle from "public/empty-state/empty-cycle.svg";
import emptyCycle from "public/empty-state/cycle.svg";
// helpers
import { getDateRangeStatus } from "helpers/date-time.helper";
// types
@ -205,10 +207,17 @@ export const CyclesView: React.FC<Props> = ({ cycles, viewType }) => {
)
) : (
<EmptyState
type="cycle"
title="Create New Cycle"
description="Sprint more effectively with Cycles by confining your project to a fixed amount of time. Create new cycle now."
imgURL={emptyCycle}
title="Plan your project with cycles"
description="Cycle is a custom time period in which a team works to complete items on their backlog."
image={emptyCycle}
buttonText="New Cycle"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "q",
});
document.dispatchEvent(e);
}}
/>
)
) : viewType === "list" ? (

View file

@ -137,7 +137,7 @@ export const JiraGetImportDetail: React.FC = () => {
label={
<span>
{value && value !== "" ? (
projects.find((p) => p.id === value)?.name
projects?.find((p) => p.id === value)?.name
) : (
<span className="text-custom-text-200">Select a project</span>
)}
@ -145,7 +145,7 @@ export const JiraGetImportDetail: React.FC = () => {
}
verticalPosition="top"
>
{projects.length > 0 ? (
{projects && projects.length > 0 ? (
projects.map((project) => (
<CustomSelect.Option key={project.id} value={project.id}>
{project.name}

View file

@ -10,8 +10,10 @@ import pagesService from "services/pages.service";
import { PagesView } from "components/pages";
// ui
import { EmptyState, Loader } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// images
import emptyPage from "public/empty-state/empty-page.svg";
import emptyPage from "public/empty-state/page.svg";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
@ -51,10 +53,17 @@ export const RecentPagesList: React.FC<TPagesListProps> = ({ viewType }) => {
})
) : (
<EmptyState
type="page"
title="Create New Page"
description="Create and document issues effortlessly in one place with Plane Notes, AI-powered for ease."
imgURL={emptyPage}
title="Have your thoughts in place"
description="You can think of Pages as an AI-powered notepad."
image={emptyPage}
buttonText="New Page"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "d",
});
document.dispatchEvent(e);
}}
/>
)
) : (

View file

@ -18,8 +18,10 @@ import {
} from "components/pages";
// ui
import { EmptyState, Loader } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// images
import emptyPage from "public/empty-state/empty-page.svg";
import emptyPage from "public/empty-state/page.svg";
// types
import { IPage, TPageViewProps } from "types";
import {
@ -255,10 +257,17 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
)
) : (
<EmptyState
type="page"
title="Create New Page"
description="Create and document issues effortlessly in one place with Plane Notes, AI-powered for ease."
imgURL={emptyPage}
title="Have your thoughts in place"
description="You can think of Pages as an AI-powered notepad."
image={emptyPage}
buttonText="New Page"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "d",
});
document.dispatchEvent(e);
}}
/>
)}
</div>

View file

@ -20,7 +20,11 @@ import {
import { ExclamationIcon } from "components/icons";
// helpers
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
import { renderShortTime, renderShortDate, renderLongDateFormat } from "helpers/date-time.helper";
import {
render24HourFormatTime,
renderShortDate,
renderLongDateFormat,
} from "helpers/date-time.helper";
// types
import { IPage, IProjectMember } from "types";
@ -97,11 +101,13 @@ export const SinglePageDetailedItem: React.FC<TSingleStatProps> = ({
<div className="flex items-center gap-2">
<Tooltip
tooltipContent={`Last updated at ${
renderShortTime(page.updated_at) +
render24HourFormatTime(page.updated_at) +
` ${new Date(page.updated_at).getHours() < 12 ? "am" : "pm"}`
} on ${renderShortDate(page.updated_at)}`}
>
<p className="text-sm text-custom-text-200">{renderShortTime(page.updated_at)}</p>
<p className="text-sm text-custom-text-200">
{render24HourFormatTime(page.updated_at)}
</p>
</Tooltip>
{page.is_favorite ? (
<button

View file

@ -21,7 +21,11 @@ import {
import { ExclamationIcon } from "components/icons";
// helpers
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
import { renderLongDateFormat, renderShortDate, renderShortTime } from "helpers/date-time.helper";
import {
renderLongDateFormat,
renderShortDate,
render24HourFormatTime,
} from "helpers/date-time.helper";
// types
import { IPage, IProjectMember } from "types";
@ -98,12 +102,12 @@ export const SinglePageListItem: React.FC<TSingleStatProps> = ({
<div className="ml-2 flex flex-shrink-0">
<div className="flex items-center gap-2">
<Tooltip
tooltipContent={`Last updated at ${renderShortTime(
tooltipContent={`Last updated at ${render24HourFormatTime(
page.updated_at
)} on ${renderShortDate(page.updated_at)}`}
>
<p className="text-sm text-custom-text-200">
{renderShortTime(page.updated_at)}
{render24HourFormatTime(page.updated_at)}
</p>
</Tooltip>
{page.is_favorite ? (

View file

@ -1,77 +1,42 @@
import React from "react";
import Image from "next/image";
// ui
import { PrimaryButton } from "components/ui";
// icon
import { PlusIcon } from "@heroicons/react/24/outline";
// helper
import { capitalizeFirstLetter } from "helpers/string.helper";
type Props = {
type: "cycle" | "module" | "project" | "issue" | "view" | "page" | "estimate";
title: string;
description: React.ReactNode | string;
imgURL: string;
action?: () => void;
image: any;
buttonText: string;
buttonIcon?: any;
onClick?: () => void;
isFullScreen?: boolean;
};
export const EmptyState: React.FC<Props> = ({ type, title, description, imgURL, action }) => {
const shortcutKey = (type: string) => {
switch (type) {
case "cycle":
return "Q";
case "module":
return "M";
case "project":
return "P";
case "issue":
return "C";
case "view":
return "V";
case "page":
return "D";
default:
return null;
}
};
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-5 text-center">
<div className="h-32 w-72">
<Image src={imgURL} height="128" width="288" alt={type} />
</div>
<h3 className="text-xl font-semibold">{title}</h3>
{shortcutKey(type) && (
<span>
Use shortcut{" "}
<span className="text-custom-text-200 mx-1 rounded-sm border border-custom-border-100 bg-custom-background-90 px-2 py-1 text-sm font-medium">
{shortcutKey(type)}
</span>{" "}
to create {type} from anywhere.
</span>
)}
<p className="max-w-md text-sm text-custom-text-200">{description}</p>
<PrimaryButton
className="flex items-center gap-1"
onClick={() => {
if (action) {
action();
return;
}
if (!shortcutKey(type)) return;
const e = new KeyboardEvent("keydown", {
key: shortcutKey(type) as string,
});
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4 font-bold text-white" />
Create New {capitalizeFirstLetter(type)}
export const EmptyState: React.FC<Props> = ({
title,
description,
image,
onClick,
buttonText,
buttonIcon,
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={buttonText} />
<h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">{title}</h6>
<p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>
<PrimaryButton className="flex items-center gap-1.5" onClick={onClick}>
{buttonIcon}
{buttonText}
</PrimaryButton>
</div>
);
};
</div>
);

View file

@ -18,7 +18,7 @@ import { VIEWS_LIST } from "constants/fetch-keys";
import useToast from "hooks/use-toast";
// helpers
import { truncateText } from "helpers/string.helper";
import { renderShortDateWithYearFormat, renderShortTime } from "helpers/date-time.helper";
import { renderShortDateWithYearFormat, render24HourFormatTime } from "helpers/date-time.helper";
type Props = {
view: IView;
@ -105,12 +105,12 @@ export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDe
filters
</p>
<Tooltip
tooltipContent={`Last updated at ${renderShortTime(
tooltipContent={`Last updated at ${render24HourFormatTime(
view.updated_at
)} ${renderShortDateWithYearFormat(view.updated_at)}`}
>
<p className="text-sm text-custom-text-200">
{renderShortTime(view.updated_at)}
{render24HourFormatTime(view.updated_at)}
</p>
</Tooltip>
{view.is_favorite ? (