@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.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/admin/assets.ts +4 -0
- package/admin/database.ts +4 -0
- package/admin/index.ts +6 -0
- package/admin/login.ts +4 -0
- package/admin/navigation.ts +4 -0
- package/admin/pages-editor.ts +4 -0
- package/admin/pages.ts +4 -0
- package/admin/projects-editor.ts +4 -0
- package/admin/projects.ts +4 -0
- package/admin/settings.ts +4 -0
- package/admin/setup.ts +4 -0
- package/admin/storage.ts +4 -0
- package/admin/styles.ts +4 -0
- package/app/(site)/[slug]/loading.tsx +20 -0
- package/app/(site)/[slug]/page.tsx +83 -0
- package/app/(site)/error.tsx +32 -0
- package/app/(site)/layout.tsx +53 -0
- package/app/(site)/loading.tsx +20 -0
- package/app/(site)/not-found.tsx +41 -0
- package/app/(site)/page.tsx +43 -0
- package/app/(site)/preview/page.tsx +99 -0
- package/app/(site)/work/[slug]/loading.tsx +23 -0
- package/app/(site)/work/[slug]/page.tsx +84 -0
- package/app/admin/assets/page.tsx +573 -0
- package/app/admin/database/page.tsx +302 -0
- package/app/admin/error.tsx +53 -0
- package/app/admin/layout.tsx +273 -0
- package/app/admin/login/page.tsx +88 -0
- package/app/admin/navigation/page.tsx +157 -0
- package/app/admin/page.tsx +17 -0
- package/app/admin/pages/[slug]/page.tsx +849 -0
- package/app/admin/pages/page.tsx +588 -0
- package/app/admin/projects/[slug]/page.tsx +3 -0
- package/app/admin/projects/page.tsx +669 -0
- package/app/admin/settings/page.tsx +132 -0
- package/app/admin/setup/page.tsx +64 -0
- package/app/admin/storage/page.tsx +518 -0
- package/app/admin/styles/page.tsx +243 -0
- package/app/api/admin/assets/file/route.ts +81 -0
- package/app/api/admin/assets/health/route.ts +170 -0
- package/app/api/admin/assets/register/route.ts +163 -0
- package/app/api/admin/assets/registry/route.ts +98 -0
- package/app/api/admin/assets/relink/confirm/route.ts +242 -0
- package/app/api/admin/assets/relink/route.ts +202 -0
- package/app/api/admin/assets/scan/route.ts +271 -0
- package/app/api/admin/auth/route.ts +160 -0
- package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
- package/app/api/admin/custom-sections/route.ts +127 -0
- package/app/api/admin/database/route.ts +53 -0
- package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
- package/app/api/admin/pages/[slug]/route.ts +617 -0
- package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
- package/app/api/admin/pages/route.ts +129 -0
- package/app/api/admin/preview/route.ts +53 -0
- package/app/api/admin/r2/connect/route.ts +181 -0
- package/app/api/admin/r2/delete/route.ts +198 -0
- package/app/api/admin/r2/disconnect/route.ts +42 -0
- package/app/api/admin/r2/rename/route.ts +265 -0
- package/app/api/admin/r2/status/route.ts +106 -0
- package/app/api/admin/r2/upload-url/route.ts +148 -0
- package/app/api/admin/revalidate/route.ts +55 -0
- package/app/api/admin/settings/route.ts +279 -0
- package/app/api/admin/setup/complete/route.ts +51 -0
- package/app/api/admin/setup/route.ts +118 -0
- package/app/api/admin/storage/switch/route.ts +117 -0
- package/app/api/admin/styles/fonts/route.ts +97 -0
- package/app/api/admin/styles/route.ts +304 -0
- package/app/api/assets/[...path]/route.ts +98 -0
- package/app/api/custom-sections/[id]/route.ts +43 -0
- package/app/api/draft-mode/disable/route.ts +10 -0
- package/app/api/draft-mode/enable/route.ts +26 -0
- package/app/api/projects/route.ts +42 -0
- package/app/api/styles/route.ts +88 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +7 -0
- package/app/layout.tsx +53 -0
- package/app/robots.ts +17 -0
- package/app/sitemap.ts +48 -0
- package/app/studio/[[...index]]/page.tsx +8 -0
- package/components/admin/MetadataEditor.tsx +173 -0
- package/components/admin/PublishToggle.tsx +130 -0
- package/components/admin/icons.tsx +40 -0
- package/components/admin/nav-builder/NavBuilder.tsx +182 -0
- package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
- package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
- package/components/admin/nav-builder/NavGridCell.tsx +48 -0
- package/components/admin/nav-builder/NavGridItem.tsx +189 -0
- package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
- package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
- package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
- package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
- package/components/admin/nav-builder/index.ts +10 -0
- package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
- package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
- package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
- package/components/admin/setup-wizard/DoneStep.tsx +187 -0
- package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
- package/components/admin/setup-wizard/StorageStep.tsx +308 -0
- package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
- package/components/admin/setup-wizard/index.ts +9 -0
- package/components/admin/styles/ColorsEditor.tsx +214 -0
- package/components/admin/styles/FontsEditor.tsx +258 -0
- package/components/admin/styles/GridLayoutEditor.tsx +292 -0
- package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
- package/components/admin/styles/TypographyEditor.tsx +266 -0
- package/components/admin/styles/index.ts +9 -0
- package/components/admin/styles/shared.tsx +68 -0
- package/components/blocks/BlockRenderer.tsx +404 -0
- package/components/blocks/ButtonBlockRenderer.tsx +52 -0
- package/components/blocks/CoverBlockRenderer.tsx +239 -0
- package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
- package/components/blocks/EnterAnimationWrapper.tsx +140 -0
- package/components/blocks/HoverAnimationWrapper.tsx +308 -0
- package/components/blocks/ImageBlockRenderer.tsx +61 -0
- package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
- package/components/blocks/PageBackground.tsx +28 -0
- package/components/blocks/PageNavAnimation.tsx +35 -0
- package/components/blocks/PageNavColor.tsx +24 -0
- package/components/blocks/PageRenderer.tsx +142 -0
- package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
- package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
- package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
- package/components/blocks/SectionRenderer.tsx +170 -0
- package/components/blocks/SectionV2Renderer.tsx +330 -0
- package/components/blocks/ShaderCanvas.tsx +392 -0
- package/components/blocks/SpacerBlockRenderer.tsx +17 -0
- package/components/blocks/TextBlockRenderer.tsx +87 -0
- package/components/blocks/TypewriterRichText.tsx +464 -0
- package/components/blocks/TypewriterWrapper.tsx +149 -0
- package/components/blocks/VideoBlockRenderer.tsx +304 -0
- package/components/blocks/index.ts +2 -0
- package/components/builder/AssetBrowser.tsx +2 -0
- package/components/builder/BlockLivePreview.tsx +101 -0
- package/components/builder/BlockTypePicker.tsx +178 -0
- package/components/builder/BuilderCanvas.tsx +354 -0
- package/components/builder/CanvasMinimap.tsx +200 -0
- package/components/builder/CanvasToolbar.tsx +202 -0
- package/components/builder/ColorPicker.tsx +243 -0
- package/components/builder/ColorSwatchPicker.tsx +274 -0
- package/components/builder/ColumnDragContext.tsx +51 -0
- package/components/builder/ColumnDragOverlay.tsx +110 -0
- package/components/builder/CustomSectionInstanceCard.tsx +97 -0
- package/components/builder/DeviceFrame.tsx +123 -0
- package/components/builder/DndWrapper.tsx +337 -0
- package/components/builder/InsertionLines.tsx +186 -0
- package/components/builder/ParallaxGroupCanvas.tsx +228 -0
- package/components/builder/ParallaxSlideHeader.tsx +113 -0
- package/components/builder/ReadOnlyFrame.tsx +417 -0
- package/components/builder/SectionEditorBar.tsx +288 -0
- package/components/builder/SectionTypePicker.tsx +422 -0
- package/components/builder/SectionV2Canvas.tsx +297 -0
- package/components/builder/SectionV2Column.tsx +488 -0
- package/components/builder/SettingsPanel.tsx +911 -0
- package/components/builder/SortableBlock.tsx +230 -0
- package/components/builder/SortableRow.tsx +362 -0
- package/components/builder/VirtualAssetGrid.tsx +397 -0
- package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
- package/components/builder/asset-browser/FileLightbox.tsx +116 -0
- package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
- package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
- package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
- package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
- package/components/builder/asset-browser/helpers.ts +88 -0
- package/components/builder/asset-browser/index.ts +1 -0
- package/components/builder/asset-browser/types.ts +49 -0
- package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
- package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
- package/components/builder/asset-browser/useR2Operations.ts +189 -0
- package/components/builder/blockStyles.tsx +295 -0
- package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
- package/components/builder/editors/CoverBlockEditor.tsx +488 -0
- package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
- package/components/builder/editors/HoverEffectPicker.tsx +209 -0
- package/components/builder/editors/ImageBlockEditor.tsx +206 -0
- package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
- package/components/builder/editors/ProjectGridEditor.tsx +648 -0
- package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
- package/components/builder/editors/StaggerSettings.tsx +108 -0
- package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
- package/components/builder/editors/TextBlockEditor.tsx +462 -0
- package/components/builder/editors/TextStylePicker.tsx +183 -0
- package/components/builder/editors/VideoBlockEditor.tsx +278 -0
- package/components/builder/editors/index.ts +10 -0
- package/components/builder/editors/shared.tsx +345 -0
- package/components/builder/hooks/useColumnDrag.ts +472 -0
- package/components/builder/hooks/useColumnResize.ts +221 -0
- package/components/builder/index.ts +12 -0
- package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
- package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
- package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
- package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
- package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
- package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
- package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
- package/components/builder/live-preview/index.ts +10 -0
- package/components/builder/live-preview/shared.tsx +153 -0
- package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
- package/components/builder/settings-panel/BlockSettings.tsx +94 -0
- package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
- package/components/builder/settings-panel/LayoutTab.tsx +310 -0
- package/components/builder/settings-panel/PageSettings.tsx +200 -0
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
- package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
- package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
- package/components/builder/settings-panel/index.ts +19 -0
- package/components/builder/settings-panel/responsive-helpers.ts +524 -0
- package/components/ui/CustomCursor.tsx +118 -0
- package/components/ui/NavContentLightbox.tsx +152 -0
- package/components/ui/Navbar.tsx +582 -0
- package/components/ui/PortfolioTracker.tsx +87 -0
- package/components/ui/ScrollToTop.tsx +47 -0
- package/lib/animation/enter-presets.ts +147 -0
- package/lib/animation/enter-resolve.ts +90 -0
- package/lib/animation/enter-types.ts +128 -0
- package/lib/animation/hover-effect-presets.ts +210 -0
- package/lib/animation/hover-effect-types.ts +126 -0
- package/lib/asset-retry.ts +111 -0
- package/lib/assets.ts +92 -0
- package/lib/audit.ts +35 -0
- package/lib/auth-token.ts +94 -0
- package/lib/auth.ts +13 -0
- package/lib/builder/cascade-helpers.ts +51 -0
- package/lib/builder/cascade.ts +533 -0
- package/lib/builder/constants.ts +103 -0
- package/lib/builder/defaults.ts +182 -0
- package/lib/builder/history.ts +48 -0
- package/lib/builder/index.ts +21 -0
- package/lib/builder/layout-styles.ts +344 -0
- package/lib/builder/masonry.ts +166 -0
- package/lib/builder/responsive.ts +156 -0
- package/lib/builder/serializer.ts +845 -0
- package/lib/builder/store-blocks.ts +193 -0
- package/lib/builder/store-canvas.ts +319 -0
- package/lib/builder/store-helpers.ts +490 -0
- package/lib/builder/store-sections.ts +709 -0
- package/lib/builder/store.ts +333 -0
- package/lib/builder/templates.ts +297 -0
- package/lib/builder/types.ts +374 -0
- package/lib/builder/utils.ts +37 -0
- package/lib/color-utils.ts +116 -0
- package/lib/config/index.ts +57 -0
- package/lib/config/types.ts +122 -0
- package/lib/contexts/AssetContext.tsx +79 -0
- package/lib/contexts/NavAnimationContext.tsx +44 -0
- package/lib/contexts/NavColorContext.tsx +38 -0
- package/lib/contexts/PageExitContext.tsx +194 -0
- package/lib/contexts/ThumbStatusContext.tsx +83 -0
- package/lib/csrf-client.ts +34 -0
- package/lib/csrf.ts +68 -0
- package/lib/format-utils.ts +24 -0
- package/lib/hooks/useViewport.ts +42 -0
- package/lib/logger.ts +81 -0
- package/lib/revalidate.ts +23 -0
- package/lib/sanitize.ts +91 -0
- package/lib/sanity/client.ts +8 -0
- package/lib/sanity/queries.ts +486 -0
- package/lib/sanity/types.ts +869 -0
- package/lib/sanity/writeClient.ts +24 -0
- package/lib/security.ts +402 -0
- package/lib/setup/detect.ts +156 -0
- package/lib/shader/glsl/index.ts +27 -0
- package/lib/shader/glsl/pixelate.ts +51 -0
- package/lib/shader/glsl/rgb-shift.ts +45 -0
- package/lib/shader/glsl/ripple.ts +46 -0
- package/lib/shader/glsl/vertex.ts +14 -0
- package/lib/storage/index.ts +211 -0
- package/lib/storage/r2-adapter.ts +286 -0
- package/lib/storage/types.ts +125 -0
- package/lib/styles/provider.tsx +267 -0
- package/lib/thumbnails/generate.ts +151 -0
- package/lib/utils.ts +6 -0
- package/package.json +212 -0
- package/sanity/compose.ts +65 -0
- package/sanity/sanity.config.ts +126 -0
- package/sanity/schemas/assetRegistry.ts +301 -0
- package/sanity/schemas/blocks/blockLayout.ts +90 -0
- package/sanity/schemas/blocks/buttonBlock.ts +82 -0
- package/sanity/schemas/blocks/coverBlock.ts +229 -0
- package/sanity/schemas/blocks/imageBlock.ts +58 -0
- package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
- package/sanity/schemas/blocks/index.ts +9 -0
- package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
- package/sanity/schemas/blocks/spacerBlock.ts +41 -0
- package/sanity/schemas/blocks/textBlock.ts +139 -0
- package/sanity/schemas/blocks/videoBlock.ts +80 -0
- package/sanity/schemas/customSection.ts +69 -0
- package/sanity/schemas/customSectionInstance.ts +163 -0
- package/sanity/schemas/index.ts +111 -0
- package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
- package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
- package/sanity/schemas/objects/parallaxGroup.ts +66 -0
- package/sanity/schemas/objects/parallaxSlide.ts +217 -0
- package/sanity/schemas/objects/typewriterConfig.ts +38 -0
- package/sanity/schemas/page.ts +162 -0
- package/sanity/schemas/pageSection.ts +157 -0
- package/sanity/schemas/pageSectionV2.ts +269 -0
- package/sanity/schemas/siteSettings.ts +256 -0
- package/sanity/schemas/siteStyles.ts +212 -0
- package/site/error.ts +4 -0
- package/site/index.ts +8 -0
- package/site/not-found.ts +4 -0
- package/site/page.ts +4 -0
- package/site/preview.ts +4 -0
- package/site/robots.ts +4 -0
- package/site/sitemap.ts +4 -0
- package/site/work.ts +4 -0
- package/studio/index.ts +4 -0
- package/styles/admin.css +85 -0
- package/styles/animations.css +237 -0
- package/styles/base.css +148 -0
- package/styles/globals.css +10 -0
- 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
|
+
}
|