@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,302 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
|
|
5
|
+
// ── Types ──
|
|
6
|
+
|
|
7
|
+
interface SanityStats {
|
|
8
|
+
totalDocuments: number;
|
|
9
|
+
pages: number;
|
|
10
|
+
publishedPages: number;
|
|
11
|
+
draftPages: number;
|
|
12
|
+
projects: number;
|
|
13
|
+
siteSettings: number;
|
|
14
|
+
siteStyles: number;
|
|
15
|
+
assetRegistry: number;
|
|
16
|
+
datasets: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SanityStatus {
|
|
20
|
+
connected: boolean;
|
|
21
|
+
projectId: string;
|
|
22
|
+
dataset: string;
|
|
23
|
+
hasWriteToken: boolean;
|
|
24
|
+
stats: SanityStats | null;
|
|
25
|
+
error?: string;
|
|
26
|
+
apiVersion: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Stat Card ──
|
|
30
|
+
|
|
31
|
+
function StatCard({ label, value, sub }: { label: string; value: number | string; sub?: string }) {
|
|
32
|
+
return (
|
|
33
|
+
<div className="bg-white rounded-xl border border-neutral-200 p-4">
|
|
34
|
+
<p className="text-[11px] uppercase tracking-wider text-neutral-400 mb-1">{label}</p>
|
|
35
|
+
<p className="text-2xl font-semibold text-neutral-900">{value}</p>
|
|
36
|
+
{sub && <p className="text-xs text-neutral-400 mt-0.5">{sub}</p>}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Config Field (read-only display) ──
|
|
42
|
+
|
|
43
|
+
function ConfigField({ label, value, masked }: { label: string; value: string; masked?: boolean }) {
|
|
44
|
+
const [revealed, setRevealed] = useState(false);
|
|
45
|
+
const display = masked && !revealed ? "••••••••••••" : value || "—";
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex items-center justify-between py-3 border-b border-neutral-100 last:border-0">
|
|
49
|
+
<span className="text-sm text-neutral-500">{label}</span>
|
|
50
|
+
<div className="flex items-center gap-2">
|
|
51
|
+
<code className="text-sm text-neutral-800 bg-neutral-50 px-2 py-0.5 rounded font-mono">{display}</code>
|
|
52
|
+
{masked && value && (
|
|
53
|
+
<button
|
|
54
|
+
onClick={() => setRevealed(!revealed)}
|
|
55
|
+
className="text-xs text-[#076bff] hover:underline"
|
|
56
|
+
>
|
|
57
|
+
{revealed ? "Hide" : "Show"}
|
|
58
|
+
</button>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Document Type Row ──
|
|
66
|
+
|
|
67
|
+
function DocTypeRow({ label, count, icon }: { label: string; count: number; icon: React.ReactNode }) {
|
|
68
|
+
return (
|
|
69
|
+
<div className="flex items-center justify-between py-2.5 border-b border-neutral-100 last:border-0">
|
|
70
|
+
<div className="flex items-center gap-2.5">
|
|
71
|
+
<span className="text-neutral-400">{icon}</span>
|
|
72
|
+
<span className="text-sm text-neutral-700">{label}</span>
|
|
73
|
+
</div>
|
|
74
|
+
<span className="text-sm font-medium text-neutral-900 bg-neutral-100 rounded-full px-2.5 py-0.5 min-w-[32px] text-center">
|
|
75
|
+
{count}
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Main Page ──
|
|
82
|
+
|
|
83
|
+
export default function AdminDatabasePage() {
|
|
84
|
+
const [status, setStatus] = useState<SanityStatus | null>(null);
|
|
85
|
+
const [loading, setLoading] = useState(true);
|
|
86
|
+
const [testing, setTesting] = useState(false);
|
|
87
|
+
|
|
88
|
+
const fetchStatus = useCallback(async () => {
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch("/api/admin/database");
|
|
91
|
+
if (res.ok) {
|
|
92
|
+
const data = await res.json();
|
|
93
|
+
setStatus(data);
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
setStatus(null);
|
|
97
|
+
} finally {
|
|
98
|
+
setLoading(false);
|
|
99
|
+
}
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
fetchStatus();
|
|
104
|
+
}, [fetchStatus]);
|
|
105
|
+
|
|
106
|
+
const handleTestConnection = async () => {
|
|
107
|
+
setTesting(true);
|
|
108
|
+
setLoading(false);
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch("/api/admin/database");
|
|
111
|
+
if (res.ok) {
|
|
112
|
+
const data = await res.json();
|
|
113
|
+
setStatus(data);
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// handled by status state
|
|
117
|
+
} finally {
|
|
118
|
+
setTesting(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// ── Loading ──
|
|
123
|
+
if (loading) {
|
|
124
|
+
return (
|
|
125
|
+
<div className="flex items-center justify-center py-20">
|
|
126
|
+
<span className="text-sm text-neutral-400 animate-pulse">Connecting to database...</span>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const stats = status?.stats;
|
|
132
|
+
const isConnected = status?.connected === true;
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className="space-y-8 max-w-4xl">
|
|
136
|
+
{/* Header */}
|
|
137
|
+
<div className="flex items-center justify-between">
|
|
138
|
+
<div>
|
|
139
|
+
<h1 className="text-2xl font-semibold text-neutral-900">Database</h1>
|
|
140
|
+
<p className="text-sm text-neutral-400 mt-1">Sanity CMS connection and data overview</p>
|
|
141
|
+
</div>
|
|
142
|
+
<button
|
|
143
|
+
onClick={handleTestConnection}
|
|
144
|
+
disabled={testing}
|
|
145
|
+
className="flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg border border-neutral-200 bg-white text-neutral-700 hover:bg-neutral-50 transition-colors disabled:opacity-50"
|
|
146
|
+
>
|
|
147
|
+
{testing ? (
|
|
148
|
+
<svg className="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
149
|
+
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
|
|
150
|
+
</svg>
|
|
151
|
+
) : (
|
|
152
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
153
|
+
<polyline points="23 4 23 10 17 10" />
|
|
154
|
+
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
|
|
155
|
+
</svg>
|
|
156
|
+
)}
|
|
157
|
+
{testing ? "Testing..." : "Test Connection"}
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Connection status banner */}
|
|
162
|
+
<div className={`flex items-center gap-3 p-4 rounded-xl border ${
|
|
163
|
+
isConnected
|
|
164
|
+
? "border-green-200 bg-green-50"
|
|
165
|
+
: "border-red-200 bg-red-50"
|
|
166
|
+
}`}>
|
|
167
|
+
<div className={`w-2.5 h-2.5 rounded-full ${isConnected ? "bg-green-500" : "bg-red-500"}`} />
|
|
168
|
+
<div className="flex-1">
|
|
169
|
+
<p className={`text-sm font-medium ${isConnected ? "text-green-800" : "text-red-800"}`}>
|
|
170
|
+
{isConnected ? "Connected to Sanity" : "Connection Failed"}
|
|
171
|
+
</p>
|
|
172
|
+
{status?.error && (
|
|
173
|
+
<p className="text-xs text-red-600 mt-0.5">{status.error}</p>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
{isConnected && (
|
|
177
|
+
<span className="text-xs text-green-600 bg-green-100 px-2 py-0.5 rounded-full">
|
|
178
|
+
API v{status?.apiVersion}
|
|
179
|
+
</span>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* Stats grid */}
|
|
184
|
+
{stats && (
|
|
185
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
186
|
+
<StatCard label="Total Documents" value={stats.totalDocuments} />
|
|
187
|
+
<StatCard label="Pages" value={stats.pages} sub={`${stats.publishedPages} published, ${stats.draftPages} draft`} />
|
|
188
|
+
<StatCard label="Projects" value={stats.projects} />
|
|
189
|
+
<StatCard label="Dataset" value={status?.dataset || "—"} />
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{/* Two-column layout: Config + Schema */}
|
|
194
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
195
|
+
|
|
196
|
+
{/* Connection Configuration */}
|
|
197
|
+
<div className="bg-white rounded-xl border border-neutral-200 overflow-hidden">
|
|
198
|
+
<div className="px-5 py-4 border-b border-neutral-100">
|
|
199
|
+
<h2 className="text-sm font-semibold text-neutral-900">Connection Configuration</h2>
|
|
200
|
+
<p className="text-xs text-neutral-400 mt-0.5">Values from environment variables — edit in .env.local or hosting dashboard</p>
|
|
201
|
+
</div>
|
|
202
|
+
<div className="px-5 py-2">
|
|
203
|
+
<ConfigField label="Project ID" value={status?.projectId || ""} />
|
|
204
|
+
<ConfigField label="Dataset" value={status?.dataset || ""} />
|
|
205
|
+
<ConfigField label="API Version" value={status?.apiVersion || ""} />
|
|
206
|
+
<ConfigField label="Write Token" value={status?.hasWriteToken ? "Configured" : "Not set"} />
|
|
207
|
+
<ConfigField label="CDN" value="Disabled (real-time reads)" />
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{/* Document Types */}
|
|
212
|
+
{stats && (
|
|
213
|
+
<div className="bg-white rounded-xl border border-neutral-200 overflow-hidden">
|
|
214
|
+
<div className="px-5 py-4 border-b border-neutral-100">
|
|
215
|
+
<h2 className="text-sm font-semibold text-neutral-900">Schema Overview</h2>
|
|
216
|
+
<p className="text-xs text-neutral-400 mt-0.5">Document types and counts in this dataset</p>
|
|
217
|
+
</div>
|
|
218
|
+
<div className="px-5 py-2">
|
|
219
|
+
<DocTypeRow
|
|
220
|
+
label="Pages"
|
|
221
|
+
count={stats.pages}
|
|
222
|
+
icon={
|
|
223
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" /><polyline points="14 2 14 8 20 8" /></svg>
|
|
224
|
+
}
|
|
225
|
+
/>
|
|
226
|
+
<DocTypeRow
|
|
227
|
+
label="Projects"
|
|
228
|
+
count={stats.projects}
|
|
229
|
+
icon={
|
|
230
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="2" y="2" width="20" height="20" rx="2.18" /><line x1="7" y1="2" x2="7" y2="22" /><line x1="17" y1="2" x2="17" y2="22" /><line x1="2" y1="12" x2="22" y2="12" /></svg>
|
|
231
|
+
}
|
|
232
|
+
/>
|
|
233
|
+
<DocTypeRow
|
|
234
|
+
label="Site Settings"
|
|
235
|
+
count={stats.siteSettings}
|
|
236
|
+
icon={
|
|
237
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9c.38.17.62.55.68.95" /></svg>
|
|
238
|
+
}
|
|
239
|
+
/>
|
|
240
|
+
<DocTypeRow
|
|
241
|
+
label="Site Styles"
|
|
242
|
+
count={stats.siteStyles}
|
|
243
|
+
icon={
|
|
244
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="13.5" cy="6.5" r="0.5" fill="currentColor" /><circle cx="17.5" cy="10.5" r="0.5" fill="currentColor" /><circle cx="8.5" cy="7.5" r="0.5" fill="currentColor" /><circle cx="6.5" cy="12" r="0.5" fill="currentColor" /><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2Z" /></svg>
|
|
245
|
+
}
|
|
246
|
+
/>
|
|
247
|
+
<DocTypeRow
|
|
248
|
+
label="Asset Registry"
|
|
249
|
+
count={stats.assetRegistry}
|
|
250
|
+
icon={
|
|
251
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M21 12c0 1.66-4.03 3-9 3s-9-1.34-9-3" /><path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5" /></svg>
|
|
252
|
+
}
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{/* Sanity Studio link */}
|
|
260
|
+
<div className="bg-white rounded-xl border border-neutral-200 p-5">
|
|
261
|
+
<div className="flex items-center justify-between">
|
|
262
|
+
<div>
|
|
263
|
+
<h2 className="text-sm font-semibold text-neutral-900">Sanity Studio</h2>
|
|
264
|
+
<p className="text-xs text-neutral-400 mt-0.5">Access the raw Sanity Studio for advanced data editing</p>
|
|
265
|
+
</div>
|
|
266
|
+
<a
|
|
267
|
+
href="/studio"
|
|
268
|
+
target="_blank"
|
|
269
|
+
rel="noopener noreferrer"
|
|
270
|
+
className="flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-neutral-900 text-white hover:bg-neutral-800 transition-colors"
|
|
271
|
+
>
|
|
272
|
+
Open Studio
|
|
273
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
274
|
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
275
|
+
<polyline points="15 3 21 3 21 9" />
|
|
276
|
+
<line x1="10" y1="14" x2="21" y2="3" />
|
|
277
|
+
</svg>
|
|
278
|
+
</a>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{/* Env guide */}
|
|
283
|
+
<div className="bg-neutral-50 rounded-xl border border-neutral-200 p-5">
|
|
284
|
+
<h2 className="text-sm font-semibold text-neutral-700 mb-3">Environment Variables Reference</h2>
|
|
285
|
+
<div className="space-y-2 font-mono text-xs text-neutral-500">
|
|
286
|
+
<div className="flex gap-3">
|
|
287
|
+
<span className="text-neutral-400 w-64 shrink-0">NEXT_PUBLIC_SANITY_PROJECT_ID</span>
|
|
288
|
+
<span className="text-neutral-600">Sanity project ID (required)</span>
|
|
289
|
+
</div>
|
|
290
|
+
<div className="flex gap-3">
|
|
291
|
+
<span className="text-neutral-400 w-64 shrink-0">NEXT_PUBLIC_SANITY_DATASET</span>
|
|
292
|
+
<span className="text-neutral-600">Dataset name (default: "production")</span>
|
|
293
|
+
</div>
|
|
294
|
+
<div className="flex gap-3">
|
|
295
|
+
<span className="text-neutral-400 w-64 shrink-0">SANITY_API_TOKEN</span>
|
|
296
|
+
<span className="text-neutral-600">Write token for mutations (server-only)</span>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
export default function AdminError({
|
|
6
|
+
error,
|
|
7
|
+
reset,
|
|
8
|
+
}: {
|
|
9
|
+
error: Error & { digest?: string };
|
|
10
|
+
reset: () => void;
|
|
11
|
+
}) {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
console.error("[AdminError]", error);
|
|
14
|
+
}, [error]);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="min-h-screen flex flex-col items-center justify-center bg-white px-6 text-center">
|
|
18
|
+
<div className="w-12 h-12 rounded-full bg-red-50 flex items-center justify-center mb-4">
|
|
19
|
+
<svg
|
|
20
|
+
width="24"
|
|
21
|
+
height="24"
|
|
22
|
+
viewBox="0 0 24 24"
|
|
23
|
+
fill="none"
|
|
24
|
+
stroke="#dc2626"
|
|
25
|
+
strokeWidth="2"
|
|
26
|
+
aria-hidden="true"
|
|
27
|
+
>
|
|
28
|
+
<circle cx="12" cy="12" r="10" />
|
|
29
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
30
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
31
|
+
</svg>
|
|
32
|
+
</div>
|
|
33
|
+
<h1 className="text-xl font-semibold text-gray-900">
|
|
34
|
+
Admin Error
|
|
35
|
+
</h1>
|
|
36
|
+
<p className="mt-2 text-sm text-gray-500 max-w-sm">
|
|
37
|
+
Something went wrong in the admin panel.
|
|
38
|
+
{error.digest && (
|
|
39
|
+
<span className="block mt-1 text-xs text-gray-400">
|
|
40
|
+
Error ID: {error.digest}
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
</p>
|
|
44
|
+
<button
|
|
45
|
+
onClick={reset}
|
|
46
|
+
className="mt-6 px-5 py-2 rounded-lg text-sm font-medium text-white transition-colors"
|
|
47
|
+
style={{ backgroundColor: "#076bff" }}
|
|
48
|
+
>
|
|
49
|
+
Try again
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { usePathname, useRouter } from "next/navigation";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useState, useEffect, useRef } from "react";
|
|
6
|
+
import { getSiteConfig } from "../../lib/config";
|
|
7
|
+
|
|
8
|
+
// ============================================
|
|
9
|
+
// Navigation Configuration — flat list, no sections
|
|
10
|
+
// ============================================
|
|
11
|
+
|
|
12
|
+
const navLinks = [
|
|
13
|
+
{ href: "/admin/pages", label: "Pages", icon: "file" },
|
|
14
|
+
{ href: "/admin/projects", label: "Projects", icon: "film" },
|
|
15
|
+
{ href: "/admin/styles", label: "Customize", icon: "palette" },
|
|
16
|
+
{ href: "/admin/navigation", label: "Navigation", icon: "nav" },
|
|
17
|
+
{ href: "/admin/storage", label: "Storage", icon: "harddisk" },
|
|
18
|
+
{ href: "/admin/database", label: "Database", icon: "database" },
|
|
19
|
+
{ href: "/admin/settings", label: "Metadata", icon: "code" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// ============================================
|
|
23
|
+
// Icon Component — white stroke for dark sidebar
|
|
24
|
+
// ============================================
|
|
25
|
+
|
|
26
|
+
function NavIcon({ icon, active }: { icon: string; active?: boolean }) {
|
|
27
|
+
const size = 18;
|
|
28
|
+
const color = active ? "currentColor" : "currentColor";
|
|
29
|
+
void active; // active state handled by parent text color
|
|
30
|
+
void color;
|
|
31
|
+
switch (icon) {
|
|
32
|
+
case "file":
|
|
33
|
+
return (
|
|
34
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
35
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" />
|
|
36
|
+
<polyline points="14 2 14 8 20 8" />
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
case "film":
|
|
40
|
+
return (
|
|
41
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
42
|
+
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18" />
|
|
43
|
+
<line x1="7" y1="2" x2="7" y2="22" />
|
|
44
|
+
<line x1="17" y1="2" x2="17" y2="22" />
|
|
45
|
+
<line x1="2" y1="12" x2="22" y2="12" />
|
|
46
|
+
<line x1="2" y1="7" x2="7" y2="7" />
|
|
47
|
+
<line x1="2" y1="17" x2="7" y2="17" />
|
|
48
|
+
<line x1="17" y1="7" x2="22" y2="7" />
|
|
49
|
+
<line x1="17" y1="17" x2="22" y2="17" />
|
|
50
|
+
</svg>
|
|
51
|
+
);
|
|
52
|
+
case "palette":
|
|
53
|
+
return (
|
|
54
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
55
|
+
<circle cx="13.5" cy="6.5" r="0.5" fill="currentColor" />
|
|
56
|
+
<circle cx="17.5" cy="10.5" r="0.5" fill="currentColor" />
|
|
57
|
+
<circle cx="8.5" cy="7.5" r="0.5" fill="currentColor" />
|
|
58
|
+
<circle cx="6.5" cy="12" r="0.5" fill="currentColor" />
|
|
59
|
+
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2Z" />
|
|
60
|
+
</svg>
|
|
61
|
+
);
|
|
62
|
+
case "nav":
|
|
63
|
+
return (
|
|
64
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
65
|
+
<line x1="3" y1="6" x2="21" y2="6" />
|
|
66
|
+
<line x1="3" y1="12" x2="15" y2="12" />
|
|
67
|
+
<line x1="3" y1="18" x2="18" y2="18" />
|
|
68
|
+
</svg>
|
|
69
|
+
);
|
|
70
|
+
case "database":
|
|
71
|
+
return (
|
|
72
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
73
|
+
<ellipse cx="12" cy="5" rx="9" ry="3" />
|
|
74
|
+
<path d="M21 12c0 1.66-4.03 3-9 3s-9-1.34-9-3" />
|
|
75
|
+
<path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5" />
|
|
76
|
+
</svg>
|
|
77
|
+
);
|
|
78
|
+
case "harddisk":
|
|
79
|
+
return (
|
|
80
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
81
|
+
<path d="M22 12H2" />
|
|
82
|
+
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11Z" />
|
|
83
|
+
<line x1="6" y1="16" x2="6.01" y2="16" strokeWidth="2" />
|
|
84
|
+
<line x1="10" y1="16" x2="10.01" y2="16" strokeWidth="2" />
|
|
85
|
+
</svg>
|
|
86
|
+
);
|
|
87
|
+
case "code":
|
|
88
|
+
return (
|
|
89
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
90
|
+
<polyline points="16 18 22 12 16 6" />
|
|
91
|
+
<polyline points="8 6 2 12 8 18" />
|
|
92
|
+
</svg>
|
|
93
|
+
);
|
|
94
|
+
default:
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================
|
|
100
|
+
// Layout Component
|
|
101
|
+
// ============================================
|
|
102
|
+
|
|
103
|
+
export default function AdminLayout({
|
|
104
|
+
children,
|
|
105
|
+
}: {
|
|
106
|
+
children: React.ReactNode;
|
|
107
|
+
}) {
|
|
108
|
+
const pathname = usePathname();
|
|
109
|
+
const router = useRouter();
|
|
110
|
+
|
|
111
|
+
// Detect if we're inside the page builder (editing a specific page or project)
|
|
112
|
+
const isPageBuilder =
|
|
113
|
+
/^\/admin\/pages\/[^/]+$/.test(pathname) ||
|
|
114
|
+
/^\/admin\/projects\/[^/]+$/.test(pathname);
|
|
115
|
+
|
|
116
|
+
// Start collapsed if loading directly into the builder
|
|
117
|
+
const [sidebarOpen, setSidebarOpen] = useState(!isPageBuilder);
|
|
118
|
+
const prevPathname = useRef(pathname);
|
|
119
|
+
|
|
120
|
+
// Auto-collapse sidebar when entering the page builder,
|
|
121
|
+
// auto-expand when leaving it
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (pathname === prevPathname.current) return;
|
|
124
|
+
const wasInBuilder =
|
|
125
|
+
/^\/admin\/pages\/[^/]+$/.test(prevPathname.current) ||
|
|
126
|
+
/^\/admin\/projects\/[^/]+$/.test(prevPathname.current);
|
|
127
|
+
prevPathname.current = pathname;
|
|
128
|
+
|
|
129
|
+
if (isPageBuilder && !wasInBuilder) {
|
|
130
|
+
setSidebarOpen(false);
|
|
131
|
+
} else if (!isPageBuilder && wasInBuilder) {
|
|
132
|
+
setSidebarOpen(true);
|
|
133
|
+
}
|
|
134
|
+
}, [pathname, isPageBuilder]);
|
|
135
|
+
|
|
136
|
+
// Don't show admin shell on login or setup pages
|
|
137
|
+
if (pathname === "/admin/login" || pathname === "/admin/setup") {
|
|
138
|
+
return <>{children}</>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const handleLogout = async () => {
|
|
142
|
+
await fetch("/api/admin/auth", { method: "DELETE" });
|
|
143
|
+
router.push("/admin/login");
|
|
144
|
+
router.refresh();
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Check if a link is active
|
|
148
|
+
const isLinkActive = (href: string) => {
|
|
149
|
+
return pathname === href || pathname.startsWith(href + "/");
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div data-admin className="flex h-screen bg-[#f8f8f8]" style={{ fontFamily: "Inter, system-ui, sans-serif" }}>
|
|
154
|
+
{/* Sidebar — Dark */}
|
|
155
|
+
<aside
|
|
156
|
+
className={`flex flex-col bg-[#141414] transition-all duration-200 ${
|
|
157
|
+
sidebarOpen ? "w-56" : "w-16"
|
|
158
|
+
}`}
|
|
159
|
+
>
|
|
160
|
+
{/* Logo */}
|
|
161
|
+
<div className="flex h-14 items-center justify-between px-4 border-b border-white/[0.06]">
|
|
162
|
+
{sidebarOpen && (
|
|
163
|
+
<span className="text-[10px] font-semibold tracking-widest uppercase text-white/90 leading-tight">
|
|
164
|
+
{getSiteConfig().name.split(" ")[0]}<br />
|
|
165
|
+
<span className="text-white/50 font-normal">Web Builder</span>
|
|
166
|
+
</span>
|
|
167
|
+
)}
|
|
168
|
+
<button
|
|
169
|
+
onClick={() => setSidebarOpen(!sidebarOpen)}
|
|
170
|
+
className="text-white/40 hover:text-white/70 transition-colors p-1 rounded-md hover:bg-white/[0.06]"
|
|
171
|
+
aria-label={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
|
|
172
|
+
>
|
|
173
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
174
|
+
{sidebarOpen ? (
|
|
175
|
+
<polyline points="11 17 6 12 11 7" />
|
|
176
|
+
) : (
|
|
177
|
+
<polyline points="13 7 18 12 13 17" />
|
|
178
|
+
)}
|
|
179
|
+
</svg>
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* Nav links — flat list */}
|
|
184
|
+
<nav className="flex-1 py-3 px-2 overflow-y-auto">
|
|
185
|
+
{navLinks.map((link) => {
|
|
186
|
+
const isActive = isLinkActive(link.href);
|
|
187
|
+
return (
|
|
188
|
+
<Link
|
|
189
|
+
key={link.href}
|
|
190
|
+
href={link.href}
|
|
191
|
+
className={`flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-all mb-0.5 ${
|
|
192
|
+
isActive
|
|
193
|
+
? "bg-white/[0.08] text-white font-medium"
|
|
194
|
+
: "text-white/60 hover:bg-white/[0.06] hover:text-white/90"
|
|
195
|
+
} ${!sidebarOpen ? "justify-center px-0" : ""}`}
|
|
196
|
+
title={!sidebarOpen ? link.label : undefined}
|
|
197
|
+
>
|
|
198
|
+
<NavIcon icon={link.icon} active={isActive} />
|
|
199
|
+
{sidebarOpen && (
|
|
200
|
+
<span className="text-[13px]">{link.label}</span>
|
|
201
|
+
)}
|
|
202
|
+
</Link>
|
|
203
|
+
);
|
|
204
|
+
})}
|
|
205
|
+
</nav>
|
|
206
|
+
|
|
207
|
+
{/* Setup Wizard */}
|
|
208
|
+
<div className="px-2 mb-0.5">
|
|
209
|
+
<Link
|
|
210
|
+
href="/admin/setup"
|
|
211
|
+
className={`flex items-center gap-3 text-white/40 hover:text-white/80 transition-colors px-3 py-2 rounded-lg hover:bg-white/[0.06] w-full text-sm ${
|
|
212
|
+
!sidebarOpen ? "justify-center px-0" : ""
|
|
213
|
+
}`}
|
|
214
|
+
title="Setup Wizard"
|
|
215
|
+
>
|
|
216
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
217
|
+
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76Z" />
|
|
218
|
+
</svg>
|
|
219
|
+
{sidebarOpen && <span className="text-[13px]">Setup Wizard</span>}
|
|
220
|
+
</Link>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{/* View Site */}
|
|
224
|
+
<div className="px-2 mb-1">
|
|
225
|
+
<Link
|
|
226
|
+
href="/"
|
|
227
|
+
target="_blank"
|
|
228
|
+
className={`flex items-center gap-3 text-white/40 hover:text-white/80 transition-colors px-3 py-2 rounded-lg hover:bg-white/[0.06] w-full text-sm ${
|
|
229
|
+
!sidebarOpen ? "justify-center px-0" : ""
|
|
230
|
+
}`}
|
|
231
|
+
title="View Site"
|
|
232
|
+
>
|
|
233
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
234
|
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
235
|
+
<polyline points="15 3 21 3 21 9" />
|
|
236
|
+
<line x1="10" y1="14" x2="21" y2="3" />
|
|
237
|
+
</svg>
|
|
238
|
+
{sidebarOpen && <span className="text-[13px]">View Site</span>}
|
|
239
|
+
</Link>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{/* Logout */}
|
|
243
|
+
<div className="border-t border-white/[0.06] p-2">
|
|
244
|
+
<button
|
|
245
|
+
onClick={handleLogout}
|
|
246
|
+
className={`flex items-center gap-3 text-white/40 hover:text-red-400 transition-colors px-3 py-2 rounded-lg hover:bg-red-500/10 w-full text-sm ${
|
|
247
|
+
!sidebarOpen ? "justify-center px-0" : ""
|
|
248
|
+
}`}
|
|
249
|
+
title="Logout"
|
|
250
|
+
>
|
|
251
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
252
|
+
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
|
253
|
+
<polyline points="16 17 21 12 16 7" />
|
|
254
|
+
<line x1="21" y1="12" x2="9" y2="12" />
|
|
255
|
+
</svg>
|
|
256
|
+
{sidebarOpen && <span className="text-[13px]">Logout</span>}
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
259
|
+
</aside>
|
|
260
|
+
|
|
261
|
+
{/* Main content area — no top header bar, pages have their own titles */}
|
|
262
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
263
|
+
<main className={`flex-1 ${
|
|
264
|
+
isPageBuilder
|
|
265
|
+
? "overflow-hidden"
|
|
266
|
+
: "overflow-y-auto p-8 bg-[#f8f8f8]"
|
|
267
|
+
}`}>
|
|
268
|
+
{children}
|
|
269
|
+
</main>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
}
|