create-nextblock 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/bin/create-nextblock.js +997 -0
  2. package/package.json +25 -0
  3. package/scripts/sync-template.js +284 -0
  4. package/templates/nextblock-template/.env.example +37 -0
  5. package/templates/nextblock-template/.swcrc +30 -0
  6. package/templates/nextblock-template/README.md +194 -0
  7. package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
  8. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
  9. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
  10. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
  12. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
  13. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
  14. package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
  15. package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
  16. package/templates/nextblock-template/app/actions/email.ts +31 -0
  17. package/templates/nextblock-template/app/actions/formActions.ts +65 -0
  18. package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
  19. package/templates/nextblock-template/app/actions/postActions.ts +80 -0
  20. package/templates/nextblock-template/app/actions.ts +146 -0
  21. package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
  22. package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
  23. package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
  24. package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
  25. package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
  26. package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
  27. package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
  28. package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
  29. package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
  30. package/templates/nextblock-template/app/blog/page.tsx +77 -0
  31. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
  32. package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
  33. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
  34. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
  36. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
  37. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
  38. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
  39. package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
  40. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
  41. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
  43. package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
  44. package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
  45. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
  46. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
  47. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
  48. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
  49. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
  50. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
  51. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
  52. package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
  53. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
  54. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
  55. package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
  56. package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
  57. package/templates/nextblock-template/app/cms/layout.tsx +10 -0
  58. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
  59. package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
  60. package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
  61. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
  62. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
  63. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
  64. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
  65. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
  66. package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
  67. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
  68. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
  69. package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
  70. package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
  71. package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
  72. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
  73. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
  74. package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
  75. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
  76. package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
  77. package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
  78. package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
  79. package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
  80. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
  81. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
  82. package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
  83. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
  84. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
  85. package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
  86. package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
  87. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
  88. package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
  89. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
  90. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
  91. package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
  92. package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
  93. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
  94. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
  95. package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
  96. package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
  97. package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
  98. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
  99. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
  100. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
  101. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
  102. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
  103. package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
  104. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
  105. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
  106. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
  107. package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
  108. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
  109. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
  110. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
  111. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
  112. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
  113. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
  114. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
  115. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
  116. package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
  117. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
  118. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
  119. package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
  120. package/templates/nextblock-template/app/favicon.ico +0 -0
  121. package/templates/nextblock-template/app/globals.css +401 -0
  122. package/templates/nextblock-template/app/layout.tsx +191 -0
  123. package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
  124. package/templates/nextblock-template/app/page.tsx +109 -0
  125. package/templates/nextblock-template/app/providers.tsx +43 -0
  126. package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
  127. package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
  128. package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
  129. package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
  130. package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
  131. package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
  132. package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
  133. package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
  134. package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
  135. package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
  136. package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
  137. package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
  138. package/templates/nextblock-template/components/Header.tsx +42 -0
  139. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
  140. package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
  141. package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
  142. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
  143. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
  144. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
  145. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
  146. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
  147. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
  148. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
  149. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
  150. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
  151. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
  152. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
  153. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
  154. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
  155. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
  156. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
  157. package/templates/nextblock-template/components/blocks/types.ts +8 -0
  158. package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
  159. package/templates/nextblock-template/components/form-message.tsx +26 -0
  160. package/templates/nextblock-template/components/header-auth.tsx +71 -0
  161. package/templates/nextblock-template/components/submit-button.tsx +23 -0
  162. package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
  163. package/templates/nextblock-template/context/AuthContext.tsx +138 -0
  164. package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
  165. package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
  166. package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
  167. package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
  168. package/templates/nextblock-template/docs/files-structure.md +426 -0
  169. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
  170. package/templates/nextblock-template/eslint.config.mjs +28 -0
  171. package/templates/nextblock-template/index.d.ts +5 -0
  172. package/templates/nextblock-template/lib/blocks/README.md +670 -0
  173. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
  174. package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
  175. package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
  176. package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
  177. package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
  178. package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
  179. package/templates/nextblock-template/lib/ui/badge.ts +1 -0
  180. package/templates/nextblock-template/lib/ui/button.ts +1 -0
  181. package/templates/nextblock-template/lib/ui/card.ts +1 -0
  182. package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
  183. package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
  184. package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
  185. package/templates/nextblock-template/lib/ui/input.ts +1 -0
  186. package/templates/nextblock-template/lib/ui/label.ts +1 -0
  187. package/templates/nextblock-template/lib/ui/popover.ts +1 -0
  188. package/templates/nextblock-template/lib/ui/progress.ts +1 -0
  189. package/templates/nextblock-template/lib/ui/select.ts +1 -0
  190. package/templates/nextblock-template/lib/ui/separator.ts +1 -0
  191. package/templates/nextblock-template/lib/ui/table.ts +1 -0
  192. package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
  193. package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
  194. package/templates/nextblock-template/lib/ui/ui.ts +1 -0
  195. package/templates/nextblock-template/middleware.ts +206 -0
  196. package/templates/nextblock-template/next-env.d.ts +6 -0
  197. package/templates/nextblock-template/next.config.js +99 -0
  198. package/templates/nextblock-template/package.json +52 -0
  199. package/templates/nextblock-template/postcss.config.js +6 -0
  200. package/templates/nextblock-template/project.json +7 -0
  201. package/templates/nextblock-template/public/.gitkeep +0 -0
  202. package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
  203. package/templates/nextblock-template/scripts/backup.js +53 -0
  204. package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
  205. package/templates/nextblock-template/tailwind.config.ts +19 -0
  206. package/templates/nextblock-template/tsconfig.json +62 -0
