Skip to content

feat: page creation and content edit#352

Merged
hmbanan666 merged 2 commits into
mainfrom
pages
Mar 19, 2025
Merged

feat: page creation and content edit#352
hmbanan666 merged 2 commits into
mainfrom
pages

Conversation

@hmbanan666

@hmbanan666 hmbanan666 commented Mar 19, 2025

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Introduced comprehensive page management for creating, updating, and deleting pages via new modals and forms.
    • Enhanced command center navigation with a dedicated pages menu item.
    • Added new components for displaying and managing pages, including PageCard, PageCreateCard, UpdatePage, and DeletePage.
    • Expanded localization support with new translations and notifications in English, Georgian, and Russian.
  • Style

    • Improved component spacing and layout margins for a more consistent and responsive user interface.

@hmbanan666 hmbanan666 self-assigned this Mar 19, 2025
@coderabbitai

coderabbitai Bot commented Mar 19, 2025

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

Walkthrough

This update implements comprehensive page management enhancements across the application. The changes introduce new Vue components for displaying, creating, updating, and deleting pages in the command center along with dynamic routing for page display. UI refinements were applied to existing components' styling, and the Tiptap editor was updated to accept external content and emit change events. On the backend, new API endpoints and database functions handle CRUD operations for pages, and the store now maintains a reactive pages array. Additionally, localization and helper utilities were extended to support the new functionality.

Changes

File(s) Change Summary
apps/web-app/app/components/Checkout/Header.vue, apps/web-app/app/components/Footer.vue, apps/web-app/app/layouts/default.vue Adjusted classes: modified Header padding (from md:px-4 to sm:px-6 with added gap-1.5), removed the color="info" attribute in Footer, and added mx-0 to the layout container.
apps/web-app/app/components/TiptapEditor.vue Added a content prop and change event emitter; updated editor initialization to use dynamic content; simplified extensions; updated styling via new class attributes.
apps/web-app/app/components/CommandCenter/Navigation.vue Added a new menu item linking to /command-center/page with defined label, to, and icon properties.
apps/web-app/app/components/CommandCenter/PageCard.vue, apps/web-app/app/components/CommandCenter/PageCreateCard.vue, apps/web-app/app/components/Form/{CreatePage.vue, DeletePage.vue, UpdatePage.vue}, apps/web-app/app/components/Modal/{CreatePage.vue, UpdatePage.vue} Introduced new Vue components for page management, including card displays and form/modal interfaces for creating, updating, and deleting pages.
apps/web-app/app/pages/[[page]].vue, apps/web-app/app/pages/command-center/page/[pageId]/index.vue, apps/web-app/app/pages/command-center/page/index.vue Added new page components for dynamic page rendering, command center page editing, and listing with integrated editors and modals.
apps/web-app/app/utils/helpers.ts Extended the getLinkIconsForSelect function by adding two new icon options: { value: 'i-lucide-gift', ... } and { value: 'i-lucide-info', ... }.
apps/web-app/i18n/locales/en-US.json, apps/web-app/i18n/locales/ka-GE.json, apps/web-app/i18n/locales/ru-RU.json Added new localization key-value pairs for pages, saving actions, and toast messages (e.g., "Pages", "Create new Page", "Page created!", "Page updated!", "Page deleted!").
apps/web-app/stores/channel.ts Introduced a reactive pages array and updated the update function to assign data.pages, making page data available via the channel store.
packages/core/server/api/channel/index.get.ts, packages/core/server/api/page/[id].delete.ts, packages/core/server/api/page/[id].patch.ts, packages/core/server/api/page/index.post.ts Added API endpoints to retrieve (via channel keys), create, update, and delete pages with appropriate error handling and channel update calls.
packages/core/server/middleware/01.permissions.ts Expanded the protected routes to include /api/page for POST, PATCH, and DELETE methods with permissions ['MANAGE_OPTIONS', 'MASTER'].
packages/core/server/services/db/index.ts, packages/core/server/services/db/page.ts Updated getKeys to include a new pageKeys filter and introduced new CRUD functions (getPage, getPages, patchPage, createPage, deletePage) for pages.
packages/core/shared/services/page.ts, packages/core/types/index.d.ts Added Zod schemas and types for page creation and updating, and introduced a new global Page interface with properties such as id, slug, title, and content.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CreatePageComp as CreatePage Component
  participant API as POST /api/page
  participant DB as createPage() in db/page.ts
  participant Channel as Channel Store
  
  User->>CreatePageComp: Fill in page creation form
  CreatePageComp->>API: Send POST request with form data
  API->>DB: Validate data and create new page
  DB-->>API: Return created page info
  API->>Channel: Call setChannelAsUpdated (update pages)
  API-->>CreatePageComp: Send success response
  CreatePageComp->>User: Display success toast
Loading

Poem

I'm a hopping rabbit coding with glee,
New pages emerge for all eyes to see.
Creating, updating, deleting with care,
Each component and API now sings in the air.
From modals to forms, let innovation unfurl—
A joyous leap in our digital world!
🐇✨

Tip

