style: new empty state ui (#2923)

This commit is contained in:
sabith-tu 2023-11-28 18:47:52 +05:30 committed by sriram veeraghanta
parent d2a3d00e82
commit e16e468b8f
15 changed files with 252 additions and 89 deletions

View File

@ -0,0 +1,113 @@
import React, { useState } from "react";
import Image from "next/image";
// ui
import { Button } from "@plane/ui";
type Props = {
title: string;
description?: React.ReactNode;
image: any;
comicBox?: {
direction: "left" | "right";
title: string;
description: string;
extraPadding?: boolean;
};
primaryButton?: {
icon?: any;
text: string;
onClick: () => void;
};
secondaryButton?: React.ReactNode;
disabled?: boolean;
};
export const NewEmptyState: React.FC<Props> = ({
title,
description,
image,
primaryButton,
secondaryButton,
disabled = false,
comicBox,
}) => {
const [isHovered, setIsHovered] = useState(false);
const handleMouseEnter = () => {
setIsHovered(true);
};
const handleMouseLeave = () => {
setIsHovered(false);
};
return (
<div className=" flex flex-col justify-center items-center h-full w-full ">
<div className="border border-custom-border-200 rounded-xl px-10 py-7 flex flex-col gap-5 max-w-6xl m-5 md:m-16 shadow-sm">
<h3 className="font-semibold text-2xl">{title}</h3>
{description && <p className=" text-lg">{description}</p>}
<div className="relative w-full max-w-6xl">
<Image src={image} className="w-52 sm:w-60" alt={primaryButton?.text} />
</div>
<div className="flex justify-center items-start relative">
{primaryButton && (
<Button
className={`max-w-min m-3 relative !px-6 ${comicBox?.direction === "left" ? "flex-row-reverse" : ""}`}
size="lg"
variant="primary"
onClick={primaryButton.onClick}
disabled={disabled}
>
{primaryButton.text}
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={`bg-blue-300 absolute ${
comicBox?.direction === "left" ? "left-0 ml-2" : "right-0 mr-2"
} h-2.5 w-2.5 z-10 rounded-full animate-ping`}
/>
<div
className={`bg-blue-400/40 absolute ${
comicBox?.direction === "left" ? "left-0 ml-2.5" : "right-0 mr-2.5"
} h-1.5 w-1.5 rounded-full`}
/>
</Button>
)}
{comicBox &&
isHovered &&
(comicBox.direction === "right" ? (
<div
className={`flex max-w-sm absolute top-0 left-1/2 ${
comicBox?.extraPadding ? "ml-[125px]" : "ml-[90px]"
} pb-5`}
>
<div className="relative w-0 h-0 border-t-[11px] mt-5 border-custom-border-200 border-b-[11px] border-r-[11px] border-y-transparent">
<div className="absolute top-[-10px] right-[-12px] w-0 h-0 border-t-[10px] border-custom-background-100 border-b-[10px] border-r-[10px] border-y-transparent" />
</div>
<div className="border border-custom-border-200 rounded-md bg-custom-background-100">
<h1 className="p-5">
<h3 className="font-semibold text-lg">{comicBox?.title}</h3>
<h4 className="text-sm mt-1">{comicBox?.description}</h4>
</h1>
</div>
</div>
) : (
<div className="flex flex-row-reverse max-w-sm absolute top-0 right-1/2 mr-[90px] pb-5">
<div className="relative w-0 h-0 border-t-[11px] mt-5 border-custom-border-200 border-b-[11px] border-l-[11px] border-y-transparent">
<div className="absolute top-[-10px] left-[-12px] w-0 h-0 border-t-[10px] border-custom-background-100 border-b-[10px] border-l-[10px] border-y-transparent" />
</div>
<div className="border border-custom-border-200 rounded-md bg-custom-background-100">
<h1 className="p-5">
<h3 className="font-semibold text-lg">{comicBox?.title}</h3>
<h4 className="text-sm mt-1">{comicBox?.description}</h4>
</h1>
</div>
</div>
))}
</div>
</div>
</div>
);
};

View File

@ -5,8 +5,9 @@ import { useMobxStore } from "lib/mobx/store-provider";
// components
import { EmptyState } from "components/common";
// assets
import emptyIssue from "public/empty-state/issue.svg";
import emptyIssue from "public/empty-state/empty_issues.webp";
import { EProjectStore } from "store/command-palette.store";
import { NewEmptyState } from "components/common/new-empty-state";
export const ProjectEmptyState: React.FC = observer(() => {
const {
@ -16,12 +17,18 @@ export const ProjectEmptyState: React.FC = observer(() => {
return (
<div className="h-full w-full grid place-items-center">
<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."
<NewEmptyState
title="Create an issue and assign it to someone, even yourself"
description="Think of issues as jobs, tasks, work, or JTBD. Which we like.An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal."
image={emptyIssue}
comicBox={{
title: "Issues are building blocks in Plane.",
direction: "left",
description:
"Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.",
}}
primaryButton={{
text: "New issue",
text: "Create your first issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("PROJECT_EMPTY_STATE");

View File

@ -68,20 +68,23 @@ export const CycleLayoutRoot: React.FC = observer(() => {
</div>
) : (
<>
{/* <CycleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleId} /> */}
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
<CycleListLayout />
) : activeLayout === "kanban" ? (
<CycleKanBanLayout />
) : activeLayout === "calendar" ? (
<CycleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<CycleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<CycleSpreadsheetLayout />
) : null}
</div>
{Object.keys(getIssues ?? {}).length == 0 ? (
<CycleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleId} />
) : (
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
<CycleListLayout />
) : activeLayout === "kanban" ? (
<CycleKanBanLayout />
) : activeLayout === "calendar" ? (
<CycleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<CycleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<CycleSpreadsheetLayout />
) : null}
</div>
)}
</>
)}
</div>

View File

@ -53,20 +53,24 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
</div>
) : (
<>
{Object.keys(getIssues ?? {}).length == 0 ? (
<ModuleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} moduleId={moduleId} />
) : (
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ModuleListLayout />
) : activeLayout === "kanban" ? (
<ModuleKanBanLayout />
) : activeLayout === "calendar" ? (
<ModuleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<ModuleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<ModuleSpreadsheetLayout />
) : null}
</div>
)}
{/* <ModuleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} moduleId={moduleId} /> */}
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ModuleListLayout />
) : activeLayout === "kanban" ? (
<ModuleKanBanLayout />
) : activeLayout === "calendar" ? (
<ModuleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<ModuleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<ModuleSpreadsheetLayout />
) : null}
</div>
</>
)}
</div>

View File

@ -53,20 +53,23 @@ export const ProjectLayoutRoot: React.FC = observer(() => {
</div>
) : (
<>
{/* {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 && <ProjectEmptyState />} */}
<div className="w-full h-full relative overflow-auto">
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectSpreadsheetLayout />
) : null}
</div>
{Object.keys(getIssues ?? {}).length == 0 ? (
<ProjectEmptyState />
) : (
<div className="w-full h-full relative overflow-auto">
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectSpreadsheetLayout />
) : null}
</div>
)}
</>
)}
</div>

View File

@ -11,7 +11,8 @@ import { EmptyState } from "components/common";
// ui
import { Loader } from "@plane/ui";
// assets
import emptyModule from "public/empty-state/module.svg";
import emptyModule from "public/empty-state/empty_modules.webp";
import { NewEmptyState } from "components/common/new-empty-state";
export const ModulesListView: React.FC = observer(() => {
const router = useRouter();
@ -78,13 +79,19 @@ export const ModulesListView: React.FC = observer(() => {
{modulesView === "gantt_chart" && <ModulesListGanttChartView />}
</>
) : (
<EmptyState
title="Manage your project with modules"
description="Modules are smaller, focused projects that help you group and organize issues."
<NewEmptyState
title="Map your project milestones to Modules and track aggregated work easily."
description="A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone."
image={emptyModule}
comicBox={{
title: "Modules help group work by hierarchy.",
direction: "right",
description:
"A cart module, a chassis module, and a warehouse module are all good example of this grouping.",
}}
primaryButton={{
icon: <Plus className="h-4 w-4" />,
text: "New Module",
text: "Build your first module",
onClick: () => commandPaletteStore.toggleCreateModuleModal(true),
}}
/>

View File

@ -12,6 +12,8 @@ import { CompletedIssuesGraph, IssuesList, IssuesPieChart, IssuesStats } from "c
import { Button } from "@plane/ui";
// images
import emptyDashboard from "public/empty-state/dashboard.svg";
import { NewEmptyState } from "components/common/new-empty-state";
import emptyProject from "public/empty-state/dashboard_empty_project.webp";
export const WorkspaceDashboardView = observer(() => {
// router
@ -19,7 +21,12 @@ export const WorkspaceDashboardView = observer(() => {
const { workspaceSlug } = router.query;
// store
const { user: userStore, project: projectStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
const {
user: userStore,
project: projectStore,
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
} = useMobxStore();
const user = userStore.currentUser;
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
@ -65,22 +72,23 @@ export const WorkspaceDashboardView = observer(() => {
</div>
</div>
) : (
<div className="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>
<Button variant="primary" size="sm" onClick={() => {
setTrackElement("DASHBOARD_PAGE");
commandPaletteStore.toggleCreateProjectModal(true)
}
}>
Create Project
</Button>
</div>
<div className="hidden md:block self-end overflow-hidden pt-8">
<Image src={emptyDashboard} alt="Empty Dashboard" />
</div>
</div>
<NewEmptyState
image={emptyProject}
title="Overview of your projects, activity, and metrics"
description="When you have created a project and have issues assigned, you will see metrics, activity, and things you care about here. This is personalized to your role in projects, so project admins will see more than members."
comicBox={{
title: "Everything starts with a project in Plane",
direction: "right",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
}}
primaryButton={{
text: "Build your first project",
onClick: () => {
setTrackElement("DASHBOARD_PAGE");
commandPaletteStore.toggleCreateProjectModal(true);
},
}}
/>
)
) : null}
</div>

View File

@ -7,9 +7,10 @@ import { ProjectCard } from "components/project";
import { EmptyState } from "components/project/empty-state";
import { Loader } from "@plane/ui";
// images
import emptyProject from "public/empty-state/Project_full_screen.svg";
import emptyProject from "public/empty-state/empty_project.webp";
// icons
import { Plus } from "lucide-react";
import { NewEmptyState } from "components/common/new-empty-state";
export interface IProjectCardList {
workspaceSlug: string;
@ -18,7 +19,11 @@ export interface IProjectCardList {
export const ProjectCardList: FC<IProjectCardList> = observer((props) => {
const { workspaceSlug } = props;
// store
const { project: projectStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
const {
project: projectStore,
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
} = useMobxStore();
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
@ -50,17 +55,21 @@ export const ProjectCardList: FC<IProjectCardList> = observer((props) => {
)}
</div>
) : (
<EmptyState
<NewEmptyState
image={emptyProject}
title="Why no fly 😔"
description="Lets take off, capn!"
title="Start a Project"
description="Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal."
comicBox={{
title: "Everything starts with a project in Plane",
direction: "right",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
}}
primaryButton={{
icon: <Plus className="h-4 w-4" />,
text: "Start something new",
text: "Start your first project",
onClick: () => {
setTrackElement("PROJECTS_EMPTY_STATE");
commandPaletteStore.toggleCreateProjectModal(true)
}
commandPaletteStore.toggleCreateProjectModal(true);
},
}}
/>
)}

View File

@ -17,13 +17,14 @@ import emptyAnalytics from "public/empty-state/analytics.svg";
import { ANALYTICS_TABS } from "constants/analytics";
// type
import { NextPageWithLayout } from "types/app";
import { NewEmptyState } from "components/common/new-empty-state";
const AnalyticsPage: NextPageWithLayout = observer(() => {
// store
const {
project: { workspaceProjects },
commandPalette: { toggleCreateProjectModal },
trackEvent: { setTrackElement }
trackEvent: { setTrackElement },
} = useMobxStore();
return (
@ -36,10 +37,11 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
<Tab
key={tab.key}
className={({ selected }) =>
`rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${selected ? "bg-custom-background-80" : ""
`rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${
selected ? "bg-custom-background-80" : ""
}`
}
onClick={() => { }}
onClick={() => {}}
>
{tab.title}
</Tab>
@ -66,8 +68,8 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
text: "New Project",
onClick: () => {
setTrackElement("ANALYTICS_EMPTY_STATE");
toggleCreateProjectModal(true)
}
toggleCreateProjectModal(true);
},
}}
/>
</>

View File

@ -14,7 +14,7 @@ import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "componen
import { EmptyState } from "components/common";
import { Tooltip } from "@plane/ui";
// images
import emptyCycle from "public/empty-state/cycle.svg";
import emptyCycle from "public/empty-state/empty_cycles.webp";
// types
import { TCycleView, TCycleLayout } from "types";
import { NextPageWithLayout } from "types/app";
@ -22,13 +22,14 @@ import { NextPageWithLayout } from "types/app";
import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle";
// lib cookie
import { setLocalStorage, getLocalStorage } from "lib/local-storage";
import { NewEmptyState } from "components/common/new-empty-state";
// TODO: use-local-storage hook instead of lib file.
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
const [createModal, setCreateModal] = useState(false);
// store
const { project: projectStore, cycle: cycleStore } = useMobxStore();
const { projectCycles } = cycleStore
const { projectCycles } = cycleStore;
// router
const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query;
@ -73,7 +74,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
const cycleView = cycleStore?.cycleView;
const cycleLayout = cycleStore?.cycleLayout;
const totalCycles = projectCycles?.length ?? 0
const totalCycles = projectCycles?.length ?? 0;
if (!workspaceSlug || !projectId) return null;
@ -87,13 +88,19 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
/>
{totalCycles === 0 ? (
<div className="h-full grid place-items-center">
<EmptyState
title="Plan your project with cycles"
description="Cycle is a custom time period in which a team works to complete items on their backlog."
<NewEmptyState
title="Group and timebox your work in Cycles."
description="Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team."
image={emptyCycle}
comicBox={{
title: "Cycles are repetitive time-boxes.",
direction: "right",
description:
"A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.",
}}
primaryButton={{
icon: <Plus className="h-4 w-4" />,
text: "New Cycle",
text: "Set your first cycle",
onClick: () => {
setCreateModal(true);
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB