diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index a1705decebf..32eb6b64f7a 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -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.", diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index 1b38b1cbe0a..c7a3da312dc 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -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.", diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index c67ceceaf2e..83b1b31db10 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -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.", diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 3b71443fa08..d34a26aaaed 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -546,6 +546,8 @@ "home": { "empty": { + "quickstart_guide": "クイックスタートガイド", + "not_right_now": "今はしない", "create_project": { "title": "プロジェクトを作成", "description": "Planeのほとんどはプロジェクトから始まります。", diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index 3bfe142ea88..f715a21ae51 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -546,6 +546,8 @@ "home": { "empty": { + "quickstart_guide": "快速入门指南", + "not_right_now": "暂时不要", "create_project": { "title": "创建项目", "description": "在Plane中,大多数事情都从项目开始。", diff --git a/web/core/components/home/home-dashboard-widgets.tsx b/web/core/components/home/home-dashboard-widgets.tsx index e391567c4db..75daa836464 100644 --- a/web/core/components/home/home-dashboard-widgets.tsx +++ b/web/core/components/home/home-dashboard-widgets.tsx @@ -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"; @@ -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 ; return (
@@ -69,6 +77,8 @@ export const DashboardWidgets = observer(() => { isModalOpen={showWidgetSettings} handleOnClose={() => toggleWidgetSettings(false)} /> + {!isWikiApp && } + {isAnyWidgetEnabled ? (
{orderedWidgets.map((key) => { diff --git a/web/core/components/home/widgets/empty-states/no-projects.tsx b/web/core/components/home/widgets/empty-states/no-projects.tsx index 08a8bdaf4ee..84d7b638bce 100644 --- a/web/core/components/home/widgets/empty-states/no-projects.tsx +++ b/web/core/components/home/widgets/empty-states/no-projects.tsx @@ -1,17 +1,21 @@ 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 @@ -19,6 +23,14 @@ export const NoProjectsEmptyState = () => { 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( @@ -31,7 +43,8 @@ export const NoProjectsEmptyState = () => { id: "create-project", title: "home.empty.create_project.title", description: "home.empty.create_project.description", - icon: , + icon: , + flag: "projects", cta: { text: "home.empty.create_project.cta", onClick: (e: React.MouseEvent) => { @@ -47,7 +60,8 @@ export const NoProjectsEmptyState = () => { id: "invite-team", title: "home.empty.invite_team.title", description: "home.empty.invite_team.description", - icon: , + icon: , + flag: "visited_members", cta: { text: "home.empty.invite_team.cta", link: `/${workspaceSlug}/settings/members`, @@ -57,7 +71,8 @@ export const NoProjectsEmptyState = () => { id: "configure-workspace", title: "home.empty.configure_workspace.title", description: "home.empty.configure_workspace.description", - icon: , + icon: , + flag: "visited_workspace", cta: { text: "home.empty.configure_workspace.cta", link: "settings", @@ -85,44 +100,94 @@ export const NoProjectsEmptyState = () => { ), + 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 ( -
- {EMPTY_STATE_DATA.map((item) => ( -
+
+
{t("home.empty.quickstart_guide")}
+ +
+
+ {EMPTY_STATE_DATA.map((item) => { + const isStateComplete = isComplete(item.flag); + return ( +
- {t(item.cta.text)} - - )} -
- ))} +
+ {item.icon} +
+

{t(item.title)}

+

{t(item.description)}

+ {isStateComplete ? ( +
+ +
+ ) : item.cta.link ? ( + { + 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)} + + ) : ( + + )} +
+ ); + })} +
); -}; +}); diff --git a/web/core/components/home/widgets/links/use-links.tsx b/web/core/components/home/widgets/links/use-links.tsx index 0cb1f6ac52a..a2ad8a0cc84 100644 --- a/web/core/components/home/widgets/links/use-links.tsx +++ b/web/core/components/home/widgets/links/use-links.tsx @@ -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"), }); diff --git a/web/core/components/home/widgets/loaders/home-loader.tsx b/web/core/components/home/widgets/loaders/home-loader.tsx new file mode 100644 index 00000000000..56d32725bf2 --- /dev/null +++ b/web/core/components/home/widgets/loaders/home-loader.tsx @@ -0,0 +1,22 @@ +"use client"; + +import range from "lodash/range"; +// ui +import { Loader } from "@plane/ui"; + +export const HomeLoader = () => ( + <> + {range(3).map((index) => ( +
+
+
+ +
+ + + +
+
+ ))} + +); diff --git a/web/core/components/home/widgets/loaders/index.ts b/web/core/components/home/widgets/loaders/index.ts index ee5286f0fbf..a0925eccdf2 100644 --- a/web/core/components/home/widgets/loaders/index.ts +++ b/web/core/components/home/widgets/loaders/index.ts @@ -1 +1,2 @@ export * from "./loader"; +export * from "./home-loader"; diff --git a/web/core/components/home/widgets/recents/index.tsx b/web/core/components/home/widgets/recents/index.tsx index 6f06432fdfc..5622a2541de 100644 --- a/web/core/components/home/widgets/recents/index.tsx +++ b/web/core/components/home/widgets/recents/index.tsx @@ -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"; @@ -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"; @@ -41,15 +38,9 @@ export const RecentActivityWidget: React.FC = observer((prop const { presetFilter, showFilterSelect = true, workspaceSlug } = props; // states const [filter, setFilter] = useState(presetFilter ?? filters[0].name); - // navigation - const pathname = usePathname(); + const { t } = useTranslation(); // ref const ref = useRef(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, @@ -81,8 +72,6 @@ export const RecentActivityWidget: React.FC = observer((prop } }; - if (loader === "loaded" && !isWikiApp && joinedProjectIds?.length === 0) return ; - if (!isLoading && recents?.length === 0) return (
diff --git a/web/core/components/stickies/layout/stickies-list.tsx b/web/core/components/stickies/layout/stickies-list.tsx index 3ee1f568ba5..47e6e669805 100644 --- a/web/core/components/stickies/layout/stickies-list.tsx +++ b/web/core/components/stickies/layout/stickies-list.tsx @@ -192,7 +192,7 @@ export const StickiesLayout = (props: TStickiesLayout) => { const columnCount = getColumnCount(containerWidth); return ( -
+
); diff --git a/web/core/store/workspace/home.ts b/web/core/store/workspace/home.ts index dc53b707b30..023767ef3d6 100644 --- a/web/core/store/workspace/home.ts +++ b/web/core/store/workspace/home.ts @@ -7,6 +7,7 @@ import { IWorkspaceLinkStore, WorkspaceLinkStore } from "./link.store"; export interface IHomeStore { // observables + loading: boolean; showWidgetSettings: boolean; widgetsMap: Record; widgets: THomeWidgetKeys[]; @@ -25,6 +26,7 @@ export interface IHomeStore { export class HomeStore implements IHomeStore { // observables showWidgetSettings = false; + loading = false; widgetsMap: Record = {}; widgets: THomeWidgetKeys[] = []; // stores @@ -35,6 +37,7 @@ export class HomeStore implements IHomeStore { constructor() { makeObservable(this, { // observables + loading: observable, showWidgetSettings: observable, widgetsMap: observable, widgets: observable, @@ -68,15 +71,18 @@ export class HomeStore implements IHomeStore { fetchWidgets = async (workspaceSlug: string) => { try { + this.loading = true; const widgets = await this.workspaceService.fetchWorkspaceWidgets(workspaceSlug); runInAction(() => { this.widgets = orderBy(Object.values(widgets), "sort_order", "desc").map((widget) => widget.key); widgets.forEach((widget) => { this.widgetsMap[widget.key] = widget; }); + this.loading = false; }); } catch (error) { console.error("Failed to fetch widgets"); + this.loading = false; throw error; } }; diff --git a/web/core/store/workspace/link.store.ts b/web/core/store/workspace/link.store.ts index a6b2cd40ecd..4a19f92c0f7 100644 --- a/web/core/store/workspace/link.store.ts +++ b/web/core/store/workspace/link.store.ts @@ -93,7 +93,6 @@ export class WorkspaceLinkStore implements IWorkspaceLinkStore { }; createLink = async (workspaceSlug: string, data: Partial) => { - console.log("hereee"); const response = await this.workspaceService.createWorkspaceLink(workspaceSlug, data); runInAction(() => {