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

@ -8,6 +8,7 @@ import useSWR from "swr";
import { useForm } from "react-hook-form";
// hooks
import useUserAuth from "hooks/use-user-auth";
import useProjects from "hooks/use-projects";
// headless ui
import { Tab } from "@headlessui/react";
// services
@ -19,6 +20,11 @@ import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { EmptyState } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// images
import emptyAnalytics from "public/empty-state/analytics.svg";
// types
import { IAnalyticsParams } from "types";
// fetch-keys
@ -38,6 +44,7 @@ const Analytics = () => {
const { workspaceSlug } = router.query;
const { user } = useUserAuth();
const { projects } = useProjects();
const { control, watch, setValue } = useForm<IAnalyticsParams>({ defaultValues });
@ -84,54 +91,60 @@ const Analytics = () => {
<BreadcrumbItem title="Workspace Analytics" />
</Breadcrumbs>
}
// right={
// <PrimaryButton
// className="flex items-center gap-2"
// onClick={() => {
// const e = new KeyboardEvent("keydown", { key: "p" });
// document.dispatchEvent(e);
// }}
// >
// <PlusIcon className="h-4 w-4" />
// Save Analytics
// </PrimaryButton>
// }
>
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
<Tab.Group as={Fragment}>
<Tab.List as="div" className="space-x-2 border-b border-custom-border-100 px-5 py-3">
{tabsList.map((tab) => (
<Tab
key={tab}
className={({ selected }) =>
`rounded-3xl border border-custom-border-100 px-4 py-2 text-xs hover:bg-custom-background-80 ${
selected ? "bg-custom-background-80" : ""
}`
}
onClick={() => trackAnalyticsEvent(tab)}
>
{tab}
</Tab>
))}
</Tab.List>
<Tab.Panels as={Fragment}>
<Tab.Panel as={Fragment}>
<ScopeAndDemand fullScreen />
</Tab.Panel>
<Tab.Panel as={Fragment}>
<CustomAnalytics
analytics={analytics}
analyticsError={analyticsError}
params={params}
control={control}
setValue={setValue}
user={user}
fullScreen
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
{projects ? (
projects.length > 0 ? (
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
<Tab.Group as={Fragment}>
<Tab.List as="div" className="space-x-2 border-b border-custom-border-100 px-5 py-3">
{tabsList.map((tab) => (
<Tab
key={tab}
className={({ selected }) =>
`rounded-3xl border border-custom-border-100 px-4 py-2 text-xs hover:bg-custom-background-80 ${
selected ? "bg-custom-background-80" : ""
}`
}
onClick={() => trackAnalyticsEvent(tab)}
>
{tab}
</Tab>
))}
</Tab.List>
<Tab.Panels as={Fragment}>
<Tab.Panel as={Fragment}>
<ScopeAndDemand fullScreen />
</Tab.Panel>
<Tab.Panel as={Fragment}>
<CustomAnalytics
analytics={analytics}
analyticsError={analyticsError}
params={params}
control={control}
setValue={setValue}
user={user}
fullScreen
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
) : (
<EmptyState
title="You can see your all projects' analytics here"
description="Let's create your first project and analyse the stats with various graphs."
image={emptyAnalytics}
buttonText="New Project"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
}}
/>
)
) : null}
</WorkspaceAuthorizationLayout>
);
};

View file

