@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,518 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback, Suspense } from "react";
4
+ import { AssetBrowserInline } from "../../../components/builder/AssetBrowser";
5
+ import { csrfHeaders } from "../../../lib/csrf-client";
6
+ import { formatDate, formatBytes } from "../../../lib/format-utils";
7
+
8
+ // ============================================
9
+ // Types
10
+ // ============================================
11
+
12
+ interface R2Status {
13
+ connected: boolean;
14
+ bucket_name: string | null;
15
+ public_url: string | null;
16
+ endpoint: string | null;
17
+ connected_at: string | null;
18
+ storage_used_bytes: number | null;
19
+ object_count: number | null;
20
+ }
21
+
22
+ interface ScanResult {
23
+ scanned_count: number;
24
+ new_assets: number;
25
+ updated_assets: number;
26
+ relinked_assets: number;
27
+ missing_assets: number;
28
+ total_assets: number;
29
+ thumbnails_found: number;
30
+ thumbnails_missing: number;
31
+ }
32
+
33
+ // ============================================
34
+ // SVG Icons
35
+ // ============================================
36
+
37
+ function R2Icon({ size = 20 }: { size?: number }) {
38
+ return (
39
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="#F6821F" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
40
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
41
+ <polyline points="3.27 6.96 12 12.01 20.73 6.96" />
42
+ <line x1="12" y1="22.08" x2="12" y2="12" />
43
+ </svg>
44
+ );
45
+ }
46
+
47
+ // ============================================
48
+ // R2 Connection Form
49
+ // ============================================
50
+
51
+ function R2ConnectForm({
52
+ onSuccess,
53
+ onCancel,
54
+ }: {
55
+ onSuccess: () => void;
56
+ onCancel: () => void;
57
+ }) {
58
+ const [endpoint, setEndpoint] = useState("");
59
+ const [bucketName, setBucketName] = useState("");
60
+ const [accessKeyId, setAccessKeyId] = useState("");
61
+ const [secretAccessKey, setSecretAccessKey] = useState("");
62
+ const [publicUrl, setPublicUrl] = useState("");
63
+ const [testing, setTesting] = useState(false);
64
+ const [error, setError] = useState<string | null>(null);
65
+
66
+ const handleSubmit = async (e: React.FormEvent) => {
67
+ e.preventDefault();
68
+ setTesting(true);
69
+ setError(null);
70
+
71
+ try {
72
+ const res = await fetch("/api/admin/r2/connect", {
73
+ method: "POST",
74
+ headers: {
75
+ "Content-Type": "application/json",
76
+ ...csrfHeaders(),
77
+ },
78
+ body: JSON.stringify({
79
+ endpoint: endpoint.trim(),
80
+ bucketName: bucketName.trim(),
81
+ accessKeyId: accessKeyId.trim(),
82
+ secretAccessKey: secretAccessKey.trim(),
83
+ publicUrl: publicUrl.trim(),
84
+ }),
85
+ });
86
+
87
+ const data = await res.json();
88
+ if (!res.ok) {
89
+ setError(data.error || "Connection failed");
90
+ return;
91
+ }
92
+
93
+ onSuccess();
94
+ } catch (err) {
95
+ setError(err instanceof Error ? err.message : "Connection failed");
96
+ } finally {
97
+ setTesting(false);
98
+ }
99
+ };
100
+
101
+ const inputClass =
102
+ "w-full rounded-lg border border-neutral-200 bg-white px-3 py-2 text-sm text-neutral-900 placeholder:text-neutral-400 focus:outline-none focus:ring-2 focus:ring-[#F6821F]/30 focus:border-[#F6821F]";
103
+ const labelClass = "block text-xs font-medium text-neutral-600 mb-1";
104
+
105
+ return (
106
+ <form onSubmit={handleSubmit} className="mt-4 space-y-3">
107
+ <div>
108
+ <label className={labelClass}>S3 Endpoint</label>
109
+ <input
110
+ type="url"
111
+ value={endpoint}
112
+ onChange={(e) => setEndpoint(e.target.value)}
113
+ placeholder="https://<account-id>.r2.cloudflarestorage.com"
114
+ className={inputClass}
115
+ required
116
+ />
117
+ <p className="text-[11px] text-neutral-400 mt-0.5">
118
+ Found in Cloudflare R2 dashboard → Account Details
119
+ </p>
120
+ </div>
121
+
122
+ <div>
123
+ <label className={labelClass}>Bucket Name</label>
124
+ <input
125
+ type="text"
126
+ value={bucketName}
127
+ onChange={(e) => setBucketName(e.target.value)}
128
+ placeholder="my-assets"
129
+ className={inputClass}
130
+ required
131
+ />
132
+ </div>
133
+
134
+ <div>
135
+ <label className={labelClass}>Access Key ID</label>
136
+ <input
137
+ type="text"
138
+ value={accessKeyId}
139
+ onChange={(e) => setAccessKeyId(e.target.value)}
140
+ placeholder="R2 API token Access Key ID"
141
+ className={inputClass}
142
+ required
143
+ autoComplete="off"
144
+ />
145
+ </div>
146
+
147
+ <div>
148
+ <label className={labelClass}>Secret Access Key</label>
149
+ <input
150
+ type="password"
151
+ value={secretAccessKey}
152
+ onChange={(e) => setSecretAccessKey(e.target.value)}
153
+ placeholder="R2 API token Secret Access Key"
154
+ className={inputClass}
155
+ required
156
+ autoComplete="off"
157
+ />
158
+ </div>
159
+
160
+ <div>
161
+ <label className={labelClass}>Public Bucket URL</label>
162
+ <input
163
+ type="url"
164
+ value={publicUrl}
165
+ onChange={(e) => setPublicUrl(e.target.value)}
166
+ placeholder="https://pub-xxx.r2.dev or https://assets.example.com"
167
+ className={inputClass}
168
+ required
169
+ />
170
+ <p className="text-[11px] text-neutral-400 mt-0.5">
171
+ R2.dev subdomain or custom domain with public access enabled
172
+ </p>
173
+ </div>
174
+
175
+ {error && (
176
+ <div className="p-2.5 rounded-lg bg-red-50 border border-red-200">
177
+ <p className="text-xs text-red-700">{error}</p>
178
+ </div>
179
+ )}
180
+
181
+ <div className="flex items-center gap-2 pt-1">
182
+ <button
183
+ type="submit"
184
+ disabled={testing}
185
+ className="inline-flex items-center gap-2 rounded-lg bg-[#F6821F] text-white text-sm font-medium px-4 py-2 hover:bg-[#F6821F]/90 transition-colors disabled:opacity-50"
186
+ >
187
+ {testing ? (
188
+ <>
189
+ <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
190
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
191
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
192
+ </svg>
193
+ Testing Connection...
194
+ </>
195
+ ) : (
196
+ "Test & Connect"
197
+ )}
198
+ </button>
199
+ <button
200
+ type="button"
201
+ onClick={onCancel}
202
+ className="text-xs text-neutral-400 hover:text-neutral-600 transition-colors px-3 py-2"
203
+ >
204
+ Cancel
205
+ </button>
206
+ </div>
207
+ </form>
208
+ );
209
+ }
210
+
211
+ // ============================================
212
+ // Page Component
213
+ // ============================================
214
+
215
+ export default function AdminStoragePage() {
216
+ return (
217
+ <Suspense
218
+ fallback={
219
+ <div className="flex items-center justify-center py-20">
220
+ <span className="text-sm text-neutral-400 animate-pulse">
221
+ Loading storage...
222
+ </span>
223
+ </div>
224
+ }
225
+ >
226
+ <AdminStorageContent />
227
+ </Suspense>
228
+ );
229
+ }
230
+
231
+ function AdminStorageContent() {
232
+ // ── State ──
233
+ const [r2Status, setR2Status] = useState<R2Status | null>(null);
234
+ const [loading, setLoading] = useState(true);
235
+ const [disconnectingR2, setDisconnectingR2] = useState(false);
236
+ const [showR2Form, setShowR2Form] = useState(false);
237
+ const [scanResult, setScanResult] = useState<ScanResult | null>(null);
238
+ const [successMessage, setSuccessMessage] = useState<string | null>(null);
239
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
240
+ const [lastScannedAt, setLastScannedAt] = useState<string | null>(null);
241
+ const [browserRefreshKey] = useState(0);
242
+ const [thumbWarningDismissed, setThumbWarningDismissed] = useState(false);
243
+
244
+ // ── Fetch statuses ──
245
+ const fetchR2Status = useCallback(async () => {
246
+ try {
247
+ const res = await fetch("/api/admin/r2/status");
248
+ if (res.ok) {
249
+ const data: R2Status = await res.json();
250
+ setR2Status(data);
251
+ }
252
+ } catch {
253
+ // silent
254
+ }
255
+ }, []);
256
+
257
+ const fetchLastScan = useCallback(async () => {
258
+ try {
259
+ const res = await fetch("/api/admin/assets/registry");
260
+ if (res.ok) {
261
+ const data = await res.json();
262
+ setLastScannedAt(data.registry?.last_scanned_at || null);
263
+ }
264
+ } catch {
265
+ // silent
266
+ }
267
+ }, []);
268
+
269
+ useEffect(() => {
270
+ Promise.all([fetchR2Status(), fetchLastScan()]).finally(
271
+ () => setLoading(false)
272
+ );
273
+ }, [fetchR2Status, fetchLastScan]);
274
+
275
+ // ── Handlers ──
276
+
277
+ const handleR2Connected = () => {
278
+ setShowR2Form(false);
279
+ setSuccessMessage("Cloudflare R2 connected successfully!");
280
+ fetchR2Status();
281
+ setTimeout(() => setSuccessMessage(null), 5000);
282
+ };
283
+
284
+ const handleDisconnectR2 = async () => {
285
+ if (!confirm("Are you sure you want to disconnect Cloudflare R2?")) return;
286
+ setDisconnectingR2(true);
287
+ try {
288
+ const res = await fetch("/api/admin/r2/disconnect", {
289
+ method: "POST",
290
+ headers: { ...csrfHeaders() },
291
+ });
292
+ if (!res.ok) throw new Error("Disconnect failed");
293
+ setR2Status({ connected: false, bucket_name: null, public_url: null, endpoint: null, connected_at: null, storage_used_bytes: null, object_count: null });
294
+ setSuccessMessage("Cloudflare R2 disconnected");
295
+ setTimeout(() => setSuccessMessage(null), 4000);
296
+ } catch (err) {
297
+ setErrorMessage(err instanceof Error ? err.message : "Disconnect failed");
298
+ } finally {
299
+ setDisconnectingR2(false);
300
+ }
301
+ };
302
+
303
+ // Called by AssetBrowserInline after a successful scan
304
+ const handleScanComplete = useCallback((result: Record<string, unknown>) => {
305
+ setScanResult(result as unknown as ScanResult);
306
+ setLastScannedAt(new Date().toISOString());
307
+ }, []);
308
+
309
+ const [r2DetailsOpen, setR2DetailsOpen] = useState(false);
310
+
311
+ // ── Loading state ──
312
+ if (loading) {
313
+ return (
314
+ <div className="flex items-center justify-center py-20">
315
+ <span className="text-sm text-neutral-400 animate-pulse">Loading...</span>
316
+ </div>
317
+ );
318
+ }
319
+
320
+ return (
321
+ <div className="space-y-4">
322
+ {/* Header */}
323
+ <div className="flex items-center justify-between mb-2">
324
+ <h1 className="text-2xl font-semibold text-neutral-900">Storage</h1>
325
+ </div>
326
+
327
+ {/* Success / Error banners */}
328
+ {successMessage && (
329
+ <div className="p-3 rounded-xl border border-green-200 bg-green-50">
330
+ <p className="text-sm text-green-700">{successMessage}</p>
331
+ </div>
332
+ )}
333
+ {errorMessage && (
334
+ <div className="p-3 rounded-xl border border-red-200 bg-red-50">
335
+ <p className="text-sm text-red-700">{errorMessage}</p>
336
+ </div>
337
+ )}
338
+
339
+ {/* ====== STORAGE PROVIDER CARD ====== */}
340
+ <div className="max-w-xl">
341
+ {/* ── Cloudflare R2 Card ── */}
342
+ <div className="border rounded-xl bg-white border-[#F6821F]/40 ring-1 ring-[#F6821F]/20">
343
+ {/* Main row — always visible */}
344
+ <div className="flex items-center gap-3 px-4 py-3">
345
+ <div className="w-8 h-8 rounded-lg bg-[#F6821F]/10 flex items-center justify-center shrink-0">
346
+ <R2Icon size={16} />
347
+ </div>
348
+ <div className="flex items-center gap-2 min-w-0 flex-1">
349
+ <h2 className="text-sm font-semibold text-neutral-900 whitespace-nowrap">Cloudflare R2</h2>
350
+ {r2Status?.connected && (
351
+ <>
352
+ <span className="inline-flex items-center text-[9px] font-semibold uppercase tracking-wider text-[#F6821F] bg-[#F6821F]/10 px-1.5 py-0.5 rounded-full">
353
+ Active
354
+ </span>
355
+ <span className="inline-flex items-center gap-1 text-[10px] font-medium text-green-600">
356
+ <span className="w-1.5 h-1.5 rounded-full bg-green-500" />
357
+ </span>
358
+ </>
359
+ )}
360
+ </div>
361
+
362
+ {/* R2 capacity bar — inline right side */}
363
+ {r2Status?.connected && r2Status.storage_used_bytes != null && (() => {
364
+ const R2_FREE_TIER_BYTES = 10 * 1024 * 1024 * 1024;
365
+ const usedBytes = r2Status.storage_used_bytes!;
366
+ const pct = Math.min((usedBytes / R2_FREE_TIER_BYTES) * 100, 100);
367
+ const barColor =
368
+ pct >= 90 ? "bg-red-500" : pct >= 70 ? "bg-amber-500" : "bg-[#F6821F]";
369
+ return (
370
+ <div className="flex items-center gap-2 ml-auto shrink-0">
371
+ <span className="text-[10px] text-neutral-500 whitespace-nowrap">
372
+ {formatBytes(usedBytes)} <span className="text-neutral-400">of 10 GB</span>
373
+ </span>
374
+ <div className="w-20 h-1.5 bg-neutral-100 rounded-full overflow-hidden">
375
+ <div
376
+ className={`h-full rounded-full transition-all duration-500 ${barColor}`}
377
+ style={{ width: `${Math.max(pct, 0.5)}%` }}
378
+ />
379
+ </div>
380
+ {r2Status.object_count != null && (
381
+ <span className="text-[10px] text-neutral-400 whitespace-nowrap">
382
+ {r2Status.object_count.toLocaleString()} files
383
+ </span>
384
+ )}
385
+ </div>
386
+ );
387
+ })()}
388
+
389
+ {/* Actions */}
390
+ <div className="flex items-center gap-1 ml-auto shrink-0">
391
+ {r2Status?.connected ? (
392
+ <button
393
+ onClick={() => setR2DetailsOpen(!r2DetailsOpen)}
394
+ className="text-neutral-400 hover:text-neutral-600 transition-colors p-1 rounded-md hover:bg-neutral-50"
395
+ title="Details"
396
+ >
397
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
398
+ className={`transition-transform duration-200 ${r2DetailsOpen ? "rotate-180" : ""}`}>
399
+ <polyline points="6 9 12 15 18 9" />
400
+ </svg>
401
+ </button>
402
+ ) : !showR2Form ? (
403
+ <button
404
+ onClick={() => setShowR2Form(true)}
405
+ className="inline-flex items-center gap-1.5 rounded-lg bg-[#F6821F] text-white text-xs font-medium px-3 py-1.5 hover:bg-[#F6821F]/90 transition-colors"
406
+ >
407
+ Connect R2
408
+ </button>
409
+ ) : null}
410
+ </div>
411
+ </div>
412
+
413
+ {/* Collapsible details */}
414
+ {r2DetailsOpen && r2Status?.connected && (
415
+ <div className="border-t border-neutral-100 px-4 py-3 space-y-2">
416
+ <div className="flex items-center justify-between">
417
+ <div className="text-xs text-neutral-400">
418
+ <span className="text-neutral-500 font-medium">Bucket:</span> {r2Status.bucket_name}
419
+ {r2Status.connected_at && (
420
+ <span> · since {formatDate(r2Status.connected_at)}</span>
421
+ )}
422
+ </div>
423
+ <button
424
+ onClick={handleDisconnectR2}
425
+ disabled={disconnectingR2}
426
+ className="text-[11px] text-neutral-400 hover:text-red-500 hover:bg-red-50 transition-colors px-2 py-0.5 rounded"
427
+ >
428
+ {disconnectingR2 ? "..." : "Disconnect"}
429
+ </button>
430
+ </div>
431
+ {r2Status.public_url && (
432
+ <div className="text-xs text-neutral-400">
433
+ <span className="text-neutral-500 font-medium">URL:</span>{" "}
434
+ {r2Status.public_url}
435
+ </div>
436
+ )}
437
+ </div>
438
+ )}
439
+
440
+ {/* R2 connection form (when connecting) */}
441
+ {showR2Form && !r2Status?.connected && (
442
+ <div className="px-4 pb-4">
443
+ <R2ConnectForm
444
+ onSuccess={handleR2Connected}
445
+ onCancel={() => setShowR2Form(false)}
446
+ />
447
+ </div>
448
+ )}
449
+ </div>
450
+ </div>
451
+
452
+ {/* ====== SCAN RESULT ====== */}
453
+ {scanResult && (
454
+ <div className="p-3 rounded-xl bg-neutral-50 border border-neutral-200">
455
+ <p className="text-sm text-neutral-700">
456
+ Found <span className="font-semibold">{scanResult.scanned_count}</span> files
457
+ {scanResult.new_assets > 0 && (
458
+ <span className="text-blue-500"> · {scanResult.new_assets} new</span>
459
+ )}
460
+ {scanResult.updated_assets > 0 && (
461
+ <span className="text-green-500"> · {scanResult.updated_assets} updated</span>
462
+ )}
463
+ {scanResult.relinked_assets > 0 && (
464
+ <span className="text-purple-500"> · {scanResult.relinked_assets} relinked</span>
465
+ )}
466
+ {scanResult.missing_assets > 0 && (
467
+ <span className="text-red-500"> · {scanResult.missing_assets} missing</span>
468
+ )}
469
+ </p>
470
+ {/* Thumbnail stats */}
471
+ {(scanResult.thumbnails_found > 0 || scanResult.thumbnails_missing > 0) && (
472
+ <p className="text-xs text-neutral-500 mt-1.5">
473
+ Thumbnails:{" "}
474
+ <span className="text-green-600 font-medium">{scanResult.thumbnails_found} found</span>
475
+ {scanResult.thumbnails_missing > 0 && (
476
+ <span className="text-amber-600 font-medium"> · {scanResult.thumbnails_missing} missing</span>
477
+ )}
478
+ </p>
479
+ )}
480
+ </div>
481
+ )}
482
+
483
+ {/* Stale thumbnails warning */}
484
+ {scanResult &&
485
+ !thumbWarningDismissed &&
486
+ scanResult.thumbnails_missing > 0 &&
487
+ (scanResult.thumbnails_found + scanResult.thumbnails_missing) > 0 &&
488
+ scanResult.thumbnails_missing / (scanResult.thumbnails_found + scanResult.thumbnails_missing) > 0.3 && (
489
+ <div className="p-3 rounded-xl bg-amber-50 border border-amber-200 relative">
490
+ <button
491
+ onClick={() => setThumbWarningDismissed(true)}
492
+ className="absolute top-2 right-2 text-amber-400 hover:text-amber-600 transition-colors"
493
+ title="Dismiss"
494
+ >
495
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
496
+ <line x1="18" y1="6" x2="6" y2="18" />
497
+ <line x1="6" y1="6" x2="18" y2="18" />
498
+ </svg>
499
+ </button>
500
+ <p className="text-sm font-medium text-amber-800">
501
+ {scanResult.thumbnails_missing} of{" "}
502
+ {scanResult.thumbnails_found + scanResult.thumbnails_missing} images are missing thumbnails
503
+ </p>
504
+ <p className="text-xs text-amber-700 mt-1">
505
+ Visitors are loading full-resolution files. Run{" "}
506
+ <code className="bg-amber-100 px-1 py-0.5 rounded text-[11px] font-mono">
507
+ builder-thumbs sync
508
+ </code>{" "}
509
+ to generate them.
510
+ </p>
511
+ </div>
512
+ )}
513
+
514
+ {/* ====== INLINE ASSET BROWSER ====== */}
515
+ <AssetBrowserInline refreshKey={browserRefreshKey} onScanComplete={handleScanComplete} />
516
+ </div>
517
+ );
518
+ }