⚡🧪 Multi-step agentic review comment chat (experimental)
  • We're introducing multi-step agentic chat in review comments. This experimental feature enhances review discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments.
    - To enable this feature, set early_access to true under in the settings.

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4865400 and 01efd96.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • pnpm-workspace.yaml (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@sonarqubecloud

Copy link
Copy Markdown

@hmbanan666 hmbanan666 merged commit 1880c8c into main Mar 19, 2025
@hmbanan666 hmbanan666 deleted the pages branch March 19, 2025 18:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (19)
apps/web-app/app/components/CommandCenter/Navigation.vue (1)

74-78: Icon naming pattern differs from other menu items.

You've added a new menu item for pages using the icon i-lucide-layers-2, while other menu items use the food: prefix (like food:dashboard, food:cooking, etc.). Consider using a consistent naming pattern for better maintainability, unless this is an intentional distinction.

-    icon: 'i-lucide-layers-2',
+    icon: 'food:pages',
apps/web-app/app/components/CommandCenter/PageCreateCard.vue (1)

1-11: Good UI design for create card component

The component has good visual styling with appropriate spacing, icons, and button configuration. The dashed border effectively communicates the "create new" affordance to users.

One improvement to consider:

-  <div class="flex flex-col gap-4 justify-center items-center h-full min-h-56 border-2 border-(--ui-border) border-dashed rounded-xl">
+  <div class="flex flex-col gap-4 justify-center items-center h-full min-h-56 border-2 border-(--ui-border) border-dashed rounded-xl cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">

Adding cursor-pointer and hover effects would enhance the interactive feel of this component.

packages/core/types/index.d.ts (1)

244-251: Well-defined Page interface

The Page interface is properly structured with appropriate types and follows the same pattern as other entities in the application. The use of LocaleValue array for title supports multilingual content.

Consider if additional fields might be needed for future requirements such as:

  • authorId or createdBy for tracking who created/modified the page
  • isActive or status field for controlling visibility
  • metaTitle and metaDescription for SEO
apps/web-app/app/components/Form/CreatePage.vue (1)

76-93: API error handling could be improved

While you're handling errors by showing a toast, consider providing more specific error messages based on the error type. This would help users understand what went wrong and how to fix it.

  } catch (error) {
    console.error(error)
-   actionToast.error()
+   if (error.response?.status === 409) {
+     actionToast.error(t('toast.page-already-exists'))
+   } else if (error.response?.data?.message) {
+     actionToast.error(error.response.data.message)
+   } else {
+     actionToast.error(t('toast.error-creating-page'))
+   }
  }
apps/web-app/app/components/Modal/UpdatePage.vue (1)

29-33: The closeAll function might be too broad

The closeAll function closes all overlays in the application, which might include unrelated ones. Consider closing only the current modal.

function closeAll() {
-  for (const o of overlay.overlays) {
-    overlay.close(o.id)
-  }
+  // Close only the current modal
+  if (overlay.current) {
+    overlay.close(overlay.current.id)
+  }
}
apps/web-app/stores/channel.ts (1)

153-153: Consider adding helper methods for page operations

While you've added the pages array to the returned store object, consider adding helper methods for common page operations similar to other entities in the store.

Consider adding methods like:

function getPage(id: string): ComputedRef<Page | undefined> {
  return computed(() => pages.value.find((page) => page.id === id))
}

function getPageBySlug(slug: string): ComputedRef<Page | undefined> {
  return computed(() => pages.value.find((page) => page.slug === slug))
}

This would make it easier to retrieve specific pages throughout the application and maintain consistency with other entity access patterns.

packages/core/shared/services/page.ts (1)

4-9: Consider enhancing slug validation for better data integrity

The slug validation currently only checks for length (2-50 characters), which may allow invalid characters or formats. Typical slug fields should:

  • Contain only lowercase alphanumeric characters, hyphens, or underscores
  • Not start or end with hyphens or underscores
  • Not contain consecutive hyphens or underscores
- slug: z.string().min(2).max(50),
+ slug: z.string().min(2).max(50).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 
+   { message: "Slug must contain only lowercase letters, numbers, and hyphens and cannot start or end with a hyphen" }),
apps/web-app/app/components/CommandCenter/PageCard.vue (2)

2-14: Add fallback UI for when page is not found

The component uses optional chaining (page?.slug and page?.title) but doesn't provide fallback content when the page is not found, which could result in an empty or incomplete card.

<template>
  <NuxtLink :to="`/command-center/page/${id}`" class="bg-(--ui-bg-elevated)/50 rounded-xl relative min-h-28 text-center flex flex-col gap-1 items-center justify-center">
    <UIcon
      name="i-lucide-file-text"
      class="size-6 text-(--ui-text-dimmed)"
    />

    <p class="text-xs text-(--ui-text-muted)">
-      /{{ page?.slug }}
+      /{{ page?.slug || $t('common.not-found') }}
    </p>
    <p>
-      {{ getLocaleValue({ values: page?.title, locale, defaultLocale: channel.defaultLocale }) }}
+      {{ page ? getLocaleValue({ values: page.title, locale, defaultLocale: channel.defaultLocale }) : $t('common.not-found') }}
    </p>
  </NuxtLink>
</template>

18-24: Validate page existence in the computed property

The component finds the page in the channel store but doesn't handle the case where the page might not exist. Consider adding validation or error handling.

const { id } = defineProps<{
  id: string
}>()

const { locale } = useI18n()
const channel = useChannelStore()
- const page = computed(() => channel.pages.find((p) => p.id === id))
+ const page = computed(() => {
+   const foundPage = channel.pages.find((p) => p.id === id)
+   if (!foundPage) {
+     console.warn(`Page with id ${id} not found`)
+   }
+   return foundPage
+ })
apps/web-app/app/components/Form/DeletePage.vue (1)