@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import Image from "next/image";
import useSWR, { mutate } from "swr";
@ -8,6 +9,9 @@ import useSWR, { mutate } from "swr";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// services
import userService from "services/user.service";
// hooks
import useUser from "hooks/use-user";
import useProjects from "hooks/use-projects";
// components
import {
CompletedIssuesGraph,
@ -15,11 +19,18 @@ import {
IssuesPieChart,
IssuesStats,
} from "components/workspace";
import { ProductUpdatesModal } from "components/ui";
// ui
import { PrimaryButton, ProductUpdatesModal } from "components/ui";
// images
import emptyDashboard from "public/empty-state/dashboard.svg";
// helpers
import { render12HourFormatTime, renderShortDate } from "helpers/date-time.helper";
// types
import type { NextPage } from "next";
// fetch-keys
import { USER_WORKSPACE_DASHBOARD } from "constants/fetch-keys";
// constants
import { DAYS } from "constants/project";
const WorkspacePage: NextPage = () => {
const [month, setMonth] = useState(new Date().getMonth() + 1);
@ -28,11 +39,18 @@ const WorkspacePage: NextPage = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { user } = useUser();
const { projects } = useProjects();
const { data: workspaceDashboardData } = useSWR(
workspaceSlug ? USER_WORKSPACE_DASHBOARD(workspaceSlug as string) : null,
workspaceSlug ? () => userService.userWorkspaceDashboard(workspaceSlug as string, month) : null
);
const today = new Date();
const greeting =
today.getHours() < 12 ? "morning" : today.getHours() < 18 ? "afternoon" : "evening";
useEffect(() => {
if (!workspaceSlug) return;
@ -47,42 +65,76 @@ const WorkspacePage: NextPage = () => {
setIsOpen={setIsProductUpdatesModalOpen}
/>
)}
<div className="p-8">
<div className="flex flex-col gap-8">
<div className="text-custom-text-200 flex flex-col justify-between gap-x-2 gap-y-6 rounded-lg border border-custom-border-100 bg-custom-background-100 px-4 py-6 md:flex-row md:items-center md:py-3">
<p className="font-semibold">
Plane is open source, support us by starring us on GitHub.
</p>
<div className="flex items-center gap-2">
<button
onClick={() => setIsProductUpdatesModalOpen(true)}
className="rounded-md border-2 border-custom-border-100 px-3 py-1.5 text-sm font-medium duration-300"
>
{`What's New?`}
</button>
<a
href="https://github.com/makeplane/plane"
target="_blank"
className="rounded-md border-2 border-custom-border-100 px-3 py-1.5 text-sm font-medium duration-300"
rel="noopener noreferrer"
>
Star us on GitHub
</a>
{projects ? (
projects.length > 0 ? (
<div className="p-8">
<div className="flex flex-col gap-8">
<div className="text-custom-text-200 flex flex-col justify-between gap-x-2 gap-y-6 rounded-lg border border-custom-border-100 bg-custom-background-100 px-4 py-6 md:flex-row md:items-center md:py-3">
<p className="font-semibold">
Plane is open source, support us by starring us on GitHub.
</p>
<div className="flex items-center gap-2">
<button
onClick={() => setIsProductUpdatesModalOpen(true)}
className="rounded-md border-2 border-custom-border-100 px-3 py-1.5 text-sm font-medium duration-300"
>
{`What's New?`}
</button>
<a
href="https://github.com/makeplane/plane"
target="_blank"
className="rounded-md border-2 border-custom-border-100 px-3 py-1.5 text-sm font-medium duration-300"
rel="noopener noreferrer"
>
Star us on GitHub
</a>
</div>
</div>
<IssuesStats data={workspaceDashboardData} />
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
<IssuesList issues={workspaceDashboardData?.overdue_issues} type="overdue" />
<IssuesList issues={workspaceDashboardData?.upcoming_issues} type="upcoming" />
<IssuesPieChart groupedIssues={workspaceDashboardData?.state_distribution} />
<CompletedIssuesGraph
issues={workspaceDashboardData?.completed_issues}
month={month}
setMonth={setMonth}
/>
</div>
</div>
</div>
<IssuesStats data={workspaceDashboardData} />
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
<IssuesList issues={workspaceDashboardData?.overdue_issues} type="overdue" />
<IssuesList issues={workspaceDashboardData?.upcoming_issues} type="upcoming" />
<IssuesPieChart groupedIssues={workspaceDashboardData?.state_distribution} />
<CompletedIssuesGraph
issues={workspaceDashboardData?.completed_issues}
month={month}
setMonth={setMonth}
/>
) : (
<div className="p-8">
<h3 className="text-2xl font-semibold">
Good {greeting}, {user?.first_name} {user?.last_name}
</h3>
<h6 className="text-custom-text-400 font-medium">
{DAYS[today.getDay()]}, {renderShortDate(today)} {render12HourFormatTime(today)}
</h6>
<div className="mt-7 bg-custom-primary-100/5 flex justify-between gap-5 md:gap-8">
<div className="p-5 md:p-8 pr-0">
<h5 className="text-xl font-semibold">Create a project</h5>
<p className="mt-2 mb-5">
Manage your projects by creating issues, cycles, modules, views and pages.
</p>
<PrimaryButton
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
}}
>
Create Project
</PrimaryButton>
</div>
<div className="hidden md:block self-end overflow-hidden pt-8">
<Image src={emptyDashboard} alt="Empty Dashboard" />
</div>
</div>
</div>
</div>
</div>
)
) : null}
</WorkspaceAuthorizationLayout>
);
};

