From 6d0ee6c61ff46b33f37e38efdf1b0b0014dbd46e Mon Sep 17 00:00:00 2001 From: claude_code Date: Mon, 22 Jun 2026 02:18:31 +0300 Subject: [PATCH] feat(home): add prominent "New note" button to dashboard Add a big "New note" action to the Home screen that creates a new page and opens it. Since the home screen has no active space, the target space is resolved from the user's writable spaces (CASL Manage/Page gate, mirroring the space sidebar): created directly when there is one writable space, picked from a dropdown when there are several, hidden when there are none. Menu items are disabled while a create is in flight to avoid duplicate pages. - New component features/home/components/new-note-button.tsx - Render it at the top of pages/dashboard/home.tsx (above the carousel) - Add i18n keys "New note" / "Create in space" to en-US and ru-RU Co-Authored-By: Claude Opus 4.8 --- .../public/locales/en-US/translation.json | 2 + .../public/locales/ru-RU/translation.json | 2 + .../home/components/new-note-button.tsx | 111 ++++++++++++++++++ apps/client/src/pages/dashboard/home.tsx | 5 + 4 files changed, 120 insertions(+) create mode 100644 apps/client/src/features/home/components/new-note-button.tsx diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index fc39a8d9..34f896e0 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -119,6 +119,8 @@ "Name": "Name", "New email": "New email", "New page": "New page", + "New note": "New note", + "Create in space": "Create in space", "New password": "New password", "No group found": "No group found", "No page history saved yet.": "No page history saved yet.", diff --git a/apps/client/public/locales/ru-RU/translation.json b/apps/client/public/locales/ru-RU/translation.json index 73a68aa4..050bf5c3 100644 --- a/apps/client/public/locales/ru-RU/translation.json +++ b/apps/client/public/locales/ru-RU/translation.json @@ -119,6 +119,8 @@ "Name": "Имя", "New email": "Новый электронный адрес", "New page": "Новая страница", + "New note": "Новая заметка", + "Create in space": "Создать в пространстве", "New password": "Новый пароль", "No group found": "Группа не найдена", "No page history saved yet.": "История страниц ещё не сохранена.", diff --git a/apps/client/src/features/home/components/new-note-button.tsx b/apps/client/src/features/home/components/new-note-button.tsx new file mode 100644 index 00000000..fc838fbb --- /dev/null +++ b/apps/client/src/features/home/components/new-note-button.tsx @@ -0,0 +1,111 @@ +import { Button, Menu, Text } from "@mantine/core"; +import { IconPlus } from "@tabler/icons-react"; +import { createMongoAbility } from "@casl/ability"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts"; +import { useCreatePageMutation } from "@/features/page/queries/page-query.ts"; +import { buildPageUrl } from "@/features/page/page.utils.ts"; +import { ISpace } from "@/features/space/types/space.types.ts"; +import { + SpaceAbility, + SpaceCaslAction, + SpaceCaslSubject, +} from "@/features/space/permissions/permissions.type.ts"; +import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; +import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts"; + +// A space is writable when the member's CASL rules allow managing pages — +// mirrors the create-page gate used in the space sidebar. +function canCreatePage(space: ISpace): boolean { + const ability = createMongoAbility( + (space.membership?.permissions ?? []) as any, + ); + return ability.can(SpaceCaslAction.Manage, SpaceCaslSubject.Page); +} + +// Prominent home-screen action to create a new note (page). Because the home +// screen has no active space, the target space is resolved from the user's +// writable spaces: created directly when there is one, picked from a dropdown +// when there are several. +export default function NewNoteButton() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const createPageMutation = useCreatePageMutation(); + const { data } = useGetSpacesQuery({ limit: 100 }); + + const writableSpaces = (data?.items ?? []).filter(canCreatePage); + + const createNote = async (space: ISpace) => { + try { + // `spaceId` is accepted by the create-page endpoint but is not part of + // the shared `IPageInput` type; cast to satisfy the mutation signature. + const createdPage = await createPageMutation.mutateAsync({ + spaceId: space.id, + } as any); + navigate(buildPageUrl(space.slug, createdPage.slugId, createdPage.title)); + } catch { + // useCreatePageMutation already surfaces a red notification on error. + } + }; + + // No writable space → nothing to create in; render nothing. + if (writableSpaces.length === 0) return null; + + const isPending = createPageMutation.isPending; + + // Exactly one writable space → create directly, no picker needed. + if (writableSpaces.length === 1) { + return ( + + ); + } + + // Multiple writable spaces → pick the target space from a dropdown. + return ( + + + + + + {t("Create in space")} + {writableSpaces.map((space) => ( + + } + onClick={() => createNote(space)} + > + + {space.name} + + + ))} + + + ); +} diff --git a/apps/client/src/pages/dashboard/home.tsx b/apps/client/src/pages/dashboard/home.tsx index ede96a55..0d05bb94 100644 --- a/apps/client/src/pages/dashboard/home.tsx +++ b/apps/client/src/pages/dashboard/home.tsx @@ -1,5 +1,6 @@ import { Container, Space } from "@mantine/core"; import HomeTabs from "@/features/home/components/home-tabs"; +import NewNoteButton from "@/features/home/components/new-note-button"; import SpaceCarousel from "@/features/space/components/space-carousel.tsx"; import { getAppName } from "@/lib/config.ts"; import { Helmet } from "react-helmet-async"; @@ -16,6 +17,10 @@ export default function Home() { + + + +