27-45: Improve error feedback to users

The current error handling logs to the console but provides a generic error toast to the user. Consider providing more specific error messages to help users understand what went wrong.

async function onSubmit() {
  actionToast.start()
  emit('submitted')

  try {
    await $fetch(`/api/page/${id}`, {
      method: 'DELETE',
    })

    await channel.update()
    actionToast.success(t('toast.page-deleted'))
    emit('success')

    router.push(redirectTo)
  } catch (error) {
    console.error(error)
-    actionToast.error()
+    const errorMessage = error.data?.message || t('toast.generic_error')
+    actionToast.error(errorMessage)
  }
}

Also add a reactive property for the confirmation dialog:

const isConfirmOpen = ref(false)

function confirmDelete() {
  isConfirmOpen.value = true
}
apps/web-app/app/components/Form/UpdatePage.vue (1)

79-96: Improve error handling with specific error messages

The current error handling logs to the console but provides a generic error toast. Consider providing more specific error messages for better user feedback.

async function onSubmit(event: FormSubmitEvent<PageUpdateSchema>) {
  actionToast.start()
  emit('submitted')

  try {
    await $fetch(`/api/page/${id}`, {
      method: 'PATCH',
      body: event.data,
    })

    await channel.update()
    actionToast.success(t('toast.page-updated'))
    emit('success')
  } catch (error) {
    console.error(error)
-    actionToast.error()
+    if (error.response?.status === 404) {
+      actionToast.error(t('toast.page-not-found'))
+    } else if (error.data?.message) {
+      actionToast.error(error.data.message)
+    } else {
+      actionToast.error(t('toast.generic_error'))
+    }
  }
}
apps/web-app/app/components/TiptapEditor.vue (2)

118-125: Consider using more specific extensions instead of the entire starter kit

The TiptapStarterKit includes many extensions that might not be needed. For better performance and bundle size, consider importing only the specific extensions you're using in the editor.


127-129: Add debounce to the content change emission

Emitting the change event on every document change can lead to excessive updates, especially during fast typing. Consider adding a debounce mechanism.

-watch(() => editor.value?.state.doc, () => {
-  emit('change', editor.value?.getHTML() as string)
-})
+const debouncedEmit = useDebounceFn(() => {
+  emit('change', editor.value?.getHTML() as string)
+}, 300)
+
+watch(() => editor.value?.state.doc, () => {
+  debouncedEmit()
+})
packages/core/server/services/db/page.ts (2)

38-45: Missing field validation in createPage

The createPage function doesn't validate that required fields are present in the data object. Consider adding validation to ensure necessary fields like 'id' exist before attempting to create the page.

export async function createPage(data: Omit<Page, 'createdAt' | 'updatedAt'>): Promise<Page | null> {
+  if (!data.id) {
+    throw new Error('Page ID is required')
+  }
+
  await useStorage('db').setItem(`page:${data.id}`, {
    ...data,
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
  })
  return getPage(data.id)
}

47-49: Consider returning a boolean for deletePage

Returning a boolean indicating whether the deletion was successful would make error handling more consistent with other functions.

-export async function deletePage(id: string): Promise<void> {
-  await useStorage('db').removeItem(`page:${id}`)
+export async function deletePage(id: string): Promise<boolean> {
+  try {
+    await useStorage('db').removeItem(`page:${id}`)
+    return true
+  } catch (error) {
+    console.error(`Failed to delete page ${id}:`, error)
+    return false
+  }
}
apps/web-app/app/pages/command-center/page/[pageId]/index.vue (2)

21-23: TiptapEditor integration needs proper error handling.

The TiptapEditor component correctly receives content and emits changes, but there's no validation or error handling for potentially malformed content.

Consider adding content validation before submission:

-    <TiptapEditor :content="page?.content" @change="(c: string) => { content = c }" />
+    <TiptapEditor 
+      :content="page?.content" 
+      @change="(c: string) => { 
+        content = c;
+        contentHasChanged = true;
+      }" 
+    />

And add a computed property to detect if content has changed:

const contentHasChanged = ref(false)
const canSubmit = computed(() => contentHasChanged.value && !!content.value)
🧰 Tools
🪛 ESLint

[error] 22-22: Parsing error: Unexpected token :.

(vue/no-parsing-error)


48-66: Consider adding optimistic UI updates and content validation.

The submission function works correctly but could benefit from optimistic UI updates and additional validation.

Consider enhancing the submit function with validation and optimistic updates:

async function onSubmit() {
+ if (!content.value?.trim()) {
+   actionToast.error(t('error.default'))
+   return
+ }

  actionToast.start()
+ const previousContent = page.value?.content
+ // Optimistically update local state
+ if (page.value) {
+   page.value.content = content.value
+ }

  try {
    await $fetch(`/api/page/${params.pageId}`, {
      method: 'PATCH',
      body: {
        locale: channel.defaultLocale,
        content: content.value,
      },
    })

    await channel.update()
    actionToast.success(t('toast.page-updated'))
+   contentHasChanged.value = false
  } catch (error) {
    console.error(error)
    actionToast.error()
+   // Revert optimistic update on error
+   if (page.value && previousContent) {
+     page.value.content = previousContent
+   }
  }
}
apps/web-app/app/pages/command-center/page/index.vue (2)

