@morphika/andami 0.1.2

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 (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/admin/assets.ts +4 -0
  4. package/admin/database.ts +4 -0
  5. package/admin/index.ts +6 -0
  6. package/admin/login.ts +4 -0
  7. package/admin/navigation.ts +4 -0
  8. package/admin/pages-editor.ts +4 -0
  9. package/admin/pages.ts +4 -0
  10. package/admin/projects-editor.ts +4 -0
  11. package/admin/projects.ts +4 -0
  12. package/admin/settings.ts +4 -0
  13. package/admin/setup.ts +4 -0
  14. package/admin/storage.ts +4 -0
  15. package/admin/styles.ts +4 -0
  16. package/app/(site)/[slug]/loading.tsx +20 -0
  17. package/app/(site)/[slug]/page.tsx +83 -0
  18. package/app/(site)/error.tsx +32 -0
  19. package/app/(site)/layout.tsx +53 -0
  20. package/app/(site)/loading.tsx +20 -0
  21. package/app/(site)/not-found.tsx +41 -0
  22. package/app/(site)/page.tsx +43 -0
  23. package/app/(site)/preview/page.tsx +99 -0
  24. package/app/(site)/work/[slug]/loading.tsx +23 -0
  25. package/app/(site)/work/[slug]/page.tsx +84 -0
  26. package/app/admin/assets/page.tsx +573 -0
  27. package/app/admin/database/page.tsx +302 -0
  28. package/app/admin/error.tsx +53 -0
  29. package/app/admin/layout.tsx +273 -0
  30. package/app/admin/login/page.tsx +88 -0
  31. package/app/admin/navigation/page.tsx +157 -0
  32. package/app/admin/page.tsx +17 -0
  33. package/app/admin/pages/[slug]/page.tsx +849 -0
  34. package/app/admin/pages/page.tsx +588 -0
  35. package/app/admin/projects/[slug]/page.tsx +3 -0
  36. package/app/admin/projects/page.tsx +669 -0
  37. package/app/admin/settings/page.tsx +132 -0
  38. package/app/admin/setup/page.tsx +64 -0
  39. package/app/admin/storage/page.tsx +518 -0
  40. package/app/admin/styles/page.tsx +243 -0
  41. package/app/api/admin/assets/file/route.ts +81 -0
  42. package/app/api/admin/assets/health/route.ts +170 -0
  43. package/app/api/admin/assets/register/route.ts +163 -0
  44. package/app/api/admin/assets/registry/route.ts +98 -0
  45. package/app/api/admin/assets/relink/confirm/route.ts +242 -0
  46. package/app/api/admin/assets/relink/route.ts +202 -0
  47. package/app/api/admin/assets/scan/route.ts +271 -0
  48. package/app/api/admin/auth/route.ts +160 -0
  49. package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
  50. package/app/api/admin/custom-sections/route.ts +127 -0
  51. package/app/api/admin/database/route.ts +53 -0
  52. package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
  53. package/app/api/admin/pages/[slug]/route.ts +617 -0
  54. package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
  55. package/app/api/admin/pages/route.ts +129 -0
  56. package/app/api/admin/preview/route.ts +53 -0
  57. package/app/api/admin/r2/connect/route.ts +181 -0
  58. package/app/api/admin/r2/delete/route.ts +198 -0
  59. package/app/api/admin/r2/disconnect/route.ts +42 -0
  60. package/app/api/admin/r2/rename/route.ts +265 -0
  61. package/app/api/admin/r2/status/route.ts +106 -0
  62. package/app/api/admin/r2/upload-url/route.ts +148 -0
  63. package/app/api/admin/revalidate/route.ts +55 -0
  64. package/app/api/admin/settings/route.ts +279 -0
  65. package/app/api/admin/setup/complete/route.ts +51 -0
  66. package/app/api/admin/setup/route.ts +118 -0
  67. package/app/api/admin/storage/switch/route.ts +117 -0
  68. package/app/api/admin/styles/fonts/route.ts +97 -0
  69. package/app/api/admin/styles/route.ts +304 -0
  70. package/app/api/assets/[...path]/route.ts +98 -0
  71. package/app/api/custom-sections/[id]/route.ts +43 -0
  72. package/app/api/draft-mode/disable/route.ts +10 -0
  73. package/app/api/draft-mode/enable/route.ts +26 -0
  74. package/app/api/projects/route.ts +42 -0
  75. package/app/api/styles/route.ts +88 -0
  76. package/app/favicon.ico +0 -0
  77. package/app/globals.css +7 -0
  78. package/app/layout.tsx +53 -0
  79. package/app/robots.ts +17 -0
  80. package/app/sitemap.ts +48 -0
  81. package/app/studio/[[...index]]/page.tsx +8 -0
  82. package/components/admin/MetadataEditor.tsx +173 -0
  83. package/components/admin/PublishToggle.tsx +130 -0
  84. package/components/admin/icons.tsx +40 -0
  85. package/components/admin/nav-builder/NavBuilder.tsx +182 -0
  86. package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
  87. package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
  88. package/components/admin/nav-builder/NavGridCell.tsx +48 -0
  89. package/components/admin/nav-builder/NavGridItem.tsx +189 -0
  90. package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
  91. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
  92. package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
  93. package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
  94. package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
  95. package/components/admin/nav-builder/index.ts +10 -0
  96. package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
  97. package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
  98. package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
  99. package/components/admin/setup-wizard/DoneStep.tsx +187 -0
  100. package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
  101. package/components/admin/setup-wizard/StorageStep.tsx +308 -0
  102. package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
  103. package/components/admin/setup-wizard/index.ts +9 -0
  104. package/components/admin/styles/ColorsEditor.tsx +214 -0
  105. package/components/admin/styles/FontsEditor.tsx +258 -0
  106. package/components/admin/styles/GridLayoutEditor.tsx +292 -0
  107. package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
  108. package/components/admin/styles/TypographyEditor.tsx +266 -0
  109. package/components/admin/styles/index.ts +9 -0
  110. package/components/admin/styles/shared.tsx +68 -0
  111. package/components/blocks/BlockRenderer.tsx +404 -0
  112. package/components/blocks/ButtonBlockRenderer.tsx +52 -0
  113. package/components/blocks/CoverBlockRenderer.tsx +239 -0
  114. package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
  115. package/components/blocks/EnterAnimationWrapper.tsx +140 -0
  116. package/components/blocks/HoverAnimationWrapper.tsx +308 -0
  117. package/components/blocks/ImageBlockRenderer.tsx +61 -0
  118. package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
  119. package/components/blocks/PageBackground.tsx +28 -0
  120. package/components/blocks/PageNavAnimation.tsx +35 -0
  121. package/components/blocks/PageNavColor.tsx +24 -0
  122. package/components/blocks/PageRenderer.tsx +142 -0
  123. package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
  124. package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
  125. package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
  126. package/components/blocks/SectionRenderer.tsx +170 -0
  127. package/components/blocks/SectionV2Renderer.tsx +330 -0
  128. package/components/blocks/ShaderCanvas.tsx +392 -0
  129. package/components/blocks/SpacerBlockRenderer.tsx +17 -0
  130. package/components/blocks/TextBlockRenderer.tsx +87 -0
  131. package/components/blocks/TypewriterRichText.tsx +464 -0
  132. package/components/blocks/TypewriterWrapper.tsx +149 -0
  133. package/components/blocks/VideoBlockRenderer.tsx +304 -0
  134. package/components/blocks/index.ts +2 -0
  135. package/components/builder/AssetBrowser.tsx +2 -0
  136. package/components/builder/BlockLivePreview.tsx +101 -0
  137. package/components/builder/BlockTypePicker.tsx +178 -0
  138. package/components/builder/BuilderCanvas.tsx +354 -0
  139. package/components/builder/CanvasMinimap.tsx +200 -0
  140. package/components/builder/CanvasToolbar.tsx +202 -0
  141. package/components/builder/ColorPicker.tsx +243 -0
  142. package/components/builder/ColorSwatchPicker.tsx +274 -0
  143. package/components/builder/ColumnDragContext.tsx +51 -0
  144. package/components/builder/ColumnDragOverlay.tsx +110 -0
  145. package/components/builder/CustomSectionInstanceCard.tsx +97 -0
  146. package/components/builder/DeviceFrame.tsx +123 -0
  147. package/components/builder/DndWrapper.tsx +337 -0
  148. package/components/builder/InsertionLines.tsx +186 -0
  149. package/components/builder/ParallaxGroupCanvas.tsx +228 -0
  150. package/components/builder/ParallaxSlideHeader.tsx +113 -0
  151. package/components/builder/ReadOnlyFrame.tsx +417 -0
  152. package/components/builder/SectionEditorBar.tsx +288 -0
  153. package/components/builder/SectionTypePicker.tsx +422 -0
  154. package/components/builder/SectionV2Canvas.tsx +297 -0
  155. package/components/builder/SectionV2Column.tsx +488 -0
  156. package/components/builder/SettingsPanel.tsx +911 -0
  157. package/components/builder/SortableBlock.tsx +230 -0
  158. package/components/builder/SortableRow.tsx +362 -0
  159. package/components/builder/VirtualAssetGrid.tsx +397 -0
  160. package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
  161. package/components/builder/asset-browser/FileLightbox.tsx +116 -0
  162. package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
  163. package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
  164. package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
  165. package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
  166. package/components/builder/asset-browser/helpers.ts +88 -0
  167. package/components/builder/asset-browser/index.ts +1 -0
  168. package/components/builder/asset-browser/types.ts +49 -0
  169. package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
  170. package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
  171. package/components/builder/asset-browser/useR2Operations.ts +189 -0
  172. package/components/builder/blockStyles.tsx +295 -0
  173. package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
  174. package/components/builder/editors/CoverBlockEditor.tsx +488 -0
  175. package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
  176. package/components/builder/editors/HoverEffectPicker.tsx +209 -0
  177. package/components/builder/editors/ImageBlockEditor.tsx +206 -0
  178. package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
  179. package/components/builder/editors/ProjectGridEditor.tsx +648 -0
  180. package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
  181. package/components/builder/editors/StaggerSettings.tsx +108 -0
  182. package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
  183. package/components/builder/editors/TextBlockEditor.tsx +462 -0
  184. package/components/builder/editors/TextStylePicker.tsx +183 -0
  185. package/components/builder/editors/VideoBlockEditor.tsx +278 -0
  186. package/components/builder/editors/index.ts +10 -0
  187. package/components/builder/editors/shared.tsx +345 -0
  188. package/components/builder/hooks/useColumnDrag.ts +472 -0
  189. package/components/builder/hooks/useColumnResize.ts +221 -0
  190. package/components/builder/index.ts +12 -0
  191. package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
  192. package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
  193. package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
  194. package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
  195. package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
  196. package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
  197. package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
  198. package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
  199. package/components/builder/live-preview/index.ts +10 -0
  200. package/components/builder/live-preview/shared.tsx +153 -0
  201. package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
  202. package/components/builder/settings-panel/BlockSettings.tsx +94 -0
  203. package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
  204. package/components/builder/settings-panel/LayoutTab.tsx +310 -0
  205. package/components/builder/settings-panel/PageSettings.tsx +200 -0
  206. package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
  207. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
  208. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
  209. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
  210. package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
  211. package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
  212. package/components/builder/settings-panel/index.ts +19 -0
  213. package/components/builder/settings-panel/responsive-helpers.ts +524 -0
  214. package/components/ui/CustomCursor.tsx +118 -0
  215. package/components/ui/NavContentLightbox.tsx +152 -0
  216. package/components/ui/Navbar.tsx +582 -0
  217. package/components/ui/PortfolioTracker.tsx +87 -0
  218. package/components/ui/ScrollToTop.tsx +47 -0
  219. package/lib/animation/enter-presets.ts +147 -0
  220. package/lib/animation/enter-resolve.ts +90 -0
  221. package/lib/animation/enter-types.ts +128 -0
  222. package/lib/animation/hover-effect-presets.ts +210 -0
  223. package/lib/animation/hover-effect-types.ts +126 -0
  224. package/lib/asset-retry.ts +111 -0
  225. package/lib/assets.ts +92 -0
  226. package/lib/audit.ts +35 -0
  227. package/lib/auth-token.ts +94 -0
  228. package/lib/auth.ts +13 -0
  229. package/lib/builder/cascade-helpers.ts +51 -0
  230. package/lib/builder/cascade.ts +533 -0
  231. package/lib/builder/constants.ts +103 -0
  232. package/lib/builder/defaults.ts +182 -0
  233. package/lib/builder/history.ts +48 -0
  234. package/lib/builder/index.ts +21 -0
  235. package/lib/builder/layout-styles.ts +344 -0
  236. package/lib/builder/masonry.ts +166 -0
  237. package/lib/builder/responsive.ts +156 -0
  238. package/lib/builder/serializer.ts +845 -0
  239. package/lib/builder/store-blocks.ts +193 -0
  240. package/lib/builder/store-canvas.ts +319 -0
  241. package/lib/builder/store-helpers.ts +490 -0
  242. package/lib/builder/store-sections.ts +709 -0
  243. package/lib/builder/store.ts +333 -0
  244. package/lib/builder/templates.ts +297 -0
  245. package/lib/builder/types.ts +374 -0
  246. package/lib/builder/utils.ts +37 -0
  247. package/lib/color-utils.ts +116 -0
  248. package/lib/config/index.ts +57 -0
  249. package/lib/config/types.ts +122 -0
  250. package/lib/contexts/AssetContext.tsx +79 -0
  251. package/lib/contexts/NavAnimationContext.tsx +44 -0
  252. package/lib/contexts/NavColorContext.tsx +38 -0
  253. package/lib/contexts/PageExitContext.tsx +194 -0
  254. package/lib/contexts/ThumbStatusContext.tsx +83 -0
  255. package/lib/csrf-client.ts +34 -0
  256. package/lib/csrf.ts +68 -0
  257. package/lib/format-utils.ts +24 -0
  258. package/lib/hooks/useViewport.ts +42 -0
  259. package/lib/logger.ts +81 -0
  260. package/lib/revalidate.ts +23 -0
  261. package/lib/sanitize.ts +91 -0
  262. package/lib/sanity/client.ts +8 -0
  263. package/lib/sanity/queries.ts +486 -0
  264. package/lib/sanity/types.ts +869 -0
  265. package/lib/sanity/writeClient.ts +24 -0
  266. package/lib/security.ts +402 -0
  267. package/lib/setup/detect.ts +156 -0
  268. package/lib/shader/glsl/index.ts +27 -0
  269. package/lib/shader/glsl/pixelate.ts +51 -0
  270. package/lib/shader/glsl/rgb-shift.ts +45 -0
  271. package/lib/shader/glsl/ripple.ts +46 -0
  272. package/lib/shader/glsl/vertex.ts +14 -0
  273. package/lib/storage/index.ts +211 -0
  274. package/lib/storage/r2-adapter.ts +286 -0
  275. package/lib/storage/types.ts +125 -0
  276. package/lib/styles/provider.tsx +267 -0
  277. package/lib/thumbnails/generate.ts +151 -0
  278. package/lib/utils.ts +6 -0
  279. package/package.json +212 -0
  280. package/sanity/compose.ts +65 -0
  281. package/sanity/sanity.config.ts +126 -0
  282. package/sanity/schemas/assetRegistry.ts +301 -0
  283. package/sanity/schemas/blocks/blockLayout.ts +90 -0
  284. package/sanity/schemas/blocks/buttonBlock.ts +82 -0
  285. package/sanity/schemas/blocks/coverBlock.ts +229 -0
  286. package/sanity/schemas/blocks/imageBlock.ts +58 -0
  287. package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
  288. package/sanity/schemas/blocks/index.ts +9 -0
  289. package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
  290. package/sanity/schemas/blocks/spacerBlock.ts +41 -0
  291. package/sanity/schemas/blocks/textBlock.ts +139 -0
  292. package/sanity/schemas/blocks/videoBlock.ts +80 -0
  293. package/sanity/schemas/customSection.ts +69 -0
  294. package/sanity/schemas/customSectionInstance.ts +163 -0
  295. package/sanity/schemas/index.ts +111 -0
  296. package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
  297. package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
  298. package/sanity/schemas/objects/parallaxGroup.ts +66 -0
  299. package/sanity/schemas/objects/parallaxSlide.ts +217 -0
  300. package/sanity/schemas/objects/typewriterConfig.ts +38 -0
  301. package/sanity/schemas/page.ts +162 -0
  302. package/sanity/schemas/pageSection.ts +157 -0
  303. package/sanity/schemas/pageSectionV2.ts +269 -0
  304. package/sanity/schemas/siteSettings.ts +256 -0
  305. package/sanity/schemas/siteStyles.ts +212 -0
  306. package/site/error.ts +4 -0
  307. package/site/index.ts +8 -0
  308. package/site/not-found.ts +4 -0
  309. package/site/page.ts +4 -0
  310. package/site/preview.ts +4 -0
  311. package/site/robots.ts +4 -0
  312. package/site/sitemap.ts +4 -0
  313. package/site/work.ts +4 -0
  314. package/studio/index.ts +4 -0
  315. package/styles/admin.css +85 -0
  316. package/styles/animations.css +237 -0
  317. package/styles/base.css +148 -0
  318. package/styles/globals.css +10 -0
  319. package/tsconfig.json +25 -0
@@ -0,0 +1,588 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback } from "react";
4
+ import Link from "next/link";
5
+ import { useRouter } from "next/navigation";
6
+ import { csrfHeaders } from "../../../lib/csrf-client";
7
+ import type { PageListItem } from "../../../lib/sanity/types";
8
+ import PublishToggle from "../../../components/admin/PublishToggle";
9
+ import { EditIcon, DuplicateIcon, DeleteIcon, PreviewIcon } from "../../../components/admin/icons";
10
+
11
+ function HomeIcon({ active }: { active: boolean }) {
12
+ return (
13
+ <svg width="16" height="16" viewBox="0 0 24 24" fill={active ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.5">
14
+ <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
15
+ <polyline points="9 22 9 12 15 12 15 22" />
16
+ </svg>
17
+ );
18
+ }
19
+
20
+ // ============================================
21
+ // Create Page Modal
22
+ // ============================================
23
+
24
+ function CreatePageModal({
25
+ open,
26
+ onClose,
27
+ onCreated,
28
+ }: {
29
+ open: boolean;
30
+ onClose: () => void;
31
+ onCreated: () => void;
32
+ }) {
33
+ const [title, setTitle] = useState("");
34
+ const [slug, setSlug] = useState("");
35
+ const [error, setError] = useState("");
36
+ const [creating, setCreating] = useState(false);
37
+ const [autoSlug, setAutoSlug] = useState(true);
38
+
39
+ // Reset state when modal opens/closes
40
+ useEffect(() => {
41
+ if (open) {
42
+ setTitle("");
43
+ setSlug("");
44
+ setError("");
45
+ setAutoSlug(true);
46
+ }
47
+ }, [open]);
48
+
49
+ useEffect(() => {
50
+ if (!open) return;
51
+ const handleKey = (e: KeyboardEvent) => {
52
+ if (e.key === "Escape") onClose();
53
+ };
54
+ window.addEventListener("keydown", handleKey);
55
+ return () => window.removeEventListener("keydown", handleKey);
56
+ }, [open, onClose]);
57
+
58
+ useEffect(() => {
59
+ if (autoSlug && title) {
60
+ setSlug(
61
+ title
62
+ .toLowerCase()
63
+ .replace(/[^a-z0-9]+/g, "-")
64
+ .replace(/^-|-$/g, "")
65
+ );
66
+ }
67
+ }, [title, autoSlug]);
68
+
69
+ const handleSubmit = async (e: React.FormEvent) => {
70
+ e.preventDefault();
71
+ if (!title || !slug) {
72
+ setError("Title and slug are required");
73
+ return;
74
+ }
75
+ setCreating(true);
76
+ setError("");
77
+ try {
78
+ const res = await fetch("/api/admin/pages", {
79
+ method: "POST",
80
+ headers: { "Content-Type": "application/json", ...csrfHeaders() },
81
+ body: JSON.stringify({
82
+ title,
83
+ slug,
84
+ page_type: "page",
85
+ }),
86
+ });
87
+ const data = await res.json();
88
+ if (!res.ok) {
89
+ setError(data.error || "Failed to create page");
90
+ return;
91
+ }
92
+ setTitle("");
93
+ setSlug("");
94
+ setAutoSlug(true);
95
+ onCreated();
96
+ onClose();
97
+ } catch {
98
+ setError("Network error");
99
+ } finally {
100
+ setCreating(false);
101
+ }
102
+ };
103
+
104
+ if (!open) return null;
105
+
106
+ return (
107
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-6">
108
+ <div className="rounded-2xl border border-neutral-200/80 bg-white shadow-2xl w-full max-w-md">
109
+ <div className="p-6">
110
+ <h2 className="text-sm font-medium uppercase tracking-wider text-neutral-900 mb-1">
111
+ New Page
112
+ </h2>
113
+ <p className="text-xs text-neutral-400 mb-5">
114
+ Add sections and blocks after creating the page
115
+ </p>
116
+ <form onSubmit={handleSubmit} className="space-y-4">
117
+ <div>
118
+ <label className="block text-xs font-medium uppercase tracking-wider text-neutral-500 mb-1">
119
+ Title
120
+ </label>
121
+ <input
122
+ type="text"
123
+ value={title}
124
+ onChange={(e) => setTitle(e.target.value)}
125
+ placeholder="Page title"
126
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10"
127
+ autoFocus
128
+ />
129
+ </div>
130
+ <div>
131
+ <label className="block text-xs font-medium uppercase tracking-wider text-neutral-500 mb-1">
132
+ Slug
133
+ </label>
134
+ <input
135
+ type="text"
136
+ value={slug}
137
+ onChange={(e) => { setSlug(e.target.value); setAutoSlug(false); }}
138
+ placeholder="page-slug"
139
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10"
140
+ />
141
+ <p className="text-xs text-neutral-400 mt-1">URL: /{slug || "page-slug"}</p>
142
+ </div>
143
+ {error && <p className="text-xs text-[var(--admin-error)]">{error}</p>}
144
+ <div className="flex gap-3 justify-end pt-2">
145
+ <button
146
+ type="button"
147
+ onClick={onClose}
148
+ className="rounded-lg border border-neutral-200 px-5 py-2.5 text-sm text-neutral-500 hover:text-neutral-800 hover:border-neutral-300 transition-colors"
149
+ >
150
+ Cancel
151
+ </button>
152
+ <button
153
+ type="submit"
154
+ disabled={creating}
155
+ className="rounded-lg bg-[#076bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors disabled:opacity-50"
156
+ >
157
+ {creating ? "Creating..." : "Create"}
158
+ </button>
159
+ </div>
160
+ </form>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ );
165
+ }
166
+
167
+ // ============================================
168
+ // Delete Confirm Modal
169
+ // ============================================
170
+
171
+ function DeleteConfirmModal({
172
+ page,
173
+ onClose,
174
+ onDeleted,
175
+ }: {
176
+ page: PageListItem | null;
177
+ onClose: () => void;
178
+ onDeleted: () => void;
179
+ }) {
180
+ const [deleting, setDeleting] = useState(false);
181
+ const [error, setError] = useState("");
182
+
183
+ useEffect(() => {
184
+ if (!page) return;
185
+ const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
186
+ window.addEventListener("keydown", handleKey);
187
+ return () => window.removeEventListener("keydown", handleKey);
188
+ }, [page, onClose]);
189
+
190
+ useEffect(() => { setError(""); }, [page]);
191
+
192
+ if (!page) return null;
193
+
194
+ const handleDelete = async () => {
195
+ setDeleting(true);
196
+ setError("");
197
+ try {
198
+ const res = await fetch(`/api/admin/pages/${page.slug.current}`, { method: "DELETE", headers: { ...csrfHeaders() } });
199
+ if (res.ok) { onDeleted(); onClose(); }
200
+ else {
201
+ const data = await res.json().catch(() => ({ error: "Delete failed" }));
202
+ setError(data.error || `Delete failed (${res.status})`);
203
+ }
204
+ } catch { setError("Network error"); }
205
+ finally { setDeleting(false); }
206
+ };
207
+
208
+ return (
209
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
210
+ <div className="w-full max-w-sm rounded-2xl border border-neutral-200/80 bg-white p-6 shadow-2xl">
211
+ <h2 className="text-sm font-medium uppercase tracking-wider text-neutral-900 mb-3">Delete Page</h2>
212
+ <p className="text-xs text-neutral-500 mb-6">
213
+ Are you sure you want to delete &ldquo;{page.title}&rdquo;? This action cannot be undone.
214
+ </p>
215
+ {error && <p className="text-xs text-[var(--admin-error)] mb-4">{error}</p>}
216
+ <div className="flex gap-3 justify-end">
217
+ <button onClick={onClose} className="rounded-lg border border-neutral-200 px-5 py-2.5 text-sm text-neutral-500 hover:text-neutral-800 hover:border-neutral-300 transition-colors">Cancel</button>
218
+ <button onClick={handleDelete} disabled={deleting} className="rounded-lg bg-[var(--admin-error)] px-5 py-2.5 text-sm font-medium text-white hover:bg-[var(--admin-error-dark)] transition-colors disabled:opacity-50">{deleting ? "Deleting..." : "Delete"}</button>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ );
223
+ }
224
+
225
+ // ============================================
226
+ // Edit Page Settings Modal
227
+ // ============================================
228
+
229
+ function EditPageModal({
230
+ page,
231
+ onClose,
232
+ onUpdated,
233
+ }: {
234
+ page: PageListItem | null;
235
+ onClose: () => void;
236
+ onUpdated: () => void;
237
+ }) {
238
+ const [title, setTitle] = useState("");
239
+ const [slug, setSlug] = useState("");
240
+ const [error, setError] = useState("");
241
+ const [saving, setSaving] = useState(false);
242
+
243
+ useEffect(() => {
244
+ if (page) {
245
+ setTitle(page.title);
246
+ setSlug(page.slug.current);
247
+ setError("");
248
+ }
249
+ }, [page]);
250
+
251
+ useEffect(() => {
252
+ if (!page) return;
253
+ const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
254
+ window.addEventListener("keydown", handleKey);
255
+ return () => window.removeEventListener("keydown", handleKey);
256
+ }, [page, onClose]);
257
+
258
+ if (!page) return null;
259
+
260
+ const handleSubmit = async (e: React.FormEvent) => {
261
+ e.preventDefault();
262
+ if (!title.trim()) { setError("Title is required"); return; }
263
+ if (!slug.trim()) { setError("Slug is required"); return; }
264
+
265
+ setSaving(true);
266
+ setError("");
267
+ try {
268
+ const res = await fetch(`/api/admin/pages/${page.slug.current}`, {
269
+ method: "PATCH",
270
+ headers: { "Content-Type": "application/json", ...csrfHeaders() },
271
+ body: JSON.stringify({ title: title.trim(), newSlug: slug.trim() }),
272
+ });
273
+ const data = await res.json();
274
+ if (!res.ok) {
275
+ setError(data.error || "Failed to update");
276
+ return;
277
+ }
278
+ onUpdated();
279
+ onClose();
280
+ } catch {
281
+ setError("Network error");
282
+ } finally {
283
+ setSaving(false);
284
+ }
285
+ };
286
+
287
+ return (
288
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
289
+ <div className="w-full max-w-md rounded-2xl border border-neutral-200/80 bg-white p-6 shadow-2xl">
290
+ <h2 className="text-sm font-medium uppercase tracking-wider text-neutral-900 mb-4">
291
+ Page Settings
292
+ </h2>
293
+ <form onSubmit={handleSubmit} className="space-y-4">
294
+ <div>
295
+ <label className="block text-xs font-medium uppercase tracking-wider text-neutral-500 mb-1">Title</label>
296
+ <input
297
+ type="text"
298
+ value={title}
299
+ onChange={(e) => setTitle(e.target.value)}
300
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10"
301
+ autoFocus
302
+ />
303
+ </div>
304
+ <div>
305
+ <label className="block text-xs font-medium uppercase tracking-wider text-neutral-500 mb-1">Slug</label>
306
+ <input
307
+ type="text"
308
+ value={slug}
309
+ onChange={(e) => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""))}
310
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10"
311
+ />
312
+ <p className="text-xs text-neutral-400 mt-1">URL: /{slug || "page-slug"}</p>
313
+ </div>
314
+ {error && <p className="text-xs text-[var(--admin-error)]">{error}</p>}
315
+ <div className="flex gap-3 justify-end pt-2">
316
+ <button type="button" onClick={onClose} className="rounded-lg border border-neutral-200 px-5 py-2.5 text-sm text-neutral-500 hover:text-neutral-800 hover:border-neutral-300 transition-colors">Cancel</button>
317
+ <button type="submit" disabled={saving} className="rounded-lg bg-[#076bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors disabled:opacity-50">{saving ? "Saving..." : "Save"}</button>
318
+ </div>
319
+ </form>
320
+ </div>
321
+ </div>
322
+ );
323
+ }
324
+
325
+ // ============================================
326
+ // Home Confirm Modal
327
+ // ============================================
328
+
329
+ function HomeConfirmModal({
330
+ page,
331
+ onClose,
332
+ onConfirmed,
333
+ }: {
334
+ page: PageListItem | null;
335
+ onClose: () => void;
336
+ onConfirmed: () => void;
337
+ }) {
338
+ const [setting, setSetting] = useState(false);
339
+ const [error, setError] = useState("");
340
+
341
+ useEffect(() => {
342
+ if (!page) return;
343
+ const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
344
+ window.addEventListener("keydown", handleKey);
345
+ return () => window.removeEventListener("keydown", handleKey);
346
+ }, [page, onClose]);
347
+
348
+ useEffect(() => { setError(""); }, [page]);
349
+
350
+ if (!page) return null;
351
+
352
+ const handleConfirm = async () => {
353
+ setSetting(true);
354
+ setError("");
355
+ try {
356
+ const res = await fetch(`/api/admin/pages/${page.slug.current}/set-home`, {
357
+ method: "POST",
358
+ headers: { ...csrfHeaders() },
359
+ });
360
+ if (res.ok) {
361
+ onConfirmed();
362
+ onClose();
363
+ } else {
364
+ const data = await res.json().catch(() => ({ error: "Failed" }));
365
+ setError(data.error || `Failed (${res.status})`);
366
+ }
367
+ } catch {
368
+ setError("Network error");
369
+ } finally {
370
+ setSetting(false);
371
+ }
372
+ };
373
+
374
+ return (
375
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
376
+ <div className="w-full max-w-sm rounded-2xl border border-neutral-200/80 bg-white p-6 shadow-2xl">
377
+ <h2 className="text-sm font-medium uppercase tracking-wider text-neutral-900 mb-3">Set as Home Page</h2>
378
+ <p className="text-xs text-neutral-500 mb-6">
379
+ Do you want to set &ldquo;{page.title}&rdquo; as the index home page? The current home page will be unset.
380
+ </p>
381
+ {error && <p className="text-xs text-[var(--admin-error)] mb-4">{error}</p>}
382
+ <div className="flex gap-3 justify-end">
383
+ <button onClick={onClose} className="rounded-lg border border-neutral-200 px-5 py-2.5 text-sm text-neutral-500 hover:text-neutral-800 hover:border-neutral-300 transition-colors">Cancel</button>
384
+ <button onClick={handleConfirm} disabled={setting} className="rounded-lg bg-[#076bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors disabled:opacity-50">{setting ? "Setting..." : "Set as Home"}</button>
385
+ </div>
386
+ </div>
387
+ </div>
388
+ );
389
+ }
390
+
391
+ // ============================================
392
+ // Pages List Page (excludes projects)
393
+ // ============================================
394
+
395
+ export default function AdminPagesPage() {
396
+ const router = useRouter();
397
+ const [pages, setPages] = useState<PageListItem[]>([]);
398
+ const [loading, setLoading] = useState(true);
399
+ const [search, setSearch] = useState("");
400
+ const [showCreate, setShowCreate] = useState(false);
401
+ const [deletingPage, setDeletingPage] = useState<PageListItem | null>(null);
402
+ const [editingPage, setEditingPage] = useState<PageListItem | null>(null);
403
+ const [homeConfirmPage, setHomeConfirmPage] = useState<PageListItem | null>(null);
404
+ const [duplicating, setDuplicating] = useState<string | null>(null);
405
+ const fetchPages = useCallback(async () => {
406
+ try {
407
+ const res = await fetch("/api/admin/pages");
408
+ const data = await res.json();
409
+ // Exclude projects — they have their own section
410
+ setPages((data.pages || []).filter((p: PageListItem) => p.page_type !== "project"));
411
+ } catch {
412
+ setPages([]);
413
+ } finally {
414
+ setLoading(false);
415
+ }
416
+ }, []);
417
+
418
+ useEffect(() => { fetchPages(); }, [fetchPages]);
419
+
420
+ const filtered = pages.filter((p) => {
421
+ if (!search) return true;
422
+ const q = search.toLowerCase();
423
+ return p.title.toLowerCase().includes(q) || p.slug.current.toLowerCase().includes(q);
424
+ });
425
+
426
+
427
+
428
+ const handleDuplicate = async (page: PageListItem) => {
429
+ setDuplicating(page._id);
430
+ try {
431
+ const res = await fetch(`/api/admin/pages/${page.slug.current}/duplicate`, { method: "POST", headers: { ...csrfHeaders() } });
432
+ if (res.ok) {
433
+ await fetchPages();
434
+ } else {
435
+ const data = await res.json().catch(() => ({ error: "Duplicate failed" }));
436
+ console.error("Duplicate failed:", data.error || `HTTP ${res.status}`);
437
+ }
438
+ } catch (err) {
439
+ console.error("Duplicate operation failed:", err);
440
+ } finally {
441
+ setDuplicating(null);
442
+ }
443
+ };
444
+
445
+ const handleHomeClick = (page: PageListItem) => {
446
+ if (page.is_home) return; // Already home, do nothing
447
+ setHomeConfirmPage(page);
448
+ };
449
+
450
+ return (
451
+ <div>
452
+ {/* Header */}
453
+ <div className="flex items-center justify-between mb-6">
454
+ <h1 className="text-2xl font-semibold text-neutral-900">
455
+ Pages
456
+ </h1>
457
+ <button
458
+ onClick={() => setShowCreate(true)}
459
+ className="rounded-lg bg-[#076bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors"
460
+ >
461
+ + New Page
462
+ </button>
463
+ </div>
464
+
465
+ {/* Search */}
466
+ <div className="mb-6">
467
+ <input
468
+ type="text"
469
+ value={search}
470
+ onChange={(e) => setSearch(e.target.value)}
471
+ placeholder="Search by title or slug..."
472
+ className="w-full max-w-md rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10 shadow-sm"
473
+ />
474
+ </div>
475
+
476
+ {/* Page List */}
477
+ {loading ? (
478
+ <div className="flex items-center justify-center py-20">
479
+ <p className="text-xs text-neutral-400 animate-pulse">Loading pages...</p>
480
+ </div>
481
+ ) : filtered.length === 0 ? (
482
+ <div className="flex flex-col items-center justify-center py-20 border border-dashed border-neutral-300 rounded">
483
+ <p className="text-sm text-neutral-500 mb-2">
484
+ {pages.length === 0 ? "No pages yet" : "No pages match your search"}
485
+ </p>
486
+ {pages.length === 0 && (
487
+ <button onClick={() => setShowCreate(true)} className="text-xs text-[#076bff] hover:underline">
488
+ Create your first page
489
+ </button>
490
+ )}
491
+ </div>
492
+ ) : (
493
+ <div className="space-y-1">
494
+ {/* Table header */}
495
+ <div className="hidden md:grid grid-cols-[1fr_60px_160px_120px_160px] gap-4 px-4 py-2 border-b border-neutral-200">
496
+ <span className="text-xs font-medium uppercase tracking-wider text-neutral-400">Title</span>
497
+ <span className="text-xs font-medium uppercase tracking-wider text-neutral-400">Home</span>
498
+ <span className="text-xs font-medium uppercase tracking-wider text-neutral-400">Status</span>
499
+ <span className="text-xs font-medium uppercase tracking-wider text-neutral-400">Created</span>
500
+ <span className="text-xs font-medium uppercase tracking-wider text-neutral-400 text-right">Actions</span>
501
+ </div>
502
+
503
+ {filtered.map((page) => (
504
+ <div
505
+ key={page._id}
506
+ className="group flex flex-col gap-2 rounded-xl bg-white px-4 py-3 hover:bg-neutral-50/50 transition-colors border border-transparent hover:border-neutral-200 md:grid md:grid-cols-[1fr_60px_160px_120px_160px] md:gap-4 md:items-center"
507
+ >
508
+ {/* Title */}
509
+ <div className="cursor-pointer" onClick={() => router.push(`/admin/pages/${page.slug.current}`)}>
510
+ <p className="text-sm text-neutral-900 group-hover:text-[#076bff] transition-colors">
511
+ {page.title}
512
+ </p>
513
+ <p className="text-xs text-neutral-400">/{page.slug.current}</p>
514
+ </div>
515
+
516
+ {/* Home toggle */}
517
+ <button
518
+ onClick={(e) => { e.stopPropagation(); handleHomeClick(page); }}
519
+ className={`p-1.5 rounded w-fit transition-colors ${
520
+ page.is_home
521
+ ? "text-amber-500 cursor-default"
522
+ : "text-neutral-300 hover:text-amber-400 cursor-pointer"
523
+ }`}
524
+ title={page.is_home ? "Home page" : "Set as home page"}
525
+ >
526
+ <HomeIcon active={!!page.is_home} />
527
+ </button>
528
+
529
+ {/* Status */}
530
+ <div className="flex items-center">
531
+ <PublishToggle
532
+ mode="api"
533
+ isDraft={!!page.draft_mode}
534
+ slug={page.slug.current}
535
+ onToggled={fetchPages}
536
+ />
537
+ </div>
538
+
539
+ {/* Date */}
540
+ <span className="text-xs text-neutral-500">
541
+ {page.published_at ? new Date(page.published_at).toLocaleDateString() : "\u2014"}
542
+ </span>
543
+
544
+ {/* Actions */}
545
+ <div className="flex items-center gap-1 justify-end">
546
+ <button
547
+ onClick={(e) => { e.stopPropagation(); setEditingPage(page); }}
548
+ className="p-1.5 rounded text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100 transition-colors"
549
+ title="Edit settings"
550
+ >
551
+ <EditIcon />
552
+ </button>
553
+ <button
554
+ onClick={(e) => { e.stopPropagation(); handleDuplicate(page); }}
555
+ disabled={duplicating === page._id}
556
+ className="p-1.5 rounded text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100 transition-colors disabled:opacity-30"
557
+ title="Duplicate"
558
+ >
559
+ <DuplicateIcon />
560
+ </button>
561
+ <button
562
+ onClick={(e) => { e.stopPropagation(); setDeletingPage(page); }}
563
+ className="p-1.5 rounded text-neutral-400 hover:text-red-500 hover:bg-red-50 transition-colors"
564
+ title="Delete"
565
+ >
566
+ <DeleteIcon />
567
+ </button>
568
+ <Link
569
+ href={page.is_home ? "/" : `/${page.slug.current}`}
570
+ target="_blank"
571
+ className="p-1.5 rounded text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100 transition-colors"
572
+ title="Preview"
573
+ >
574
+ <PreviewIcon />
575
+ </Link>
576
+ </div>
577
+ </div>
578
+ ))}
579
+ </div>
580
+ )}
581
+
582
+ <CreatePageModal open={showCreate} onClose={() => setShowCreate(false)} onCreated={fetchPages} />
583
+ <EditPageModal page={editingPage} onClose={() => setEditingPage(null)} onUpdated={fetchPages} />
584
+ <DeleteConfirmModal page={deletingPage} onClose={() => setDeletingPage(null)} onDeleted={fetchPages} />
585
+ <HomeConfirmModal page={homeConfirmPage} onClose={() => setHomeConfirmPage(null)} onConfirmed={fetchPages} />
586
+ </div>
587
+ );
588
+ }
@@ -0,0 +1,3 @@
1
+ // Re-export the page editor — projects use the same builder UI as pages.
2
+ // This route exists so the sidebar correctly highlights "Projects" when editing a project.
3
+ export { default } from "../../../../app/admin/pages/[slug]/page";