View file

@ -5,13 +5,15 @@ import { useRouter } from "next/router";
// headless ui
import { Disclosure, Popover, Transition } from "@headlessui/react";
// icons
import { ChevronDownIcon, PlusIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/24/outline";
// images
import emptyMyIssues from "public/empty-state/my-issues.svg";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// hooks
import useIssues from "hooks/use-issues";
// ui
import { Spinner, EmptySpace, EmptySpaceItem, PrimaryButton } from "components/ui";
import { Spinner, PrimaryButton, EmptyState } from "components/ui";
import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs";
// hooks
import useMyIssuesProperties from "hooks/use-my-issues-filter";
@ -23,13 +25,14 @@ import { MyIssuesListItem } from "components/issues";
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import type { NextPage } from "next";
import useProjects from "hooks/use-projects";
const MyIssuesPage: NextPage = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
// fetching user issues
const { myIssues } = useIssues(workspaceSlug as string);
const { projects } = useProjects();
const [properties, setProperties] = useMyIssuesProperties(workspaceSlug as string);
@ -148,30 +151,39 @@ const MyIssuesPage: NextPage = () => {
)}
</Disclosure>
) : (
<div className="flex h-full w-full flex-col items-center justify-center px-4">
<EmptySpace
title="You don't have any issue assigned to you 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-gray-200 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);
}}
/>
</EmptySpace>
</div>
<EmptyState
title={
projects
? projects.length > 0
? "You don't have any issue assigned to you yet"
: "Issues assigned to you will appear here"
: ""
}
description={
projects
? projects.length > 0
? "Keep track of your work in a single place."
: "Let's create your first project and add issues that you want to accomplish."
: ""
}
image={emptyMyIssues}
buttonText={projects ? (projects.length > 0 ? "New Issue" : "New Project") : ""}
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
let e: KeyboardEvent;
if (projects && projects.length > 0)
e = new KeyboardEvent("keydown", {
key: "c",
});
else
e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
}}
/>
)}
</>
) : (

View file

@ -10,7 +10,6 @@ import { Tab } from "@headlessui/react";
import useLocalStorage from "hooks/use-local-storage";
import useUserAuth from "hooks/use-user-auth";
// services
import cycleService from "services/cycles.service";
import projectService from "services/project.service";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";

View file

@ -21,9 +21,9 @@ import {
import { EmptyState, Loader, PrimaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { ChartBarIcon, PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import { PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
// images
import emptyModule from "public/empty-state/empty-module.svg";
import emptyModule from "public/empty-state/module.svg";
// types
import { IModule, SelectModuleType } from "types/modules";
import type { NextPage } from "next";
@ -141,10 +141,17 @@ const ProjectModules: NextPage = () => {
</div>
) : (
<EmptyState
type="module"
title="Create New Module"
description="Modules are smaller, focused projects that help you group and organize issues within a specific time frame."
imgURL={emptyModule}
title="Manage your project with modules"
description="Modules are smaller, focused projects that help you group and organize issues."
image={emptyModule}
buttonText="New Module"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "m",
});
document.dispatchEvent(e);
}}
/>
)
) : (

View file

@ -41,7 +41,7 @@ import {
} from "@heroicons/react/24/outline";
import { ColorPalletteIcon, ClipboardIcon } from "components/icons";
// helpers
import { renderShortTime, renderShortDate } from "helpers/date-time.helper";
import { render24HourFormatTime, renderShortDate } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
import { orderArrayBy } from "helpers/array.helper";
// types
@ -397,11 +397,11 @@ const SinglePage: NextPage = () => {
<div className="flex items-center">
<div className="flex items-center gap-6 text-custom-text-200">
<Tooltip
tooltipContent={`Last updated at ${renderShortTime(
tooltipContent={`Last updated at ${render24HourFormatTime(
pageDetails.updated_at
)} on ${renderShortDate(pageDetails.updated_at)}`}
>
<p className="text-sm">{renderShortTime(pageDetails.updated_at)}</p>
<p className="text-sm">{render24HourFormatTime(pageDetails.updated_at)}</p>
</Tooltip>
<button className="flex items-center gap-2" onClick={handleCopyText}>
<LinkIcon className="h-4 w-4" />

View file

@ -23,7 +23,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// images
import emptyEstimate from "public/empty-state/empty-estimate.svg";
import emptyEstimate from "public/empty-state/estimate.svg";
// types
import { IEstimate, IProject } from "types";
import type { NextPage } from "next";
@ -158,13 +158,14 @@ const EstimatesSettings: NextPage = () => {
))}
</section>
) : (
<div className="grid h-full w-full place-items-center">
<div className="h-full w-full overflow-y-auto">
<EmptyState
type="estimate"
title="Create New Estimate"
description="Estimates help you communicate the complexity of an issue. You can create your own estimate and communicate with your team."
imgURL={emptyEstimate}
action={() => {
title="No estimates yet"
description="Estimates help you communicate the complexity of an issue."
image={emptyEstimate}
buttonText="Add Estimate"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
setEstimateToUpdate(undefined);
setEstimateFormOpen(true);
}}

View file

@ -12,15 +12,12 @@ import projectService from "services/project.service";
// components
import { SettingsHeader, SingleIntegration } from "components/project";
// ui
import {
EmptySpace,
EmptySpaceItem,
IntegrationAndImportExportBanner,
Loader,
} from "components/ui";
import { EmptyState, IntegrationAndImportExportBanner, Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon, PuzzlePieceIcon } from "@heroicons/react/24/outline";
// images
import emptyIntegration from "public/empty-state/integration.svg";
// types
import { IProject } from "types";
import type { NextPage } from "next";
@ -74,21 +71,13 @@ const ProjectIntegrations: NextPage = () => {
</div>
</section>
) : (
<div className="grid h-full w-full place-items-center">
<EmptySpace
title="You haven't added any integration yet."
description="Add GitHub and other integrations to sync your project issues."
Icon={PuzzlePieceIcon}
>
<EmptySpaceItem
title="Add new integration"
Icon={PlusIcon}
action={() => {
router.push(`/${workspaceSlug}/settings/integrations`);
}}
/>
</EmptySpace>
</div>
<EmptyState
title="You haven't configured integrations"
description="Configure GitHub and other integrations to sync your project issues."
image={emptyIntegration}
buttonText="Configure now"
onClick={() => router.push(`/${workspaceSlug}/settings/integrations`)}
/>
)
) : (
<Loader className="space-y-5">

View file

@ -21,10 +21,12 @@ import {
} from "components/labels";
import { SettingsHeader } from "components/project";
// ui
import { Loader, PrimaryButton } from "components/ui";
import { EmptyState, Loader, PrimaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// images
import emptyLabel from "public/empty-state/label.svg";
// types
import { IIssueLabels } from "types";
import type { NextPage } from "next";
@ -133,16 +135,33 @@ const LabelsSettings: NextPage = () => {
)}
<>
{issueLabels ? (
issueLabels.map((label) => {
const children = issueLabels?.filter((l) => l.parent === label.id);
issueLabels.length > 0 ? (
issueLabels.map((label) => {
const children = issueLabels?.filter((l) => l.parent === label.id);
if (children && children.length === 0) {
if (!label.parent)
if (children && children.length === 0) {
if (!label.parent)
return (
<SingleLabel
key={label.id}
label={label}
addLabelToGroup={() => addLabelToGroup(label)}
editLabel={(label) => {
editLabel(label);
scrollToRef.current?.scrollIntoView({
behavior: "smooth",
});
}}
handleLabelDelete={() => setSelectDeleteLabel(label)}
/>
);
} else
return (
<SingleLabel
<SingleLabelGroup
key={label.id}
label={label}
addLabelToGroup={() => addLabelToGroup(label)}
labelChildren={children}
addLabelToGroup={addLabelToGroup}
editLabel={(label) => {
editLabel(label);
scrollToRef.current?.scrollIntoView({
@ -150,26 +169,20 @@ const LabelsSettings: NextPage = () => {
});
}}
handleLabelDelete={() => setSelectDeleteLabel(label)}
user={user}
/>
);
} else
return (
<SingleLabelGroup
key={label.id}
label={label}
labelChildren={children}
addLabelToGroup={addLabelToGroup}
editLabel={(label) => {
editLabel(label);
scrollToRef.current?.scrollIntoView({
behavior: "smooth",
});
}}
handleLabelDelete={() => setSelectDeleteLabel(label)}
user={user}
/>
);
})
})
) : (
<EmptyState
title="No labels yet"
description="Create labels to help organize and filter issues in you project"
image={emptyLabel}
buttonText="Add label"
onClick={newLabel}
isFullScreen={false}
/>
)
) : (
<Loader className="space-y-5">
<Loader.Item height="40px" />

View file

@ -16,7 +16,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
//icons
import { PlusIcon } from "components/icons";
// images
import emptyView from "public/empty-state/empty-view.svg";
import emptyView from "public/empty-state/view.svg";
// fetching keys
import { PROJECT_DETAILS, VIEWS_LIST } from "constants/fetch-keys";
// components
@ -115,10 +115,17 @@ const ProjectViews: NextPage = () => {
</div>
) : (
<EmptyState
type="view"
title="Create New View"
title="Get focused with views"
description="Views aid in saving your issues by applying various filters and grouping options."
imgURL={emptyView}
image={emptyView}
buttonText="New View"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "v",
});
document.dispatchEvent(e);
}}
/>
)
) : (

View file

@ -16,12 +16,12 @@ import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { JoinProjectModal } from "components/project/join-project-modal";
import { DeleteProjectModal, SingleProjectCard } from "components/project";
// ui
import { Loader, EmptyState, PrimaryButton } from "components/ui";
import { EmptyState, Loader, PrimaryButton } from "components/ui";
import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// images
import emptyProject from "public/empty-state/empty-project.svg";
import emptyProject from "public/empty-state/project.svg";
// types
import type { NextPage } from "next";
// fetch-keys
@ -88,16 +88,7 @@ const ProjectsPage: NextPage = () => {
/>
{projects ? (
<div className="h-full w-full overflow-hidden">
{projects.length === 0 ? (
<div className="h-full w-full grid place-items-center p-8">
<EmptyState
type="project"
title="Create New Project"
description="Projects are a collection of issues. They can be used to represent the development work for a product, project, or service."
imgURL={emptyProject}
/>
</div>
) : (
{projects.length > 0 ? (
<div className="h-full p-8 overflow-y-auto">
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
{projects.map((project) => (
@ -110,6 +101,20 @@ const ProjectsPage: NextPage = () => {
))}
</div>
</div>
) : (
<EmptyState
image={emptyProject}
title="No projects yet"
description="Get started by creating your first project"
buttonText="New Project"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
}}
/>
)}
</div>
) : (