5-11: Consider adding loading and empty states.

The current implementation doesn't handle cases where pages might be loading or when there are no pages to display.

Add loading and empty states to improve user experience:

-    <div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
+    <div v-if="channel.isLoading" class="flex justify-center items-center p-8">
+      <UiSpinner size="lg" />
+    </div>
+    <div v-else-if="channel.pages.length === 0" class="flex justify-center items-center p-8">
+      <p class="text-gray-500">{{ t('common.no-items-found') }}</p>
+    </div>
+    <div v-else class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
      <CommandCenterPageCard
        v-for="page in channel.pages"
        :id="page.id"
        :key="page.id"
      />
      <CommandCenterPageCreateCard @click="modalCreatePage.open()" />
    </div>

24-28: Consider adding data fetching on component mount.

The component relies on the channel store to already have page data loaded, but doesn't explicitly fetch data when it mounts.

Add explicit data fetching on component mount to ensure pages are loaded:

const { t } = useI18n()
const overlay = useOverlay()
const modalCreatePage = overlay.create(ModalCreatePage)

const channel = useChannelStore()
+ 
+ onMounted(async () => {
+   if (!channel.pages.length) {
+     await channel.update()
+   }
+ })
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e17c84 and 4865400.

📒 Files selected for processing (29)
  • apps/web-app/app/components/Checkout/Header.vue (1 hunks)
  • apps/web-app/app/components/CommandCenter/Navigation.vue (1 hunks)
  • apps/web-app/app/components/CommandCenter/PageCard.vue (1 hunks)
  • apps/web-app/app/components/CommandCenter/PageCreateCard.vue (1 hunks)
  • apps/web-app/app/components/Footer.vue (0 hunks)
  • apps/web-app/app/components/Form/CreatePage.vue (1 hunks)
  • apps/web-app/app/components/Form/DeletePage.vue (1 hunks)
  • apps/web-app/app/components/Form/UpdatePage.vue (1 hunks)
  • apps/web-app/app/components/Modal/CreatePage.vue (1 hunks)
  • apps/web-app/app/components/Modal/UpdatePage.vue (1 hunks)
  • apps/web-app/app/components/TiptapEditor.vue (1 hunks)
  • apps/web-app/app/layouts/default.vue (1 hunks)
  • apps/web-app/app/pages/[[page]].vue (1 hunks)
  • apps/web-app/app/pages/command-center/page/[pageId]/index.vue (1 hunks)
  • apps/web-app/app/pages/command-center/page/index.vue (1 hunks)
  • apps/web-app/app/utils/helpers.ts (1 hunks)
  • apps/web-app/i18n/locales/en-US.json (4 hunks)
  • apps/web-app/i18n/locales/ka-GE.json (4 hunks)
  • apps/web-app/i18n/locales/ru-RU.json (4 hunks)
  • apps/web-app/stores/channel.ts (3 hunks)
  • packages/core/server/api/channel/index.get.ts (3 hunks)
  • packages/core/server/api/page/[id].delete.ts (1 hunks)
  • packages/core/server/api/page/[id].patch.ts (1 hunks)
  • packages/core/server/api/page/index.post.ts (1 hunks)
  • packages/core/server/middleware/01.permissions.ts (1 hunks)
  • packages/core/server/services/db/index.ts (3 hunks)
  • packages/core/server/services/db/page.ts (1 hunks)
  • packages/core/shared/services/page.ts (1 hunks)
  • packages/core/types/index.d.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/web-app/app/components/Footer.vue
🧰 Additional context used
🧬 Code Definitions (3)
packages/core/server/api/page/index.post.ts (2)
packages/core/shared/services/page.ts (1) (1)
  • pageCreateSchema (4-9)
packages/core/server/services/db/page.ts (1) (1)
  • createPage (38-45)
packages/core/server/api/page/[id].delete.ts (1)
packages/core/server/services/db/page.ts (1) (1)
  • deletePage (47-49)
packages/core/server/api/page/[id].patch.ts (2)
packages/core/shared/services/page.ts (1) (1)
  • pageUpdateSchema (13-18)
packages/core/server/services/db/page.ts (2) (2)
  • getPage (1-3)
  • patchPage (21-36)
🪛 ESLint
apps/web-app/app/pages/command-center/page/[pageId]/index.vue

[error] 22-22: Parsing error: Unexpected token :.

(vue/no-parsing-error)

🔇 Additional comments (49)
apps/web-app/app/layouts/default.vue (1)

10-10: LGTM: Horizontal margin adjustment.

Adding mx-0 to remove horizontal margins is a good adjustment that will allow content to utilize the full width of the container.

packages/core/server/middleware/01.permissions.ts (1)

33-37: Permission configuration for page endpoints looks good.

The permission setup for the new page API endpoints follows the established pattern in the codebase, requiring 'MANAGE_OPTIONS' or 'MASTER' permissions for write operations.

Is it intentional that GET requests to /api/page are not protected? If pages should be publicly readable, this is correct, but if they should be access-controlled, consider adding protection for GET requests as well.

apps/web-app/app/components/Checkout/Header.vue (1)

2-2: Improved responsive layout and spacing.

The updated class attributes improve the header's layout with proper flex properties and responsive padding. The previous implementation had redundant padding values (px-4 md:px-4), while the new one creates a better responsive adjustment with the addition of spacing between flex items.

