Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@

"home": {
"empty": {
"quickstart_guide": "Your quickstart guide",
"not_right_now": "Not right now",
"create_project": {
"title": "Create a project",
"description": "Most things start with a project in Plane.",
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/es/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,8 @@

"home": {
"empty": {
"quickstart_guide": "Guía de inicio rápido",
"not_right_now": "Ahora no",
"create_project": {
"title": "Crear un proyecto",
"description": "La mayoría de las cosas comienzan con un proyecto en Plane.",
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/fr/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,8 @@

"home": {
"empty": {
"quickstart_guide": "Guide de démarrage rapide",
"not_right_now": "Pas maintenant",
"create_project": {
"title": "Créer un projet",
"description": "La plupart des choses commencent par un projet dans Plane.",
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/ja/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,8 @@

"home": {
"empty": {
"quickstart_guide": "クイックスタートガイド",
"not_right_now": "今はしない",
"create_project": {
"title": "プロジェクトを作成",
"description": "Planeのほとんどはプロジェクトから始まります。",
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/zh-CN/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,8 @@

"home": {
"empty": {
"quickstart_guide": "快速入门指南",
"not_right_now": "暂时不要",
"create_project": {
"title": "创建项目",
"description": "在Plane中,大多数事情都从项目开始。",
Expand Down
18 changes: 14 additions & 4 deletions web/core/components/home/home-dashboard-widgets.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { useParams, usePathname } from "next/navigation";
// plane imports
import { useTranslation } from "@plane/i18n";
import { THomeWidgetKeys, THomeWidgetProps } from "@plane/types";
// components
import { SimpleEmptyState } from "@/components/empty-state";
// hooks
import { useProject } from "@/hooks/store";
import { useHome } from "@/hooks/store/use-home";
// plane web components
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { HomePageHeader } from "@/plane-web/components/home/header";
import { StickiesWidget } from "../stickies";
import { RecentActivityWidget } from "./widgets";
import { HomeLoader, NoProjectsEmptyState, RecentActivityWidget } from "./widgets";
import { DashboardQuickLinks } from "./widgets/links";
import { ManageWidgetsModal } from "./widgets/manage";

Expand Down Expand Up @@ -52,14 +53,21 @@ export const HOME_WIDGETS_LIST: {
export const DashboardWidgets = observer(() => {
// router
const { workspaceSlug } = useParams();
// navigation
const pathname = usePathname();
// store hooks
const { toggleWidgetSettings, widgetsMap, showWidgetSettings, orderedWidgets, isAnyWidgetEnabled, loading } =
useHome();
const { loader } = useProject();
// plane hooks
const { t } = useTranslation();
// store hooks
const { toggleWidgetSettings, widgetsMap, showWidgetSettings, orderedWidgets, isAnyWidgetEnabled } = useHome();
// derived values
const noWidgetsResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/dashboard/widgets" });

// derived values
const isWikiApp = pathname.includes(`/${workspaceSlug.toString()}/pages`);
if (!workspaceSlug) return null;
if (loading || loader !== "loaded") return <HomeLoader />;

return (
<div className="h-full w-full relative flex flex-col gap-7">
Expand All @@ -69,6 +77,8 @@ export const DashboardWidgets = observer(() => {
isModalOpen={showWidgetSettings}
handleOnClose={() => toggleWidgetSettings(false)}
/>
{!isWikiApp && <NoProjectsEmptyState />}

{isAnyWidgetEnabled ? (
<div className="flex flex-col divide-y-[1px] divide-custom-border-100">
{orderedWidgets.map((key) => {
Expand Down
135 changes: 100 additions & 35 deletions web/core/components/home/widgets/empty-states/no-projects.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import React from "react";
// mobx
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { Briefcase, Hotel, Users } from "lucide-react";
import { Briefcase, Check, Hotel, Users, X } from "lucide-react";
// plane ui
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
// helpers
import { cn } from "@plane/utils";
import { getFileURL } from "@/helpers/file.helper";
// hooks
import { useCommandPalette, useEventTracker, useUser, useUserPermissions } from "@/hooks/store";
import { useCommandPalette, useEventTracker, useProject, useUser, useUserPermissions } from "@/hooks/store";
// plane web constants

export const NoProjectsEmptyState = () => {
export const NoProjectsEmptyState = observer(() => {
// navigation
const { workspaceSlug } = useParams();
// store hooks
const { allowPermissions } = useUserPermissions();
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { data: currentUser } = useUser();
const { joinedProjectIds } = useProject();
// local storage
const { storedValue, setValue } = useLocalStorage(`quickstart-guide-${workspaceSlug}`, {
hide: false,
visited_members: false,
visited_workspace: false,
visited_profile: false,
});
const { t } = useTranslation();
// derived values
const canCreateProject = allowPermissions(
Expand All @@ -31,7 +43,8 @@ export const NoProjectsEmptyState = () => {
id: "create-project",
title: "home.empty.create_project.title",
description: "home.empty.create_project.description",
icon: <Briefcase className="w-[40px] h-[40px] text-custom-primary-100" />,
icon: <Briefcase className="size-10" />,
flag: "projects",
cta: {
text: "home.empty.create_project.cta",
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
Expand All @@ -47,7 +60,8 @@ export const NoProjectsEmptyState = () => {
id: "invite-team",
title: "home.empty.invite_team.title",
description: "home.empty.invite_team.description",
icon: <Users className="w-[40px] h-[40px] text-custom-primary-100" />,
icon: <Users className="size-10" />,
flag: "visited_members",
cta: {
text: "home.empty.invite_team.cta",
link: `/${workspaceSlug}/settings/members`,
Expand All @@ -57,7 +71,8 @@ export const NoProjectsEmptyState = () => {
id: "configure-workspace",
title: "home.empty.configure_workspace.title",
description: "home.empty.configure_workspace.description",
icon: <Hotel className="w-[40px] h-[40px] text-custom-primary-100" />,
icon: <Hotel className="size-10" />,
flag: "visited_workspace",
cta: {
text: "home.empty.configure_workspace.cta",
link: "settings",
Expand Down Expand Up @@ -85,44 +100,94 @@ export const NoProjectsEmptyState = () => {
</span>
</Link>
),
flag: "visited_profile",
cta: {
text: "home.empty.personalize_account.cta",
link: "/profile",
},
},
];
const isComplete = (type: string) => {
switch (type) {
case "projects":
return joinedProjectIds?.length > 0;
case "visited_members":
return storedValue?.visited_members;
case "visited_workspace":
return storedValue?.visited_workspace;
case "visited_profile":
return storedValue?.visited_profile;
}
};

if (storedValue?.hide) return null;

return (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
{EMPTY_STATE_DATA.map((item) => (
<div
key={item.id}
className="flex flex-col items-center justify-center p-6 bg-custom-background-100 rounded-lg text-center border border-custom-border-200/40"
<div>
<div className="flex items-center justify-between mb-4">
<div className="text-base font-semibold text-custom-text-350">{t("home.empty.quickstart_guide")}</div>
<button
className="text-custom-text-300 font-medium text-sm flex items-center gap-1"
onClick={() => {
if (!storedValue) return;
setValue({ ...storedValue, hide: true });
}}
>
<div className="grid place-items-center bg-custom-primary-100/10 rounded-full size-24 mb-3">
<span className="text-3xl my-auto">{item.icon}</span>
</div>
<h3 className="text-lg font-medium text-custom-text-100 mb-2">{t(item.title)}</h3>
<p className="text-sm text-custom-text-200 mb-4 w-[80%] flex-1">{t(item.description)}</p>

{item.cta.link ? (
<Link
href={item.cta.link}
className="text-custom-primary-100 hover:text-custom-primary-200 text-sm font-medium"
>
{t(item.cta.text)}
</Link>
) : (
<button
type="button"
className="text-custom-primary-100 hover:text-custom-primary-200 text-sm font-medium"
onClick={item.cta.onClick}
<X className="size-4" />
{t("home.empty.not_right_now")}
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
{EMPTY_STATE_DATA.map((item) => {
const isStateComplete = isComplete(item.flag);
return (
<div
key={item.id}
className="flex flex-col items-center justify-center p-6 bg-custom-background-100 rounded-lg text-center border border-custom-border-200/40"
>
{t(item.cta.text)}
</button>
)}
</div>
))}
<div
className={cn(
"grid place-items-center bg-custom-background-90 rounded-full size-20 mb-3 text-custom-text-400",
{
"text-custom-primary-100 bg-custom-primary-100/10": !isStateComplete,
}
)}
>
<span className="text-3xl my-auto">{item.icon}</span>
</div>
<h3 className="text-base font-medium text-custom-text-100 mb-2">{t(item.title)}</h3>
<p className="text-sm text-custom-text-300 mb-2">{t(item.description)}</p>
{isStateComplete ? (
<div className="flex items-center gap-2 bg-[#17a34a] rounded-full p-1">
<Check className="size-3 text-custom-primary-100 text-white" />
</div>
) : item.cta.link ? (
<Link
href={item.cta.link}
onClick={() => {
if (!storedValue) return;
setValue({
...storedValue,
[item.flag]: true,
});
}}
className="text-custom-primary-100 hover:text-custom-primary-200 text-sm font-medium"
>
{t(item.cta.text)}
</Link>
) : (
<button
type="button"
className="text-custom-primary-100 hover:text-custom-primary-200 text-sm font-medium"
onClick={item.cta.onClick}
>
{t(item.cta.text)}
</button>
)}
</div>
);
})}
</div>
</div>
);
};
});
4 changes: 2 additions & 2 deletions web/core/components/home/widgets/links/use-links.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ export const useLinks = (workspaceSlug: string) => {
});
toggleLinkModal(false);
} catch (error: any) {
console.error("error", error);
console.error("error", error?.data?.url?.error);
setToast({
message: error?.data?.error ?? t("links.toasts.not_created.message"),
message: error?.data?.url?.error ?? t("links.toasts.not_created.message"),
type: TOAST_TYPE.ERROR,
title: t("links.toasts.not_created.title"),
});
Expand Down
22 changes: 22 additions & 0 deletions web/core/components/home/widgets/loaders/home-loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import range from "lodash/range";
// ui
import { Loader } from "@plane/ui";

export const HomeLoader = () => (
<>
{range(3).map((index) => (
<div key={index}>
<div className="mb-2">
<div className="text-base font-semibold text-custom-text-350 mb-4">
<Loader.Item height="20px" width="100px" />
</div>
<Loader className="h-[110px] w-full flex items-center justify-center gap-2 text-custom-text-400 rounded">
<Loader.Item height="100%" width="100%" />
</Loader>
</div>
</div>
))}
</>
);
1 change: 1 addition & 0 deletions web/core/components/home/widgets/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./loader";
export * from "./home-loader";
15 changes: 2 additions & 13 deletions web/core/components/home/widgets/recents/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { useRef, useState } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import useSWR from "swr";
import { Briefcase, FileText } from "lucide-react";
import { useTranslation } from "@plane/i18n";
Expand All @@ -12,11 +11,9 @@ import { TActivityEntityData, THomeWidgetProps, TRecentActivityFilterKeys } from
import { LayersIcon } from "@plane/ui";
// components
import { ContentOverflowWrapper } from "@/components/core/content-overflow-HOC";
// hooks
import { useProject } from "@/hooks/store";
// plane web services
import { WorkspaceService } from "@/plane-web/services";
import { NoProjectsEmptyState, RecentsEmptyState } from "../empty-states";
import { RecentsEmptyState } from "../empty-states";
import { EWidgetKeys, WidgetLoader } from "../loaders";
import { FiltersDropdown } from "./filters";
import { RecentIssue } from "./issue";
Expand All @@ -41,15 +38,9 @@ export const RecentActivityWidget: React.FC<TRecentWidgetProps> = observer((prop
const { presetFilter, showFilterSelect = true, workspaceSlug } = props;
// states
const [filter, setFilter] = useState<TRecentActivityFilterKeys>(presetFilter ?? filters[0].name);
// navigation
const pathname = usePathname();
const { t } = useTranslation();
// ref
const ref = useRef<HTMLDivElement>(null);
// store hooks
const { joinedProjectIds, loader } = useProject();
const { t } = useTranslation();
// derived values
const isWikiApp = pathname.includes(`/${workspaceSlug.toString()}/pages`);

const { data: recents, isLoading } = useSWR(
workspaceSlug ? `WORKSPACE_RECENT_ACTIVITY_${workspaceSlug}_${filter}` : null,
Expand Down Expand Up @@ -81,8 +72,6 @@ export const RecentActivityWidget: React.FC<TRecentWidgetProps> = observer((prop
}
};

if (loader === "loaded" && !isWikiApp && joinedProjectIds?.length === 0) return <NoProjectsEmptyState />;

if (!isLoading && recents?.length === 0)
return (
<div ref={ref} className="max-h-[500px] overflow-y-scroll">
Expand Down
2 changes: 1 addition & 1 deletion web/core/components/stickies/layout/stickies-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export const StickiesLayout = (props: TStickiesLayout) => {
const columnCount = getColumnCount(containerWidth);

return (
<div ref={ref} className="size-full min-h-[500px]">
<div ref={ref} className="size-full">
<StickiesList {...props} columnCount={columnCount} />
</div>
);
Expand Down
Loading
Loading