@@ -0,0 +1,701 @@
1
+ // app/cms/navigation/components/NavigationMenuDnd.tsx
2
+ "use client";
3
+
4
+ import React, { useState, useTransition, useCallback, useMemo, JSX, useEffect } from 'react';
5
+ import {
6
+ DndContext,
7
+ closestCenter,
8
+ KeyboardSensor,
9
+ PointerSensor,
10
+ useSensor,
11
+ useSensors,
12
+ DragEndEvent,
13
+ DragOverlay,
14
+ DragStartEvent,
15
+ DragMoveEvent,
16
+ UniqueIdentifier,
17
+ MeasuringStrategy,
18
+ DropAnimation,
19
+ defaultDropAnimationSideEffects,
20
+ DragOverEvent,
21
+ } from '@dnd-kit/core';
22
+ import {
23
+ SortableContext,
24
+ sortableKeyboardCoordinates,
25
+ verticalListSortingStrategy,
26
+ } from '@dnd-kit/sortable';
27
+ import {
28
+ Table,
29
+ TableBody,
30
+ TableHead,
31
+ TableHeader,
32
+ TableRow,
33
+ TableCell,
34
+ } from "@nextblock-cms/ui";
35
+ import { Button } from "@nextblock-cms/ui";
36
+ import { HierarchicalNavItem, SortableNavItem } from './SortableNavItem';
37
+ import type { Database } from "@nextblock-cms/db";
38
+ import { updateNavigationStructureBatch } from '../actions';
39
+ import { GripVertical, MoreHorizontal } from 'lucide-react';
40
+
41
+ type NavigationItem = Database['public']['Tables']['navigation_items']['Row'];
42
+ type MenuLocation = Database['public']['Enums']['menu_location'];
43
+ import { Badge } from "@nextblock-cms/ui";
44
+ import { createPortal } from 'react-dom';
45
+
46
+ const INDENTATION_WIDTH = 25;
47
+
48
+ interface FoundItemInfo {
49
+ item: HierarchicalNavItem;
50
+ parent: HierarchicalNavItem | null;
51
+ siblings: HierarchicalNavItem[];
52
+ index: number;
53
+ }
54
+
55
+ const buildTree = (
56
+ items: NavigationItem[],
57
+ parentId: number | null = null,
58
+ depth = 0,
59
+ languageCode: string
60
+ ): HierarchicalNavItem[] => {
61
+ return items
62
+ .filter(item => item.parent_id === parentId)
63
+ .sort((a, b) => a.order - b.order)
64
+ .map(item => {
65
+ const navItem = item as NavigationItem;
66
+ return {
67
+ ...navItem,
68
+ id: Number(navItem.id),
69
+ depth,
70
+ children: buildTree(items, navItem.id, depth + 1, languageCode),
71
+ languageCode: languageCode,
72
+ parentLabel: items.find(p => p.id === navItem.parent_id)?.label || null,
73
+ pageSlug: 'pages' in navItem && navItem.pages && typeof navItem.pages === 'object' && 'slug' in navItem.pages ? (navItem.pages as { slug: string }).slug : null,
74
+ };
75
+ });
76
+ };
77
+
78
+ const flattenTree = (nodes: HierarchicalNavItem[]): HierarchicalNavItem[] => {
79
+ const result: HierarchicalNavItem[] = [];
80
+ const stack = [...nodes];
81
+ while (stack.length) {
82
+ const node = stack.shift();
83
+ if (node) {
84
+ result.push(node);
85
+ if (node.children && node.children.length) {
86
+ stack.unshift(...node.children);
87
+ }
88
+ }
89
+ }
90
+ return result;
91
+ };
92
+
93
+ function findItemDeep(
94
+ items: HierarchicalNavItem[],
95
+ itemId: UniqueIdentifier,
96
+ parent: HierarchicalNavItem | null = null
97
+ ): FoundItemInfo | null {
98
+ for (let i = 0; i < items.length; i++) {
99
+ const item = items[i];
100
+ if (item.id === itemId) {
101
+ return { item, parent, siblings: parent ? parent.children : items, index: i };
102
+ }
103
+ if (item.children && item.children.length > 0) {
104
+ const found = findItemDeep(item.children, itemId, item);
105
+ if (found) return found;
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+
111
+ function removeItemFromTree(
112
+ tree: HierarchicalNavItem[],
113
+ itemId: UniqueIdentifier
114
+ ): { newTree: HierarchicalNavItem[]; removedItemBranch: HierarchicalNavItem | null } {
115
+ let removedItemBranch: HierarchicalNavItem | null = null;
116
+ function filterRecursive(items: HierarchicalNavItem[]): HierarchicalNavItem[] {
117
+ return items.filter(item => {
118
+ if (item.id === itemId) {
119
+ removedItemBranch = item;
120
+ return false;
121
+ }
122
+ if (item.children) {
123
+ item.children = filterRecursive(item.children);
124
+ }
125
+ return true;
126
+ });
127
+ }
128
+ const newTree = filterRecursive(structuredClone(tree));
129
+ return { newTree, removedItemBranch };
130
+ }
131
+
132
+ function addItemToTree(
133
+ tree: HierarchicalNavItem[],
134
+ itemToAdd: HierarchicalNavItem,
135
+ targetParentId: UniqueIdentifier | null,
136
+ targetIndex: number
137
+ ): HierarchicalNavItem[] {
138
+ if (targetParentId === null) {
139
+ const newTree = [...tree];
140
+ newTree.splice(targetIndex, 0, itemToAdd);
141
+ return newTree;
142
+ }
143
+ return tree.map(node => {
144
+ if (node.id === targetParentId) {
145
+ const newChildren = [...(node.children || [])];
146
+ newChildren.splice(targetIndex, 0, itemToAdd);
147
+ return { ...node, children: newChildren };
148
+ }
149
+ if (node.children && node.children.length > 0) {
150
+ return { ...node, children: addItemToTree(node.children, itemToAdd, targetParentId, targetIndex) };
151
+ }
152
+ return node;
153
+ });
154
+ }
155
+
156
+ function normalizeTree(items: HierarchicalNavItem[], depth = 0, parentId: number | null = null): HierarchicalNavItem[] {
157
+ return items.map((item, index) => {
158
+ const normalizedItem: HierarchicalNavItem = {
159
+ ...item,
160
+ order: index,
161
+ parent_id: parentId,
162
+ depth,
163
+ };
164
+ if (normalizedItem.children && normalizedItem.children.length > 0) {
165
+ normalizedItem.children = normalizeTree(normalizedItem.children, depth + 1, normalizedItem.id as number);
166
+ } else {
167
+ normalizedItem.children = [];
168
+ }
169
+ return normalizedItem;
170
+ });
171
+ }
172
+
173
+ interface NavigationMenuDndProps {
174
+ menuKey: MenuLocation;
175
+ languageCode: string;
176
+ initialItems: NavigationItem[];
177
+ }
178
+
179
+ export default function NavigationMenuDnd({ menuKey, languageCode, initialItems }: NavigationMenuDndProps) {
180
+ void menuKey;
181
+ // Prevent SSR/hydration mismatches from dnd-kit by rendering on client only
182
+ // Keep hooks order stable; move early return after hooks
183
+ const [isMounted, setIsMounted] = useState(false);
184
+ useEffect(() => { setIsMounted(true); }, []);
185
+ const [, startTransition] = useTransition();
186
+ const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
187
+ const [hierarchicalItems, setHierarchicalItems] = useState<HierarchicalNavItem[]>(() => buildTree(initialItems, null, 0, languageCode));
188
+ const [projected, setProjected] = useState<{ parentId: UniqueIdentifier | null; index: number; depth: number; overId: UniqueIdentifier | null } | null>(null);
189
+
190
+ const sensors = useSensors(
191
+ useSensor(PointerSensor, { activationConstraint: { distance: 10 } }),
192
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
193
+ );
194
+
195
+ const flatItemIds = useMemo(() => flattenTree(hierarchicalItems).map(item => item.id), [hierarchicalItems]);
196
+ const activeItemDataForOverlay = activeId ? findItemDeep(hierarchicalItems, activeId) : null;
197
+
198
+ const getDragDepth = (offset: number, initialDepth: number) => {
199
+ const newDepth = initialDepth + Math.round(offset / INDENTATION_WIDTH);
200
+ return Math.max(0, newDepth);
201
+ };
202
+
203
+ const getProjectedDropPosition = useCallback((
204
+ activeItemLocal: HierarchicalNavItem,
205
+ overItemLocal: HierarchicalNavItem | null, // This is the item we are hovering "over"
206
+ dragDeltaX: number,
207
+ currentTree: HierarchicalNavItem[],
208
+ rawEvent: DragMoveEvent | DragOverEvent // Pass the raw event for pointer coordinates
209
+ ): { parentId: UniqueIdentifier | null; index: number; depth: number; } => {
210
+
211
+ const initialDepth = activeItemLocal.depth;
212
+ const flatCurrentTree = flattenTree(currentTree);
213
+ const activeItemFlatIndex = flatCurrentTree.findIndex(i => i.id === activeItemLocal.id);
214
+
215
+ // Define thresholds for explicit horizontal gesture
216
+ const HORIZONTAL_GESTURE_DRAG_X_THRESHOLD = INDENTATION_WIDTH * 0.4; // More sensitive: 10px
217
+ const HORIZONTAL_GESTURE_MAX_Y_DRIFT = 12; // Slightly more forgiving Y drift
218
+
219
+ let newProjectedDepth = getDragDepth(dragDeltaX, initialDepth); // Default calculation
220
+ let isHorizontalGestureIntent = false;
221
+
222
+ // --- Explicit Horizontal Gesture Detection (Highest Priority) ---
223
+ if (Math.abs(rawEvent.delta.y) < HORIZONTAL_GESTURE_MAX_Y_DRIFT) {
224
+ if (dragDeltaX > HORIZONTAL_GESTURE_DRAG_X_THRESHOLD) { // Intent to indent (drag right)
225
+ newProjectedDepth = initialDepth + 1; // Set intended depth for this gesture
226
+ isHorizontalGestureIntent = true;
227
+ if (activeItemFlatIndex > 0) {
228
+ const itemAbove = flatCurrentTree[activeItemFlatIndex - 1];
229
+ if (itemAbove.depth === initialDepth) { // Item above is a potential parent
230
+ const activeBranchIds = new Set(flattenTree([activeItemLocal]).map(i => i.id));
231
+ if (!activeBranchIds.has(itemAbove.id as number)) { // Avoid self-nesting
232
+ return { parentId: itemAbove.id, index: itemAbove.children.length, depth: newProjectedDepth };
233
+ }
234
+ }
235
+ }
236
+ // If not returned, newProjectedDepth (initialDepth + 1) is carried to fallback logic.
237
+ } else if (dragDeltaX < -HORIZONTAL_GESTURE_DRAG_X_THRESHOLD) { // Intent to outdent (drag left)
238
+ if (activeItemLocal.parent_id) { // Can only outdent if it has a parent
239
+ const parentInfo = findItemDeep(currentTree, activeItemLocal.parent_id);
240
+ if (parentInfo) {
241
+ newProjectedDepth = Math.max(0, initialDepth - 1); // Set intended depth
242
+ isHorizontalGestureIntent = true;
243
+ const grandParentId = parentInfo.parent?.id ?? null;
244
+ const newIndex = parentInfo.index + 1; // Place after original parent
245
+ return { parentId: grandParentId, index: newIndex, depth: newProjectedDepth };
246
+ }
247
+ } else { // Already at root, but a clear left drag was made
248
+ newProjectedDepth = 0; // Stay at root
249
+ isHorizontalGestureIntent = true;
250
+ // No immediate return, let fallback logic handle placing at root if needed,
251
+ // but with depth 0 confirmed.
252
+ }
253
+ }
254
+ }
255
+
256
+ // --- Fallback Logic ---
257
+ // `newProjectedDepth` now holds:
258
+ // 1. The result of an explicit horizontal gesture's intended depth (if gesture occurred & didn't return).
259
+ // 2. Or the default `getDragDepth` if no such gesture was detected.
260
+
261
+ if (!overItemLocal) {
262
+ // Dropping in empty space
263
+ if (newProjectedDepth === 0) {
264
+ return { parentId: null, index: currentTree.length, depth: 0 };
265
+ }
266
+ // Try to find a logical parent if indenting into empty space
267
+ const activeItemOriginalParentInfo = activeItemLocal.parent_id ? findItemDeep(currentTree, activeItemLocal.parent_id) : null;
268
+ if (activeItemOriginalParentInfo && activeItemOriginalParentInfo.item.depth + 1 === newProjectedDepth) {
269
+ const activeBranchIds = new Set(flattenTree([activeItemLocal]).map(i => i.id));
270
+ if (!activeBranchIds.has(activeItemOriginalParentInfo.item.id as number)) {
271
+ return { parentId: activeItemOriginalParentInfo.item.id, index: activeItemOriginalParentInfo.item.children.length, depth: newProjectedDepth };
272
+ }
273
+ }
274
+ const potentialParentsAtCorrectDepth = flatCurrentTree.filter(p => p.depth === newProjectedDepth - 1);
275
+ if (potentialParentsAtCorrectDepth.length > 0) {
276
+ const lastPotentialParent = potentialParentsAtCorrectDepth[potentialParentsAtCorrectDepth.length - 1];
277
+ const activeBranchIds = new Set(flattenTree([activeItemLocal]).map(i => i.id));
278
+ if (!activeBranchIds.has(lastPotentialParent.id as number)) {
279
+ return { parentId: lastPotentialParent.id, index: lastPotentialParent.children.length, depth: newProjectedDepth };
280
+ }
281
+ }
282
+ return { parentId: null, index: currentTree.length, depth: Math.max(0, newProjectedDepth) }; // Ensure depth is not negative
283
+ }
284
+
285
+ // Dropping over an existing item (`overItemLocal` is not null)
286
+ const overItemInfo = findItemDeep(currentTree, overItemLocal.id);
287
+ if (!overItemInfo) { // Should not happen if overItemLocal is valid
288
+ return { parentId: null, index: currentTree.length, depth: newProjectedDepth };
289
+ }
290
+
291
+ // If an explicit horizontal gesture did not set the depth (i.e., isHorizontalGestureIntent is false),
292
+ // then newProjectedDepth is still from the initial getDragDepth().
293
+ // If isHorizontalGestureIntent is true, newProjectedDepth reflects that intent.
294
+ // Now, cap this depth if we are dropping over an item.
295
+ // If an explicit horizontal gesture was intended, newProjectedDepth already reflects that.
296
+ // Only cap based on overItemInfo if it was NOT an explicit horizontal gesture that set the depth.
297
+ if (overItemInfo && !isHorizontalGestureIntent) {
298
+ newProjectedDepth = Math.min(newProjectedDepth, overItemInfo.item.depth + 1);
299
+ } else if (overItemInfo && isHorizontalGestureIntent) {
300
+ // If it WAS a horizontal gesture, we still might need to cap it if overItem is relevant
301
+ // and would lead to an invalid depth (e.g., indenting too far under overItem).
302
+ // However, the primary target of the gesture (itemAbove/originalParent) should have taken precedence.
303
+ // If we are here, it means the gesture's primary target wasn't met, and we are now considering overItem.
304
+ // In this specific case, capping is appropriate.
305
+ newProjectedDepth = Math.min(newProjectedDepth, overItemInfo.item.depth + 1);
306
+ }
307
+ newProjectedDepth = Math.max(0, newProjectedDepth); // Ensure depth is not negative
308
+
309
+
310
+ let targetParentId: UniqueIdentifier | null;
311
+ let targetIndex: number;
312
+
313
+ const overItemRect = rawEvent.over?.rect;
314
+ const pointerY = rawEvent.delta.y + (rawEvent.active.rect.current.initial?.top || 0);
315
+ const isDroppingBeforeOverItem = overItemRect ? pointerY < overItemRect.top + overItemRect.height / 2 : false;
316
+
317
+ if (newProjectedDepth > overItemInfo.item.depth) {
318
+ // Nesting: Make activeItem a child of overItemLocal
319
+ targetParentId = overItemInfo.item.id;
320
+ targetIndex = overItemInfo.item.children.length; // Default to last child
321
+ } else if (newProjectedDepth === overItemInfo.item.depth) {
322
+ // Reordering at the same level as overItemLocal
323
+ targetParentId = overItemInfo.parent?.id ?? null;
324
+ targetIndex = isDroppingBeforeOverItem ? overItemInfo.index : overItemInfo.index + 1;
325
+ } else { // newProjectedDepth < overItemInfo.item.depth (Outdenting relative to overItemLocal)
326
+ let ancestor = overItemInfo.parent; // Start with overItem's parent
327
+
328
+ while (ancestor && ancestor.depth > newProjectedDepth) {
329
+ const grandParentInfo = ancestor.parent_id ? findItemDeep(currentTree, ancestor.parent_id) : null;
330
+ ancestor = grandParentInfo?.item ?? null; // This is the new potential parent
331
+ ancestor = grandParentInfo?.item ?? null;
332
+ }
333
+ // ancestor is now the target parent (or null if root).
334
+ // ancestorParentInfo.item (if ancestor is not null) is the item whose child list we are inserting into.
335
+ // or currentTree if ancestor is null.
336
+ // The item we are placing relative to is the one whose depth is newProjectedDepth + 1
337
+ // which was the child of 'ancestor'. This is `overItemInfo.item` if it was already at that level,
338
+ // or the parent of `overItemInfo.item` if `overItemInfo.item` was deeper.
339
+
340
+ targetParentId = ancestor?.id ?? null;
341
+ let referenceItemForSiblingPlacement: HierarchicalNavItem | undefined | null;
342
+
343
+ if (targetParentId) { // Outdenting to a non-root level
344
+ // We need to find which child of 'ancestor' was the block we outdented from.
345
+ // This would be the item at depth `newProjectedDepth` that was an ancestor of `overItemLocal` or `overItemLocal` itself.
346
+ let itemToPlaceAfter = overItemInfo.item;
347
+ while(itemToPlaceAfter.parent_id !== targetParentId && typeof itemToPlaceAfter.parent_id === 'number') {
348
+ const parent = findItemDeep(currentTree, itemToPlaceAfter.parent_id as number); // Cast as number after check
349
+ if (!parent) break;
350
+ itemToPlaceAfter = parent.item;
351
+ }
352
+ referenceItemForSiblingPlacement = itemToPlaceAfter;
353
+ const siblings = ancestor?.children ?? [];
354
+ const refIndex = siblings.findIndex(s => s.id === referenceItemForSiblingPlacement?.id);
355
+ targetIndex = refIndex !== -1 ? refIndex + 1 : siblings.length;
356
+
357
+
358
+ } else { // Outdenting to root
359
+ // Find the root item that was the ancestor block of overItemLocal
360
+ let rootAncestor = overItemInfo.item;
361
+ while(typeof rootAncestor.parent_id === 'number') {
362
+ const parent = findItemDeep(currentTree, rootAncestor.parent_id as number); // Cast as number after check
363
+ if(!parent) break;
364
+ rootAncestor = parent.item;
365
+ }
366
+ referenceItemForSiblingPlacement = rootAncestor;
367
+ const rootItems = currentTree.filter(i => i.parent_id === null);
368
+ const refIndex = rootItems.findIndex(s => s.id === referenceItemForSiblingPlacement?.id);
369
+ targetIndex = refIndex !== -1 ? refIndex + 1 : rootItems.length;
370
+ }
371
+ // If dropping before the item that was the "parent block"
372
+ if (isDroppingBeforeOverItem && overItemLocal.id === referenceItemForSiblingPlacement?.id) {
373
+ targetIndex--;
374
+ }
375
+ }
376
+
377
+ // Ensure index is not out of bounds
378
+ const parentNodeForIndexCheck = targetParentId ? findItemDeep(currentTree, targetParentId)?.item : null;
379
+ const siblingsForIndexCheck = parentNodeForIndexCheck ? parentNodeForIndexCheck.children : currentTree.filter(i => i.parent_id === null);
380
+ targetIndex = Math.max(0, Math.min(targetIndex, siblingsForIndexCheck.length));
381
+
382
+ // Special fix for dragging to be the very first item at root
383
+ if (targetParentId === null && isDroppingBeforeOverItem && overItemInfo.parent === null && overItemInfo.index === 0) {
384
+ targetIndex = 0;
385
+ }
386
+
387
+ return { parentId: targetParentId, index: targetIndex, depth: newProjectedDepth };
388
+ }, []);
389
+
390
+
391
+ const onDragMove = (event: DragMoveEvent) => {
392
+ const { active, over, delta } = event;
393
+ if (!active) {
394
+ setProjected(null);
395
+ return;
396
+ }
397
+
398
+ const activeItemSearchResult = findItemDeep(hierarchicalItems, active.id);
399
+ if (!activeItemSearchResult) {
400
+ setProjected(null);
401
+ return;
402
+ }
403
+ const activeItem = activeItemSearchResult.item;
404
+
405
+ // Pure horizontal drag detection
406
+ const isPureHorizontalDrag = Math.abs(delta.x) > INDENTATION_WIDTH * 0.6 && Math.abs(delta.y) < 8;
407
+
408
+ if (isPureHorizontalDrag) {
409
+ const flatItems = flattenTree(hierarchicalItems);
410
+ const activeItemIndexInFlatList = flatItems.findIndex(item => item.id === active.id);
411
+
412
+ if (delta.x > 0) { // Right-drag (indent)
413
+ if (activeItemIndexInFlatList > 0) {
414
+ const itemAbove = flatItems[activeItemIndexInFlatList - 1];
415
+ // Ensure itemAbove is a valid potential parent (e.g., not a child of activeItem, and at a suitable depth)
416
+ // For simplicity here, we assume any item above can be a parent if it's not the active item itself or its descendant.
417
+ // More robust checks might be needed depending on exact requirements (e.g. itemAbove.depth === activeItem.depth)
418
+ // For now, we allow indenting under any item above it, if it's not part of the active branch.
419
+ const activeBranchIds = new Set(flattenTree([activeItem]).map(i => i.id));
420
+ if (!activeBranchIds.has(itemAbove.id as number) && itemAbove.depth >= activeItem.depth -1) { // Allow indenting under same level or one level up
421
+ setProjected({
422
+ parentId: itemAbove.id,
423
+ index: itemAbove.children.length,
424
+ depth: activeItem.depth + 1,
425
+ overId: itemAbove.id, // Set overId to the new parent
426
+ });
427
+ return;
428
+ }
429
+ }
430
+ } else { // Left-drag (outdent)
431
+ const foundItem = findItemDeep(hierarchicalItems, active.id);
432
+ if (!foundItem) return;
433
+ const { parent: currentParent } = foundItem;
434
+ const grandParentId = currentParent?.parent_id ?? null;
435
+
436
+ let insertIndex;
437
+ if (currentParent) {
438
+ const parentInfo = findItemDeep(hierarchicalItems, currentParent.id);
439
+ insertIndex = parentInfo ? parentInfo.index + 1 : hierarchicalItems.length;
440
+ } else {
441
+ // If no current parent, it's a root item. Outdenting means staying at root.
442
+ // We need to find its index among root items to place it correctly if it were to be moved.
443
+ // However, for a pure outdent, it just stays at its level.
444
+ // The setProjected below handles depth. The index might need adjustment if we allow reordering at root via pure outdent.
445
+ // For now, if it's a root item and outdenting, it effectively does nothing to its position, only confirms depth.
446
+ const rootItems = hierarchicalItems.filter(item => item.parent_id === null);
447
+ const currentRootIndex = rootItems.findIndex(item => item.id === active.id);
448
+ insertIndex = currentRootIndex +1; // This might not be ideal, depends on desired behavior for outdenting root.
449
+ // Let's assume it means placing it after itself if it's the last root, or after its original position.
450
+ // A more robust way would be to find its original parent's siblings.
451
+ }
452
+
453
+ // If currentParent is null, it's a root item. grandParentId is null.
454
+ // insertIndex calculation for root items:
455
+ if (!currentParent) {
456
+ const rootItems = hierarchicalItems.filter(i => i.parent_id === null);
457
+ const activeRootIndex = rootItems.findIndex(i => i.id === active.id);
458
+ // Place it after its current position among root items if outdenting from root
459
+ // Or at the end if it's somehow not found (should not happen)
460
+ insertIndex = activeRootIndex !== -1 ? activeRootIndex + 1 : rootItems.length;
461
+ }
462
+
463
+
464
+ setProjected({
465
+ parentId: grandParentId,
466
+ index: insertIndex,
467
+ depth: Math.max(0, activeItem.depth - 1),
468
+ overId: null, // No specific item is "over" in a pure outdent
469
+ });
470
+ return;
471
+ }
472
+ }
473
+
474
+ // Fallback to existing logic if not a pure horizontal drag or if conditions for indent/outdent weren't met
475
+ const overItemSearchResult = over ? findItemDeep(hierarchicalItems, over.id) : null;
476
+ const overItem = overItemSearchResult?.item ?? null;
477
+
478
+ if (over && activeItem) {
479
+ const activeBranchIds = new Set(flattenTree([activeItem]).map(i => i.id));
480
+ if (activeBranchIds.has(over.id as number)) {
481
+ setProjected(null);
482
+ return;
483
+ }
484
+ }
485
+
486
+ const newProjection = getProjectedDropPosition(activeItem, overItem, delta.x, hierarchicalItems, event);
487
+ setProjected({ ...newProjection, overId: over ? over.id : null });
488
+ };
489
+
490
+ const handleDragEnd = useCallback(async (event: DragEndEvent) => {
491
+ const { active } = event;
492
+ setActiveId(null);
493
+
494
+ if (!projected || !active.id) {
495
+ setProjected(null);
496
+ return;
497
+ }
498
+
499
+ const originalTree = structuredClone(hierarchicalItems);
500
+
501
+ const { newTree: treeWithoutActive, removedItemBranch } = removeItemFromTree(hierarchicalItems, active.id);
502
+ if (!removedItemBranch) {
503
+ setProjected(null);
504
+ return;
505
+ }
506
+
507
+ let tempParentId = projected.parentId;
508
+ let isSelfNesting = false;
509
+ while(tempParentId !== null){
510
+ if(tempParentId === active.id){
511
+ isSelfNesting = true;
512
+ break;
513
+ }
514
+ const parentNodeInfo = findItemDeep(treeWithoutActive, tempParentId);
515
+ if (!parentNodeInfo) break;
516
+ tempParentId = parentNodeInfo.item.parent_id ?? null;
517
+ }
518
+
519
+ if(isSelfNesting){
520
+ setProjected(null);
521
+ setHierarchicalItems(originalTree);
522
+ return;
523
+ }
524
+
525
+ function updateDepthRecursive(item: HierarchicalNavItem, newBaseDepth: number): HierarchicalNavItem {
526
+ const updatedItem = { ...item, depth: newBaseDepth };
527
+ if (updatedItem.children && updatedItem.children.length > 0) {
528
+ updatedItem.children = updatedItem.children.map(child => updateDepthRecursive(child, newBaseDepth + 1));
529
+ }
530
+ return updatedItem;
531
+ }
532
+ const branchWithCorrectedDepth = updateDepthRecursive(removedItemBranch, projected.depth);
533
+
534
+ const finalTreeWithAddedItem = addItemToTree(treeWithoutActive, branchWithCorrectedDepth, projected.parentId, projected.index);
535
+ const normalizedFinalTree = normalizeTree(finalTreeWithAddedItem);
536
+
537
+ setHierarchicalItems(normalizedFinalTree);
538
+
539
+ const itemsToUpdateDb = flattenTree(normalizedFinalTree).map(item => ({
540
+ id: item.id,
541
+ order: item.order,
542
+ parent_id: item.parent_id ?? null, // Coalesce undefined to null
543
+ }));
544
+
545
+ startTransition(async () => {
546
+ try {
547
+ const result = await updateNavigationStructureBatch(itemsToUpdateDb);
548
+ if (result?.error) {
549
+ console.error("Failed to update navigation structure:", result.error);
550
+ setHierarchicalItems(originalTree);
551
+ }
552
+ } catch (error) {
553
+ console.error("Exception during navigation update:", error);
554
+ setHierarchicalItems(originalTree);
555
+ }
556
+ });
557
+ setProjected(null);
558
+ }, [projected, hierarchicalItems, startTransition]);
559
+
560
+
561
+ const _handleDragStart = (event: DragStartEvent) => {
562
+ setActiveId(event.active.id);
563
+ setProjected(null);
564
+ };
565
+
566
+
567
+ const renderItemsRecursive = (itemsToRender: HierarchicalNavItem[]): (JSX.Element | null)[] => {
568
+ return itemsToRender.map((item) => {
569
+ if (!item) return null;
570
+
571
+ const itemInfo = findItemDeep(hierarchicalItems, item.id); // Get fresh info for index calculation
572
+
573
+ // Condition 1: Projected to be the SIBLING AFTER this item
574
+ const isProjectedSiblingAfterThis = projected &&
575
+ activeId && activeId !== item.id &&
576
+ projected.overId === item.id && // 'item' is what we are hovering/dragging past
577
+ projected.parentId === (item.parent_id ?? null) && // Target parent is this item's parent
578
+ itemInfo && projected.index === itemInfo.index + 1; // Target index is right after this item
579
+
580
+ // Condition 2: Projected to be the FIRST CHILD of this item
581
+ // This is for non-indent specific drops (e.g. from getProjectedDropPosition)
582
+ const isProjectedAsFirstChildOfThis = projected &&
583
+ activeId && activeId !== item.id &&
584
+ projected.parentId === item.id && // This item is the target parent
585
+ projected.index === 0 &&
586
+ // Ensure this isn't the pure indent case where item has no children (which is handled by LastChildIndent)
587
+ !(projected.overId === item.id && item.children.length === 0 && projected.index === 0);
588
+
589
+
590
+ // Condition 3: Projected to be the LAST CHILD of this item (specifically for pure horizontal indent)
591
+ // This is when 'item' is the 'itemAbove' from the indent logic, which becomes the parent.
592
+ const isProjectedAsLastChildOfThisIndent = projected &&
593
+ activeId && activeId !== item.id &&
594
+ projected.parentId === item.id && // This item is the target parent
595
+ projected.overId === item.id && // Crucially, the 'overId' points to this item (from pure indent logic)
596
+ projected.index === item.children.length; // Target index is at the end of current children
597
+
598
+ return (
599
+ <React.Fragment key={item.id}>
600
+ <SortableNavItem item={item} />
601
+
602
+ {/* Render projection line if it's a SIBLING AFTER this, OR if it's the FIRST CHILD of this (non-indent case) */}
603
+ {/* This line appears directly under 'item', before its children are rendered. */}
604
+ {(isProjectedSiblingAfterThis || isProjectedAsFirstChildOfThis) && (
605
+ <TableRow style={{opacity: 0.5}} className="pointer-events-none !p-0 h-1">
606
+ <TableCell style={{ paddingLeft: `${projected.depth * INDENTATION_WIDTH + 16}px`, height: '2px' }} className="py-0 border-none">
607
+ <div className="bg-primary h-0.5 w-full rounded-full"></div>
608
+ </TableCell>
609
+ <TableCell colSpan={5} className="py-0 border-none h-1"><div className="bg-primary h-0.5 w-full rounded-full"></div></TableCell>
610
+ </TableRow>
611
+ )}
612
+
613
+ {item.children && item.children.length > 0 && renderItemsRecursive(item.children)}
614
+
615
+ {/* Render projection line if it's the LAST CHILD of this (due to indent) */}
616
+ {/* This line appears after all of 'item's children, effectively at the bottom of 'item's group. */}
617
+ {isProjectedAsLastChildOfThisIndent && (
618
+ <TableRow style={{opacity: 0.5}} className="pointer-events-none !p-0 h-1">
619
+ <TableCell style={{ paddingLeft: `${projected.depth * INDENTATION_WIDTH + 16}px`, height: '2px' }} className="py-0 border-none">
620
+ <div className="bg-primary h-0.5 w-full rounded-full"></div>
621
+ </TableCell>
622
+ <TableCell colSpan={5} className="py-0 border-none h-1"><div className="bg-primary h-0.5 w-full rounded-full"></div></TableCell>
623
+ </TableRow>
624
+ )}
625
+ </React.Fragment>
626
+ );
627
+ });
628
+ };
629
+
630
+ const dropAnimation: DropAnimation = {
631
+ sideEffects: defaultDropAnimationSideEffects({
632
+ styles: { active: { opacity: '0.4' } },
633
+ }),
634
+ };
635
+
636
+ if (!isMounted) return null;
637
+
638
+ return (
639
+ <DndContext
640
+ sensors={sensors}
641
+ collisionDetection={closestCenter} // Consider other strategies if needed for precise boundary detection
642
+ onDragStart={_handleDragStart}
643
+ onDragEnd={handleDragEnd}
644
+ onDragMove={onDragMove}
645
+ onDragOver={onDragMove} // Use onDragOver as well if move isn't frequent enough
646
+ measuring={{ droppable: { strategy: MeasuringStrategy.Always } }}
647
+ >
648
+ <SortableContext items={flatItemIds} strategy={verticalListSortingStrategy}>
649
+ <div className="rounded-lg border overflow-hidden dark:border-slate-700">
650
+ <Table>
651
+ <TableHeader>
652
+ <TableRow className="dark:border-slate-700">
653
+ <TableHead className="w-[250px] sm:w-[350px] py-2.5">Label</TableHead>
654
+ <TableHead className="py-2.5">URL</TableHead>
655
+ <TableHead className="py-2.5">Order</TableHead>
656
+ <TableHead className="py-2.5">Parent</TableHead>
657
+ <TableHead className="py-2.5">Linked Page</TableHead>
658
+ <TableHead className="text-right w-[80px] py-2.5">Actions</TableHead>
659
+ </TableRow>
660
+ </TableHeader>
661
+ <TableBody>
662
+ {renderItemsRecursive(hierarchicalItems)}
663
+ {projected && projected.parentId === null && activeId && projected.index === hierarchicalItems.filter(i=>i.parent_id === null).length && (
664
+ <TableRow style={{opacity: 0.5}} className="pointer-events-none !p-0 h-1">
665
+ <TableCell style={{ paddingLeft: `${projected.depth * INDENTATION_WIDTH + 16}px`, height: '2px' }} className="py-0 border-none">
666
+ <div className="bg-primary h-0.5 w-full rounded-full"></div>
667
+ </TableCell>
668
+ <TableCell colSpan={5} className="py-0 border-none h-1"><div className="bg-primary h-0.5 w-full rounded-full"></div></TableCell>
669
+ </TableRow>
670
+ )}
671
+ </TableBody>
672
+ </Table>
673
+ </div>
674
+ {typeof document !== 'undefined' && createPortal(
675
+ <DragOverlay dropAnimation={dropAnimation} zIndex={1000}>
676
+ {activeId && activeItemDataForOverlay?.item ? (
677
+ <Table className="shadow-xl opacity-100 bg-card w-full">
678
+ <TableBody>
679
+ <TableRow className="bg-card hover:bg-card">
680
+ <TableCell style={{ paddingLeft: `${(projected?.depth ?? activeItemDataForOverlay.item.depth) * INDENTATION_WIDTH + 16}px` }} className="py-2">
681
+ <div className="flex items-center">
682
+ <Button variant="ghost" size="sm" className="cursor-grabbing mr-2 p-1 opacity-50"><GripVertical className="h-4 w-4 text-muted-foreground" /></Button>
683
+ <span className="font-medium">{activeItemDataForOverlay.item.label}</span>
684
+ </div>
685
+ </TableCell>
686
+ <TableCell className="text-xs text-muted-foreground max-w-[150px] truncate py-2" title={activeItemDataForOverlay.item.url}>{activeItemDataForOverlay.item.url}</TableCell>
687
+ <TableCell className="py-2"><Badge variant="outline">{activeItemDataForOverlay.item.order}</Badge></TableCell>
688
+ <TableCell className="text-xs text-muted-foreground py-2">{activeItemDataForOverlay.parent?.label || 'None'}</TableCell>
689
+ <TableCell className="text-xs text-muted-foreground py-2">{activeItemDataForOverlay.item.pageSlug ? `/${activeItemDataForOverlay.item.pageSlug}` : 'Manual URL'}</TableCell>
690
+ <TableCell className="text-right py-2"> <MoreHorizontal className="h-4 w-4 text-muted-foreground opacity-50" /> </TableCell>
691
+ </TableRow>
692
+ </TableBody>
693
+ </Table>
694
+ ) : null}
695
+ </DragOverlay>,
696
+ document.body
697
+ )}
698
+ </SortableContext>
699
+ </DndContext>
700
+ );
701
+ }