apps/web-app/app/components/Modal/CreatePage.vue (2)

1-10: Well-structured modal component

The modal is well-implemented using UModal and correctly loads the FormCreatePage component with appropriate event handling.


12-20: Clean implementation of overlay closing logic

The closeAll function efficiently handles closing all overlays when the form is submitted or successful. This ensures a smooth user experience when creating pages.

apps/web-app/app/utils/helpers.ts (1)

165-166: Good addition of new icon options

Adding 'Gift' and 'Info' icons expands the available options for users when selecting link icons. These are useful additions that enhance the user experience.

packages/core/server/api/page/index.post.ts (1)

1-25: Page creation endpoint implementation is well-structured!

The implementation follows good practices with proper:

  • Input validation using Zod schema
  • Unique ID generation
  • Title localization
  • Channel update for cache invalidation
  • Error handling and consistent response format

However, I notice that while the title is stored in a localized format (array of locale-value pairs), the content is stored as a simple string. If content also needs internationalization support, consider transforming it similarly to the title field.

apps/web-app/app/components/Form/CreatePage.vue (2)

2-44: Well-structured form component with good UX

The form layout is clean and follows a consistent pattern with other form components in the application. Good use of UI components like UButtonGroup, UDropdownMenu, and proper form validation through schema.


60-65: Content field in state but missing in UI

The state includes a content field initialized with an empty string, but there's no corresponding input field in the template. This suggests that page content is intended to be empty by default and edited elsewhere.

Is this intentional? If users should be able to add content during creation, consider adding a content editor component.

apps/web-app/app/pages/[[page]].vue (2)

10-16: Good error handling for non-existent pages

Properly throwing a 404 error when a page is not found provides a good user experience and follows RESTful principles.


18-20: Dynamic page title is a good UX feature

Setting the document title based on the page content using useHead and getLocaleValue provides good user experience and SEO benefits.

apps/web-app/app/components/Modal/UpdatePage.vue (1)

6-7:

Details

❓ Verification inconclusive

Potential issue with optional ID prop

You're using the nullish coalescing operator to provide an empty string when id is undefined, but this could lead to unexpected behavior in child components that expect a valid ID.

Verify that the child components can properly handle empty string IDs, or consider making the ID prop required or adding validation.


🏁 Script executed:

#!/bin/bash
# Check how FormUpdatePage and FormDeletePage handle empty ID values
rg -A 5 -B 5 "id.*props" --glob "**/Form*Page.vue"

Length of output: 201


Attention: Manual Verification Needed for Optional ID Prop Handling

The initial search using ripgrep did not yield results, likely due to an overly restrictive filter. To ensure that child components (such as FormUpdatePage.vue and FormDeletePage.vue) correctly handle empty string IDs, please run the following shell script to locate and inspect these files for how they process their id prop:

Please review the output of this script manually to verify that empty string IDs are either handled gracefully or if further actions (like making the id prop required or adding validation) are needed.


🏁 Script executed:

#!/bin/bash
# Locate the relevant Vue files that might use the "id" prop
files=$(fd -HI "Form(Update|Delete)Page.vue")
if [ -z "$files" ]; then
    echo "No FormUpdatePage.vue or FormDeletePage.vue files found."
else
    echo "Found the following files:"
    echo "$files"
    echo "Inspecting how the 'id' prop is used:"
    for file in $files; do
        echo "----- $file -----"
        rg -C 5 "id[ :]*" "$file"
    done
fi

Length of output: 187


Review Comment Update: Verify Handling of Empty ID Value

We've executed a search for child components (specifically, files like FormUpdatePage.vue or FormDeletePage.vue) that might depend on a non-empty id prop, but no such files were found in the repository. Without concrete evidence from these components, it's unclear if passing an empty string (via the nullish coalescing operator) may negatively affect any child component behavior.

Action Items:

  • Manual Verification: Please verify in your project if any component that receives this id (directly or indirectly) expects a valid, non-empty value.
  • Consider Enhancements: If a valid ID is required downstream, either make the id prop mandatory or incorporate additional validation instead of defaulting to an empty string.
apps/web-app/stores/channel.ts (2)

26-26: Good addition of pages state to the store

Adding the pages reactive array to the channel store is a good way to maintain state for pages across the application.


70-70: Pages data is properly updated

The update function is properly updated to include pages data from the API response.

packages/core/shared/services/page.ts (1)

13-18: Validation is consistent with create schema

The update schema correctly makes all fields optional while maintaining the same validation rules as the create schema, which is a good practice for PATCH operations.

apps/web-app/app/components/Form/UpdatePage.vue (1)

1-45:

Details

❓ Verification inconclusive

Missing content field in the update form

The pageUpdateSchema includes a content field, but it's missing from the form UI. If content should be editable through this form, add a content editor field. If content is managed elsewhere, consider documenting this decision.

Run the following to check if the content is managed in another component:


🏁 Script executed:

#!/bin/bash
# Look for components that might be handling page content editing
rg -l "v-model.*content" --type vue
# Check if there's a dedicated content editor component
fd -e vue "Content.*Editor|Editor.*Content"

Length of output: 111


Content Field Missing in Update Form

The update form in apps/web-app/app/components/Form/UpdatePage.vue does not provide a UI element for the content field defined in the pageUpdateSchema. Our initial search (adjusted for proper Vue file recognition) did not reveal any dedicated content editor component or alternative handling for content editing. Please verify manually whether the content field is handled elsewhere. If it’s meant to be editable via this form, add an appropriate content editor field; otherwise, document the decision to manage content outside this component.

packages/core/server/api/page/[id].patch.ts (1)

1-39: Code looks robust and follows best practices

The PATCH endpoint implementation is well-structured with appropriate error handling, validation, and data processing logic. It correctly:

  • Validates the request body against a schema
  • Performs proper error handling with appropriate status codes
  • Handles localization for title updates
  • Updates the channel to reflect changes

I particularly like how it preserves existing title values when updating localized content.

apps/web-app/i18n/locales/en-US.json (5)

264-266: LGTM - Added "pages" menu item

The localization entries for the Pages section in the navigation menu are correctly implemented.


267-267: LGTM - Added "save" localization

The "save" localization entry is correctly added to support the page editing functionality.


276-277: LGTM - Added page creation localization

The localization entry for creating a new page is correctly implemented alongside the existing "Create new Link" entry.


300-301: LGTM - Added page update localization

The localization entry for updating a page is correctly implemented alongside the existing "Update Link" entry.


355-357: LGTM - Added toast messages for page operations

The toast notification messages for page creation, update, and deletion operations are correctly implemented.

apps/web-app/app/components/TiptapEditor.vue (1)

112-116: Props and emits declarations are correctly implemented

The component properly defines a content prop and emits a change event when the content changes.

packages/core/server/services/db/page.ts (2)

1-3: LGTM - Simple and focused getPage function

The getPage function follows a simple pattern of retrieving an item from storage by ID.


21-36: LGTM - Well-implemented page update function

The patchPage function correctly:

  • Checks for the existence of the page before updating
  • Preserves existing data while applying updates
  • Updates the timestamp
  • Returns the updated page
apps/web-app/i18n/locales/ka-GE.json (5)

264-265: Localization additions for page functionality are consistent.

The menu entry for "Pages" is properly added to the navigation section, maintaining consistency with other menu items.


267-267: Save action is correctly added to the localization file.

This entry will be used for the save button in the page editor component.


276-277: Create page action is properly localized.

The entry follows the existing pattern for create actions in the application.


300-301: Update page action is properly localized.

The entry follows the existing pattern for update actions in the application.


355-357: Toast notifications for page operations are correctly implemented.

All necessary toast messages for page creation, updating, and deletion are properly added, maintaining consistency with other toast notifications in the system.

apps/web-app/i18n/locales/ru-RU.json (5)

264-265: Russian localization for page functionality is properly implemented.

The menu entry for "Pages" ("Страницы") is correctly added to the navigation section, maintaining consistency with other menu items.


267-267: Save action is correctly localized in Russian.

The "Save" entry ("Сохранить") will be used for the save button in the page editor component.


276-277: Create page action is properly localized in Russian.

The entry follows the existing pattern for create actions in the application, with proper Russian translation.


300-301: Update page action is properly localized in Russian.

The entry follows the existing pattern for update actions in the application, with proper Russian translation.


355-357: Toast notifications for page operations are correctly implemented in Russian.

All necessary toast messages for page creation, updating, and deletion are properly added in Russian, maintaining consistency with other toast notifications in the system.

apps/web-app/app/pages/command-center/page/[pageId]/index.vue (3)

1-19: Header with action buttons is implemented effectively.

The component header provides a clear title and appropriate action buttons for editing and saving page content. The buttons have appropriate variants and responsive styling.


26-41: Page retrieval and error handling are implemented well.

The component correctly retrieves the page from the store using the route parameter and throws an appropriate 404 error if the page is not found.


22-22:

Details

✅ Verification successful

Fix the ESLint parsing error in the TiptapEditor component.

The static analysis tool reports a parsing error on this line, but it appears to be a false positive as the code is using valid Vue syntax.

Verify if this is a false positive by running:


🏁 Script executed:

#!/bin/bash
# Check if there are any issues with the TiptapEditor component's props
rg -A 5 "TiptapEditor" --type=vue

Length of output: 61


Action: Verify ESLint Parsing for TiptapEditor

It looks like the initial ripgrep command failed because it didn’t recognize the "vue" file type. To further confirm that there are no genuine issues with the TiptapEditor component’s props—and that this ESLint parsing error is indeed a false positive—please run the updated verification command below, which explicitly searches all Vue files via a glob pattern:

After running this command, please verify:

  • That the TiptapEditor usage (including props like :content and the @change event) is correctly implemented;
  • Your ESLint configuration properly handles .vue files (ensuring the appropriate plugins and parser options are in place).

If no real issues surface, you can safely treat the ESLint message as a false positive. Please confirm the output manually.


🏁 Script executed:

#!/bin/bash
# Re-run ripgrep to search for TiptapEditor occurrences in Vue files using a glob pattern.
rg -g "*.vue" -A 5 "TiptapEditor"

Length of output: 1012


ESLint Parsing Error is a False Positive

  • Verified that the <TiptapEditor> usage in apps/web-app/app/pages/command-center/page/[pageId]/index.vue is valid.
  • Confirmed that the component’s definition in apps/web-app/app/components/TiptapEditor.vue properly uses TypeScript with Vue’s SFC syntax.
  • The ESLint issue appears to be a false positive; please review your ESLint configuration to ensure it correctly parses Vue and TypeScript syntax.
🧰 Tools
🪛 ESLint

[error] 22-22: Parsing error: Unexpected token :.

(vue/no-parsing-error)

apps/web-app/app/pages/command-center/page/index.vue (2)

1-14: Responsive grid layout is well implemented.

The page listing uses a responsive grid that adapts to different screen sizes, providing a good user experience across devices. The CommandCenterPageCard and CommandCenterPageCreateCard components are used effectively.


16-29: Script setup is clean and follows best practices.

The component correctly uses composables and page metadata. The use of the channel store centralizes data management, which is a good practice.

packages/core/server/api/channel/index.get.ts (4)

6-6: Clean import addition.

The addition of the getPages import follows the existing pattern for similar service imports. The function is properly imported from the correct relative path.


64-64: Interface updating properly extends data structure.

The destructured object now includes pageKeys, which correctly aligns with the changes made to the getKeys function.


71-71: Data retrieval pattern maintained.

The implementation follows the established pattern of retrieving data based on filtered keys, maintaining code consistency across different entity types.


91-91: Response object extended with pages data.

The addition of the pages property to the return object makes the page data available to the client, completing the data retrieval workflow.

packages/core/server/services/db/index.ts (3)

10-10: Interface extended properly.

The Keys interface has been updated to include pageKeys as a string array, maintaining consistency with the structure of other key types in the interface.


22-22: Key filtering logic follows established pattern.

The implementation for filtering page-related keys uses the same pattern as other entity types, maintaining code consistency.


33-33: Return object extended with pageKeys.

The pageKeys property is properly included in the returned object, making it available for page data retrieval operations.

packages/core/server/api/page/[id].delete.ts (2)

1-3: Imports are clean and focused.

The imports are properly defined for the functionality required by this endpoint.


4-22: Handler implementation follows API endpoint patterns.

The event handler follows the established pattern for API endpoints in the application, with proper error handling, parameter validation, and channel updates.

Consider adding an existence check before deletion to return a more specific error message if the page doesn't exist:

    const id = getRouterParam(event, 'id')
    if (!id) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Missing id',
      })
    }

+   // Check if page exists before deletion
+   const page = await getPage(id)
+   if (!page) {
+     throw createError({
+       statusCode: 404,
+       statusMessage: 'Page not found',
+     })
+   }

    await deletePage(id)

This would require importing the getPage function, but it would provide a better user experience by distinguishing between missing IDs and non-existent pages.

Comment on lines +5 to +9
<UButton
size="md"
variant="soft"
:label="$t('center.create.page')"
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing click handler on button

The button doesn't have a click handler (@click) to trigger the page creation modal. Without this, clicking the button won't perform any action.

 <UButton
   size="md"
   variant="soft"
   :label="$t('center.create.page')"
+  @click="openCreatePageModal"
 />

You'll need to implement the openCreatePageModal function in the script section.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<UButton
size="md"
variant="soft"
:label="$t('center.create.page')"
/>
<UButton
size="md"
variant="soft"
:label="$t('center.create.page')"
@click="openCreatePageModal"
/>

@@ -0,0 +1,21 @@
<template>
<div class="w-full prose prose-md dark:prose-invert text-(--ui-text) [&_p]:my-0" v-html="preparedPage.content" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential XSS vulnerability with v-html

Using v-html to render content directly can expose your application to XSS attacks if the content is user-generated or comes from an untrusted source.

Consider using a sanitization library like DOMPurify or a markdown renderer like marked with sanitization options:

- <div class="w-full prose prose-md dark:prose-invert text-(--ui-text) [&_p]:my-0" v-html="preparedPage.content" />
+ <div class="w-full prose prose-md dark:prose-invert text-(--ui-text) [&_p]:my-0" v-html="sanitizeHtml(preparedPage.content)" />

And in the script section:

import DOMPurify from 'dompurify'

function sanitizeHtml(html) {
  return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } })
}

Comment on lines +1 to +11
<template>
<UButton
type="submit"
variant="soft"
color="error"
size="xl"
block
@click="onSubmit"
>
{{ $t('center.delete.title') }}
</UButton>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add confirmation dialog before deletion

Destructive actions like deletion should have a confirmation dialog to prevent accidental deletions. The current implementation directly deletes when the button is clicked.

<template>
  <UButton
    type="submit"
    variant="soft"
    color="error"
    size="xl"
    block
-    @click="onSubmit"
+    @click="confirmDelete"
  >
    {{ $t('center.delete.title') }}
  </UButton>
+  <UModal v-model="isConfirmOpen">
+    <UCard>
+      <template #header>
+        <div class="text-center">
+          <h3 class="text-lg font-medium">{{ $t('center.delete.confirm_title') }}</h3>
+        </div>
+      </template>
+      <p>{{ $t('center.delete.confirm_message') }}</p>
+      <template #footer>
+        <div class="flex justify-end space-x-4">
+          <UButton @click="isConfirmOpen = false">{{ $t('common.cancel') }}</UButton>
+          <UButton color="error" @click="onSubmit">{{ $t('center.delete.confirm') }}</UButton>
+        </div>
+      </template>
+    </UCard>
+  </UModal>
</template>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<template>
<UButton
type="submit"
variant="soft"
color="error"
size="xl"
block
@click="onSubmit"
>
{{ $t('center.delete.title') }}
</UButton>
<template>
<UButton
type="submit"
variant="soft"
color="error"
size="xl"
block
@click="confirmDelete"
>
{{ $t('center.delete.title') }}
</UButton>
<UModal v-model="isConfirmOpen">
<UCard>
<template #header>
<div class="text-center">
<h3 class="text-lg font-medium">{{ $t('center.delete.confirm_title') }}</h3>
</div>
</template>
<p>{{ $t('center.delete.confirm_message') }}</p>
<template #footer>
<div class="flex justify-end space-x-4">
<UButton @click="isConfirmOpen = false">{{ $t('common.cancel') }}</UButton>
<UButton color="error" @click="onSubmit">{{ $t('center.delete.confirm') }}</UButton>
</div>
</template>
</UCard>
</UModal>
</template>

Comment on lines +61 to +69
const page = channel.pages.find((p) => p.id === id)

const localeState = useLocalizedState(resetState, channel.defaultLocale)

const state = ref<Partial<PageUpdateSchema>>({
locale: localeState.locale.value,
slug: page?.slug,
title: page?.title.find((title) => title.locale === localeState.locale.value)?.value,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Handle the case when page is not found

The code doesn't handle the situation where the page with the given ID doesn't exist in the channel store. This could lead to errors when accessing properties of an undefined page.

const channel = useChannelStore()
const page = channel.pages.find((p) => p.id === id)
+ 
+ if (!page) {
+   console.error(`Page with id ${id} not found`)
+   actionToast.error(t('toast.page-not-found'))
+   // Consider redirecting to a safe page or showing an error state
+ }

const localeState = useLocalizedState(resetState, channel.defaultLocale)

const state = ref<Partial<PageUpdateSchema>>({
  locale: localeState.locale.value,
  slug: page?.slug,
  title: page?.title.find((title) => title.locale === localeState.locale.value)?.value,
})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const page = channel.pages.find((p) => p.id === id)
const localeState = useLocalizedState(resetState, channel.defaultLocale)
const state = ref<Partial<PageUpdateSchema>>({
locale: localeState.locale.value,
slug: page?.slug,
title: page?.title.find((title) => title.locale === localeState.locale.value)?.value,
})
const channel = useChannelStore()
const page = channel.pages.find((p) => p.id === id)
if (!page) {
console.error(`Page with id ${id} not found`)
actionToast.error(t('toast.page-not-found'))
// Consider redirecting to a safe page or showing an error state
}
const localeState = useLocalizedState(resetState, channel.defaultLocale)
const state = ref<Partial<PageUpdateSchema>>({
locale: localeState.locale.value,
slug: page?.slug,
title: page?.title.find((title) => title.locale === localeState.locale.value)?.value,
})

Comment on lines +131 to +135
onMounted(() => {
if (unref(editor)) {
unref(editor)?.commands.setContent(content as string)
}
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add reactivity to content prop changes

The component only sets the initial content on mount but doesn't react to prop changes. This means if the parent component updates the content prop, the editor won't reflect these changes.

+watch(() => content, (newContent) => {
+  if (unref(editor) && newContent !== editor.value?.getHTML()) {
+    unref(editor)?.commands.setContent(newContent as string)
+  }
+}, { deep: true })

onMounted(() => {
  if (unref(editor)) {
    unref(editor)?.commands.setContent(content as string)
  }
})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onMounted(() => {
if (unref(editor)) {
unref(editor)?.commands.setContent(content as string)
}
})
watch(() => content, (newContent) => {
if (unref(editor) && newContent !== editor.value?.getHTML()) {
unref(editor)?.commands.setContent(newContent as string)
}
}, { deep: true })
onMounted(() => {
if (unref(editor)) {
unref(editor)?.commands.setContent(content as string)
}
})

Comment on lines +5 to +19
export async function getPages(keys: string[]): Promise<Page[]> {
const pageStorage = new Map<string, unknown>(keys.map((key) => [key, useStorage('db').getItem(key)]))
const pages: Page[] = []

for (const key of keys) {
const page = await pageStorage.get(key) as Page
if (!page) {
continue
}

pages.push(page)
}

return pages
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Inefficient implementation for batch retrieval

The current getPages implementation creates a Map but then doesn't use its advantages, making sequential await calls. This approach is inefficient for batch retrieval.

export async function getPages(keys: string[]): Promise<Page[]> {
-  const pageStorage = new Map<string, unknown>(keys.map((key) => [key, useStorage('db').getItem(key)]))
-  const pages: Page[] = []
-
-  for (const key of keys) {
-    const page = await pageStorage.get(key) as Page
-    if (!page) {
-      continue
-    }
-
-    pages.push(page)
-  }
-
-  return pages
+  const pagePromises = keys.map(key => useStorage('db').getItem<Page>(key))
+  const results = await Promise.all(pagePromises)
+  
+  // Filter out null values
+  return results.filter((page): page is Page => page !== null)
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function getPages(keys: string[]): Promise<Page[]> {
const pageStorage = new Map<string, unknown>(keys.map((key) => [key, useStorage('db').getItem(key)]))
const pages: Page[] = []
for (const key of keys) {
const page = await pageStorage.get(key) as Page
if (!page) {
continue
}
pages.push(page)
}
return pages
}
export async function getPages(keys: string[]): Promise<Page[]> {
const pagePromises = keys.map(key => useStorage('db').getItem<Page>(key))
const results = await Promise.all(pagePromises)
// Filter out null values
return results.filter((page): page is Page => page !== null)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant