Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: empty state revamp and loader improvement #3448

Merged
merged 16 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
39 changes: 13 additions & 26 deletions web/components/cycles/active-cycle-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Link from "next/link";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// hooks
import { useApplication, useCycle, useIssues, useProject } from "hooks/store";
import { useCycle, useIssues, useProject, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// ui
import { SingleProgressStats } from "components/core";
Expand All @@ -22,6 +22,7 @@ import {
import ProgressChart from "components/core/sidebar/progress-chart";
import { ActiveCycleProgressStats } from "components/cycles";
import { StateDropdown } from "components/dropdowns";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// icons
import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react";
// helpers
Expand All @@ -32,7 +33,7 @@ import { ICycle, TCycleGroups } from "@plane/types";
// constants
import { EIssuesStoreType } from "constants/issue";
import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys";
import { CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle";
import { CYCLE_EMPTY_STATE_DETAILS, CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle";

interface IActiveCycleDetails {
workspaceSlug: string;
Expand All @@ -43,12 +44,10 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
// props
const { workspaceSlug, projectId } = props;
// store hooks
const { currentUser } = useUser();
const {
issues: { fetchActiveCycleIssues },
} = useIssues(EIssuesStoreType.CYCLE);
const {
commandPalette: { toggleCreateCycleModal },
} = useApplication();
const {
fetchActiveCycle,
currentProjectActiveCycleId,
Expand Down Expand Up @@ -76,6 +75,9 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
: null
);

const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["active"];
const emptyStateImage = getEmptyStateImagePath("cycle", "active", currentUser?.theme.theme === "light");

if (!activeCycle && isLoading)
return (
<Loader>
Expand All @@ -85,27 +87,12 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props

if (!activeCycle)
return (
<div className="grid h-full place-items-center text-center">
<div className="space-y-2">
<div className="mx-auto flex justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="66" height="66" viewBox="0 0 66 66" fill="none">
<circle cx="34.375" cy="34.375" r="22" stroke="rgb(var(--color-text-400))" strokeLinecap="round" />
<path
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
fill="rgb(var(--color-text-400))"
/>
</svg>
</div>
<h4 className="text-sm text-custom-text-200">No active cycle</h4>
<button
type="button"
className="text-sm text-custom-primary-100 outline-none"
onClick={() => toggleCreateCycleModal(true)}
>
Create a new cycle
</button>
</div>
</div>
<EmptyState
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="sm"
/>
);

const endDate = new Date(activeCycle.end_date ?? "");
Expand Down
37 changes: 14 additions & 23 deletions web/components/cycles/cycles-board.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useApplication } from "hooks/store";
import { useUser } from "hooks/store";
// components
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// constants
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/cycle";

export interface ICyclesBoard {
cycleIds: string[];
Expand All @@ -16,7 +19,10 @@ export interface ICyclesBoard {
export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props;
// store hooks
const { commandPalette: commandPaletteStore } = useApplication();
const { currentUser } = useUser();

const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS];
const emptyStateImage = getEmptyStateImagePath("cycle", filter, currentUser?.theme.theme === "light");

return (
<>
Expand All @@ -41,27 +47,12 @@ export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
</div>
</div>
) : (
<div className="grid h-full place-items-center text-center">
<div className="space-y-2">
<div className="mx-auto flex justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="66" height="66" viewBox="0 0 66 66" fill="none">
<circle cx="34.375" cy="34.375" r="22" stroke="rgb(var(--color-text-400))" strokeLinecap="round" />
<path
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
fill="rgb(var(--color-text-400))"
/>
</svg>
</div>
<h4 className="text-sm text-custom-text-200">{filter === "all" ? "No cycles" : `No ${filter} cycles`}</h4>
<button
type="button"
className="text-sm text-custom-primary-100 outline-none"
onClick={() => commandPaletteStore.toggleCreateCycleModal(true)}
>
Create a new cycle
</button>
</div>
</div>
<EmptyState
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="sm"
/>
)}
</>
);
Expand Down
45 changes: 14 additions & 31 deletions web/components/cycles/cycles-list.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useApplication } from "hooks/store";
import { useUser } from "hooks/store";
// components
import { CyclePeekOverview, CyclesListItem } from "components/cycles";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { Loader } from "@plane/ui";
// constants
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/cycle";

export interface ICyclesList {
cycleIds: string[];
Expand All @@ -17,10 +20,10 @@ export interface ICyclesList {
export const CyclesList: FC<ICyclesList> = observer((props) => {
const { cycleIds, filter, workspaceSlug, projectId } = props;
// store hooks
const {
commandPalette: commandPaletteStore,
eventTracker: { setTrackElement },
} = useApplication();
const { currentUser } = useUser();

const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS];
const emptyStateImage = getEmptyStateImagePath("cycle", filter, currentUser?.theme.theme === "light");

return (
<>
Expand All @@ -46,32 +49,12 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
</div>
</div>
) : (
<div className="grid h-full place-items-center text-center">
<div className="space-y-2">
<div className="mx-auto flex justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="66" height="66" viewBox="0 0 66 66" fill="none">
<circle cx="34.375" cy="34.375" r="22" stroke="rgb(var(--color-text-400))" strokeLinecap="round" />
<path
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
fill="rgb(var(--color-text-400))"
/>
</svg>
</div>
<h4 className="text-sm text-custom-text-200">
{filter === "all" ? "No cycles" : `No ${filter} cycles`}
</h4>
<button
type="button"
className="text-sm text-custom-primary-100 outline-none"
onClick={() => {
setTrackElement("CYCLES_PAGE_EMPTY-STATE");
commandPaletteStore.toggleCreateCycleModal(true);
}}
>
Create a new cycle
</button>
</div>
</div>
<EmptyState
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="sm"
/>
)}
</>
) : (
Expand Down
75 changes: 75 additions & 0 deletions web/components/empty-state/comic-box-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useState } from "react";
import { Popover } from "@headlessui/react";
// popper
import { usePopper } from "react-popper";
// helper
import { getButtonStyling } from "@plane/ui";

type Props = {
label: string;
icon?: any;
title: string | undefined;
description: string | undefined;
onClick?: () => void;
disabled?: boolean;
};

export const ComicBoxButton: React.FC<Props> = (props) => {
const { label, icon, title, description, onClick, disabled = false } = props;
const [isHovered, setIsHovered] = useState(false);

const handleMouseEnter = () => {
setIsHovered(true);
};

const handleMouseLeave = () => {
setIsHovered(false);
};

const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>();
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "right-end",
modifiers: [
{
name: "offset",
options: {
offset: [0, 10],
},
},
],
});

return (
<Popover>
<Popover.Button ref={setReferenceElement} onClick={onClick} disabled={disabled}>
<div className={`flex items-center gap-2.5 ${getButtonStyling("primary", "lg", disabled)}`}>
{icon}
<span className="leading-4">{label}</span>
<span className="relative h-2 w-2">
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={`absolute bg-blue-300 right-0 z-10 h-2.5 w-2.5 animate-ping rounded-full`}
/>
<div className={`absolute bg-blue-400/40 right-0 h-1.5 w-1.5 mt-0.5 mr-0.5 rounded-full`} />
</span>
</div>
</Popover.Button>
{isHovered && (
<Popover.Panel
as="div"
className="flex flex-col rounded border border-custom-border-200 bg-custom-background-100 p-5 relative min-w-80"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
static
>
<div className="absolute w-2 h-2 bg-custom-background-100 border rounded-lb-sm border-custom-border-200 border-r-0 border-t-0 transform rotate-45 bottom-2 -left-[5px]" />
<h3 className="text-lg font-semibold w-full">{title}</h3>
<h4 className="mt-1 text-sm">{description}</h4>
</Popover.Panel>
)}
</Popover>
);
};
Loading
Loading