@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,331 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect } from "react";
|
|
4
|
+
import { csrfHeaders } from "../../../lib/csrf-client";
|
|
5
|
+
import type { WizardStepProps } from "./SetupWizard";
|
|
6
|
+
|
|
7
|
+
// ── Icons ──
|
|
8
|
+
|
|
9
|
+
function CheckCircle() {
|
|
10
|
+
return (
|
|
11
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
12
|
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
|
13
|
+
<polyline points="22 4 12 14.01 9 11.01" />
|
|
14
|
+
</svg>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function XCircle() {
|
|
19
|
+
return (
|
|
20
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
21
|
+
<circle cx="12" cy="12" r="10" />
|
|
22
|
+
<line x1="15" y1="9" x2="9" y2="15" />
|
|
23
|
+
<line x1="9" y1="9" x2="15" y2="15" />
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Spinner() {
|
|
29
|
+
return (
|
|
30
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="animate-spin">
|
|
31
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Types ──
|
|
37
|
+
|
|
38
|
+
interface ConnectionResult {
|
|
39
|
+
connected: boolean;
|
|
40
|
+
projectId?: string;
|
|
41
|
+
dataset?: string;
|
|
42
|
+
hasWriteToken?: boolean;
|
|
43
|
+
stats?: {
|
|
44
|
+
siteSettings: number;
|
|
45
|
+
siteStyles: number;
|
|
46
|
+
assetRegistry: number;
|
|
47
|
+
};
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface SeedResult {
|
|
52
|
+
seeded: string[];
|
|
53
|
+
skipped: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type ConnectionState = "idle" | "testing" | "connected" | "failed";
|
|
57
|
+
type SeedState = "idle" | "seeding" | "done" | "error";
|
|
58
|
+
|
|
59
|
+
// ── Component ──
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Step 2 — Database (Sanity Connection)
|
|
63
|
+
*
|
|
64
|
+
* Tests the Sanity connection and seeds initial documents if needed.
|
|
65
|
+
*/
|
|
66
|
+
export function DatabaseStep({ onNext, onBack }: WizardStepProps) {
|
|
67
|
+
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || "";
|
|
68
|
+
const hasProjectId = !!projectId;
|
|
69
|
+
|
|
70
|
+
const [connState, setConnState] = useState<ConnectionState>("idle");
|
|
71
|
+
const [connResult, setConnResult] = useState<ConnectionResult | null>(null);
|
|
72
|
+
const [seedState, setSeedState] = useState<SeedState>("idle");
|
|
73
|
+
const [seedResult, setSeedResult] = useState<SeedResult | null>(null);
|
|
74
|
+
const [error, setError] = useState<string>("");
|
|
75
|
+
|
|
76
|
+
// Auto-test on mount if project ID is present
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (hasProjectId && connState === "idle") {
|
|
79
|
+
testConnection();
|
|
80
|
+
}
|
|
81
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const testConnection = useCallback(async () => {
|
|
85
|
+
setConnState("testing");
|
|
86
|
+
setError("");
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch("/api/admin/database");
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
setConnState("failed");
|
|
92
|
+
setError(`API returned ${res.status}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const data: ConnectionResult = await res.json();
|
|
97
|
+
setConnResult(data);
|
|
98
|
+
|
|
99
|
+
if (data.connected) {
|
|
100
|
+
setConnState("connected");
|
|
101
|
+
} else {
|
|
102
|
+
setConnState("failed");
|
|
103
|
+
setError(data.error || "Connection failed");
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
setConnState("failed");
|
|
107
|
+
setError(err instanceof Error ? err.message : "Connection failed");
|
|
108
|
+
}
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
const seedDocuments = useCallback(async () => {
|
|
112
|
+
setSeedState("seeding");
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch("/api/admin/setup", {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
...csrfHeaders(),
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
const data = await res.json();
|
|
125
|
+
setSeedState("error");
|
|
126
|
+
setError(data.error || "Seeding failed");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const data: SeedResult = await res.json();
|
|
131
|
+
setSeedResult(data);
|
|
132
|
+
setSeedState("done");
|
|
133
|
+
} catch (err) {
|
|
134
|
+
setSeedState("error");
|
|
135
|
+
setError(err instanceof Error ? err.message : "Seeding failed");
|
|
136
|
+
}
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
// Should we show the seed button?
|
|
140
|
+
const needsSeeding =
|
|
141
|
+
connState === "connected" &&
|
|
142
|
+
connResult?.stats &&
|
|
143
|
+
(connResult.stats.siteSettings === 0 ||
|
|
144
|
+
connResult.stats.siteStyles === 0 ||
|
|
145
|
+
connResult.stats.assetRegistry === 0);
|
|
146
|
+
|
|
147
|
+
const docsPresent =
|
|
148
|
+
connState === "connected" &&
|
|
149
|
+
connResult?.stats &&
|
|
150
|
+
connResult.stats.siteSettings > 0 &&
|
|
151
|
+
connResult.stats.siteStyles > 0 &&
|
|
152
|
+
connResult.stats.assetRegistry > 0;
|
|
153
|
+
|
|
154
|
+
const allSeeded = seedState === "done" || (docsPresent && !needsSeeding);
|
|
155
|
+
|
|
156
|
+
const canAdvance = connState === "connected" && allSeeded;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div className="pt-8">
|
|
160
|
+
<h2 className="text-lg font-semibold text-[#111] mb-1">Database Connection</h2>
|
|
161
|
+
<p className="text-sm text-[#666] mb-8">
|
|
162
|
+
Connect to Sanity CMS to store your pages, settings, and content.
|
|
163
|
+
</p>
|
|
164
|
+
|
|
165
|
+
{/* Project ID status */}
|
|
166
|
+
<div className="bg-white rounded-xl border border-black/[0.06] divide-y divide-black/[0.06] mb-6">
|
|
167
|
+
{/* Sanity Project ID */}
|
|
168
|
+
<div className="flex items-center justify-between p-4">
|
|
169
|
+
<div>
|
|
170
|
+
<span className="text-sm font-medium text-[#333]">Sanity Project ID</span>
|
|
171
|
+
{hasProjectId && (
|
|
172
|
+
<p className="text-xs text-[#999] mt-0.5 font-mono">{projectId}</p>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
{hasProjectId ? <CheckCircle /> : <XCircle />}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{/* Connection status */}
|
|
179
|
+
<div className="flex items-center justify-between p-4">
|
|
180
|
+
<div>
|
|
181
|
+
<span className="text-sm font-medium text-[#333]">Connection</span>
|
|
182
|
+
{connState === "connected" && connResult?.dataset && (
|
|
183
|
+
<p className="text-xs text-[#999] mt-0.5">
|
|
184
|
+
Dataset: <span className="font-mono">{connResult.dataset}</span>
|
|
185
|
+
</p>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
{connState === "testing" && <Spinner />}
|
|
189
|
+
{connState === "connected" && <CheckCircle />}
|
|
190
|
+
{connState === "failed" && <XCircle />}
|
|
191
|
+
{connState === "idle" && (
|
|
192
|
+
<span className="text-xs text-[#bbb]">Not tested</span>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Write token */}
|
|
197
|
+
{connState === "connected" && (
|
|
198
|
+
<div className="flex items-center justify-between p-4">
|
|
199
|
+
<div>
|
|
200
|
+
<span className="text-sm font-medium text-[#333]">Write Token</span>
|
|
201
|
+
<p className="text-xs text-[#999] mt-0.5">Required for saving content</p>
|
|
202
|
+
</div>
|
|
203
|
+
{connResult?.hasWriteToken ? <CheckCircle /> : <XCircle />}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
{/* Document seeding */}
|
|
208
|
+
{connState === "connected" && (
|
|
209
|
+
<div className="flex items-center justify-between p-4">
|
|
210
|
+
<div>
|
|
211
|
+
<span className="text-sm font-medium text-[#333]">Initial Documents</span>
|
|
212
|
+
{seedState === "done" && seedResult && (
|
|
213
|
+
<p className="text-xs text-[#999] mt-0.5">
|
|
214
|
+
{seedResult.seeded.length > 0
|
|
215
|
+
? `Created: ${seedResult.seeded.join(", ")}`
|
|
216
|
+
: "All documents already exist"}
|
|
217
|
+
</p>
|
|
218
|
+
)}
|
|
219
|
+
{allSeeded && seedState !== "done" && (
|
|
220
|
+
<p className="text-xs text-[#999] mt-0.5">All documents present</p>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
{seedState === "seeding" && <Spinner />}
|
|
224
|
+
{(seedState === "done" || allSeeded) && <CheckCircle />}
|
|
225
|
+
{seedState === "error" && <XCircle />}
|
|
226
|
+
{seedState === "idle" && !allSeeded && (
|
|
227
|
+
<span className="text-xs text-[#bbb]">Not seeded</span>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{/* Instructions when no project ID */}
|
|
234
|
+
{!hasProjectId && (
|
|
235
|
+
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
|
236
|
+
<p className="text-sm text-amber-800 font-medium mb-2">
|
|
237
|
+
Sanity Project ID not found
|
|
238
|
+
</p>
|
|
239
|
+
<ol className="text-xs text-amber-700 space-y-1.5 list-decimal list-inside">
|
|
240
|
+
<li>
|
|
241
|
+
Create a project at{" "}
|
|
242
|
+
<a
|
|
243
|
+
href="https://www.sanity.io/manage"
|
|
244
|
+
target="_blank"
|
|
245
|
+
rel="noopener noreferrer"
|
|
246
|
+
className="underline hover:text-amber-900"
|
|
247
|
+
>
|
|
248
|
+
sanity.io/manage
|
|
249
|
+
</a>
|
|
250
|
+
</li>
|
|
251
|
+
<li>
|
|
252
|
+
Add <code className="bg-amber-100 px-1 rounded font-mono">NEXT_PUBLIC_SANITY_PROJECT_ID=your-id</code> to{" "}
|
|
253
|
+
<code className="bg-amber-100 px-1 rounded font-mono">.env.local</code>
|
|
254
|
+
</li>
|
|
255
|
+
<li>
|
|
256
|
+
Add <code className="bg-amber-100 px-1 rounded font-mono">SANITY_API_TOKEN=your-token</code> (Editor or higher)
|
|
257
|
+
</li>
|
|
258
|
+
<li>Restart the dev server and refresh this page</li>
|
|
259
|
+
</ol>
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{/* Error message */}
|
|
264
|
+
{error && connState === "failed" && (
|
|
265
|
+
<div className="bg-red-50 border border-red-200 rounded-lg p-3 mb-6">
|
|
266
|
+
<p className="text-xs text-red-700">{error}</p>
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
|
|
270
|
+
{/* Write token warning */}
|
|
271
|
+
{connState === "connected" && !connResult?.hasWriteToken && (
|
|
272
|
+
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 mb-6">
|
|
273
|
+
<p className="text-xs text-amber-700">
|
|
274
|
+
<span className="font-medium">Write token missing.</span> Add{" "}
|
|
275
|
+
<code className="bg-amber-100 px-1 rounded font-mono">SANITY_API_TOKEN</code> to{" "}
|
|
276
|
+
<code className="bg-amber-100 px-1 rounded font-mono">.env.local</code> and
|
|
277
|
+
restart the dev server. Without it, content changes won't be saved.
|
|
278
|
+
</p>
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
|
|
282
|
+
{/* Actions */}
|
|
283
|
+
<div className="flex items-center justify-between">
|
|
284
|
+
<div>
|
|
285
|
+
{onBack && (
|
|
286
|
+
<button
|
|
287
|
+
onClick={onBack}
|
|
288
|
+
className="px-4 py-2 text-sm text-[#666] hover:text-[#333] transition-colors"
|
|
289
|
+
>
|
|
290
|
+
Back
|
|
291
|
+
</button>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<div className="flex items-center gap-3">
|
|
296
|
+
{/* Test Connection */}
|
|
297
|
+
{hasProjectId && connState !== "connected" && (
|
|
298
|
+
<button
|
|
299
|
+
onClick={testConnection}
|
|
300
|
+
disabled={connState === "testing"}
|
|
301
|
+
className="px-4 py-2 text-sm border border-black/[0.1] rounded-lg hover:bg-black/[0.02] transition-colors disabled:opacity-50"
|
|
302
|
+
>
|
|
303
|
+
{connState === "testing" ? "Testing..." : "Test Connection"}
|
|
304
|
+
</button>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
{/* Seed Documents */}
|
|
308
|
+
{needsSeeding && seedState !== "done" && connResult?.hasWriteToken && (
|
|
309
|
+
<button
|
|
310
|
+
onClick={seedDocuments}
|
|
311
|
+
disabled={seedState === "seeding"}
|
|
312
|
+
className="px-4 py-2 text-sm bg-[#076bff] text-white rounded-lg hover:bg-[#0559d4] transition-colors disabled:opacity-50"
|
|
313
|
+
>
|
|
314
|
+
{seedState === "seeding" ? "Creating..." : "Create Initial Documents"}
|
|
315
|
+
</button>
|
|
316
|
+
)}
|
|
317
|
+
|
|
318
|
+
{/* Next */}
|
|
319
|
+
{canAdvance && (
|
|
320
|
+
<button
|
|
321
|
+
onClick={onNext}
|
|
322
|
+
className="px-5 py-2.5 bg-[#076bff] text-white text-sm font-medium rounded-lg hover:bg-[#0559d4] transition-colors"
|
|
323
|
+
>
|
|
324
|
+
Next
|
|
325
|
+
</button>
|
|
326
|
+
)}
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { csrfHeaders } from "../../../lib/csrf-client";
|
|
6
|
+
import type { WizardStepProps } from "./SetupWizard";
|
|
7
|
+
|
|
8
|
+
// ── Icons ──
|
|
9
|
+
|
|
10
|
+
function CheckCircle({ color = "#22c55e" }: { color?: string }) {
|
|
11
|
+
return (
|
|
12
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
13
|
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
|
14
|
+
<polyline points="22 4 12 14.01 9 11.01" />
|
|
15
|
+
</svg>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function SkipIcon() {
|
|
20
|
+
return (
|
|
21
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#bbb" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
22
|
+
<circle cx="12" cy="12" r="10" />
|
|
23
|
+
<line x1="8" y1="12" x2="16" y2="12" />
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Types ──
|
|
29
|
+
|
|
30
|
+
interface StepSummary {
|
|
31
|
+
label: string;
|
|
32
|
+
configured: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Component ──
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Step 5 — Done
|
|
39
|
+
*
|
|
40
|
+
* Shows a summary of what was configured, then marks setup as complete
|
|
41
|
+
* and redirects to the admin pages.
|
|
42
|
+
*/
|
|
43
|
+
export function DoneStep({ onBack }: WizardStepProps) {
|
|
44
|
+
const router = useRouter();
|
|
45
|
+
const [steps, setSteps] = useState<StepSummary[]>([]);
|
|
46
|
+
const [loading, setLoading] = useState(true);
|
|
47
|
+
const [completing, setCompleting] = useState(false);
|
|
48
|
+
const [error, setError] = useState<string | null>(null);
|
|
49
|
+
|
|
50
|
+
// Fetch current status to show summary
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
fetch("/api/admin/setup")
|
|
53
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
54
|
+
.then((data) => {
|
|
55
|
+
if (data?.steps) {
|
|
56
|
+
setSteps([
|
|
57
|
+
{ label: "Database", configured: data.steps.database },
|
|
58
|
+
{ label: "Storage", configured: data.steps.storage },
|
|
59
|
+
{ label: "Branding", configured: data.steps.branding },
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
setLoading(false);
|
|
63
|
+
})
|
|
64
|
+
.catch(() => setLoading(false));
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
const handleComplete = useCallback(async () => {
|
|
68
|
+
setCompleting(true);
|
|
69
|
+
setError(null);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Mark setup as complete via the setup API
|
|
73
|
+
const res = await fetch("/api/admin/setup/complete", {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: {
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
...csrfHeaders(),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
const data = await res.json();
|
|
83
|
+
setError(data.error || "Failed to complete setup");
|
|
84
|
+
setCompleting(false);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Redirect to admin pages
|
|
89
|
+
router.push("/admin/pages");
|
|
90
|
+
router.refresh();
|
|
91
|
+
} catch (err) {
|
|
92
|
+
setError(err instanceof Error ? err.message : "Failed to complete setup");
|
|
93
|
+
setCompleting(false);
|
|
94
|
+
}
|
|
95
|
+
}, [router]);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="pt-8">
|
|
99
|
+
<div className="text-center mb-8">
|
|
100
|
+
<div className="w-12 h-12 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-4">
|
|
101
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
102
|
+
<polyline points="20 6 9 17 4 12" />
|
|
103
|
+
</svg>
|
|
104
|
+
</div>
|
|
105
|
+
<h2 className="text-lg font-semibold text-[#111] mb-1">
|
|
106
|
+
You're all set!
|
|
107
|
+
</h2>
|
|
108
|
+
<p className="text-sm text-[#666]">
|
|
109
|
+
Here's a summary of your setup. You can reconfigure any of these later.
|
|
110
|
+
</p>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* Summary */}
|
|
114
|
+
{!loading && steps.length > 0 && (
|
|
115
|
+
<div className="bg-white rounded-xl border border-black/[0.06] divide-y divide-black/[0.06] mb-6">
|
|
116
|
+
{steps.map((step) => (
|
|
117
|
+
<div key={step.label} className="flex items-center justify-between p-4">
|
|
118
|
+
<span className="text-sm font-medium text-[#333]">
|
|
119
|
+
{step.label}
|
|
120
|
+
</span>
|
|
121
|
+
<div className="flex items-center gap-2">
|
|
122
|
+
{step.configured ? (
|
|
123
|
+
<>
|
|
124
|
+
<CheckCircle />
|
|
125
|
+
<span className="text-xs text-green-600 font-medium">
|
|
126
|
+
Configured
|
|
127
|
+
</span>
|
|
128
|
+
</>
|
|
129
|
+
) : (
|
|
130
|
+
<>
|
|
131
|
+
<SkipIcon />
|
|
132
|
+
<span className="text-xs text-[#bbb]">Skipped</span>
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{/* Loading state for summary */}
|
|
142
|
+
{loading && (
|
|
143
|
+
<div className="bg-white rounded-xl border border-black/[0.06] p-8 mb-6 text-center">
|
|
144
|
+
<p className="text-xs text-[#999] animate-pulse">
|
|
145
|
+
Loading summary...
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{/* Tip */}
|
|
151
|
+
<div className="p-3 rounded-lg bg-[#f8f8f8] border border-black/[0.04] mb-6">
|
|
152
|
+
<p className="text-xs text-[#999] leading-relaxed">
|
|
153
|
+
You can re-run this wizard anytime from the admin sidebar.
|
|
154
|
+
Skipped steps can be configured from the corresponding admin pages.
|
|
155
|
+
</p>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{error && (
|
|
159
|
+
<div className="p-3 rounded-lg bg-red-50 border border-red-200 mb-6">
|
|
160
|
+
<p className="text-xs text-red-700">{error}</p>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{/* Actions */}
|
|
165
|
+
<div className="flex items-center justify-between">
|
|
166
|
+
<div>
|
|
167
|
+
{onBack && (
|
|
168
|
+
<button
|
|
169
|
+
onClick={onBack}
|
|
170
|
+
className="px-4 py-2 text-sm text-[#666] hover:text-[#333] transition-colors"
|
|
171
|
+
>
|
|
172
|
+
Back
|
|
173
|
+
</button>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<button
|
|
178
|
+
onClick={handleComplete}
|
|
179
|
+
disabled={completing}
|
|
180
|
+
className="px-6 py-2.5 bg-[#076bff] text-white text-sm font-medium rounded-lg hover:bg-[#0559d4] transition-colors disabled:opacity-50"
|
|
181
|
+
>
|
|
182
|
+
{completing ? "Starting..." : "Start Building"}
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { getSiteConfig } from "../../../lib/config";
|
|
6
|
+
import type { SetupStatus } from "../../../lib/setup/detect";
|
|
7
|
+
import { WelcomeStep } from "./WelcomeStep";
|
|
8
|
+
import { DatabaseStep } from "./DatabaseStep";
|
|
9
|
+
import { StorageStep } from "./StorageStep";
|
|
10
|
+
import { BrandingStep } from "./BrandingStep";
|
|
11
|
+
import { DoneStep } from "./DoneStep";
|
|
12
|
+
|
|
13
|
+
// ── Types ──
|
|
14
|
+
|
|
15
|
+
export interface WizardStepProps {
|
|
16
|
+
onNext: () => void;
|
|
17
|
+
onBack?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface StepDef {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
component: React.ComponentType<WizardStepProps>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const STEPS: StepDef[] = [
|
|
27
|
+
{ id: "welcome", label: "Welcome", component: WelcomeStep },
|
|
28
|
+
{ id: "database", label: "Database", component: DatabaseStep },
|
|
29
|
+
{ id: "storage", label: "Storage", component: StorageStep },
|
|
30
|
+
{ id: "branding", label: "Branding", component: BrandingStep },
|
|
31
|
+
{ id: "done", label: "Done", component: DoneStep },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// ── Main Wizard ──
|
|
35
|
+
|
|
36
|
+
interface SetupWizardProps {
|
|
37
|
+
initialStatus: SetupStatus;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function SetupWizard({ initialStatus }: SetupWizardProps) {
|
|
41
|
+
const router = useRouter();
|
|
42
|
+
const config = getSiteConfig();
|
|
43
|
+
|
|
44
|
+
// Determine initial step based on what's already configured
|
|
45
|
+
const getInitialStep = (): number => {
|
|
46
|
+
if (!initialStatus.steps.database) return 0; // Welcome → Database
|
|
47
|
+
if (!initialStatus.steps.storage) return 2; // Jump to Storage
|
|
48
|
+
if (!initialStatus.steps.branding) return 3; // Jump to Branding
|
|
49
|
+
return 0; // All done — shouldn't be here, but start at Welcome
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const [currentStep, setCurrentStep] = useState(getInitialStep);
|
|
53
|
+
|
|
54
|
+
const handleNext = useCallback(() => {
|
|
55
|
+
if (currentStep < STEPS.length - 1) {
|
|
56
|
+
setCurrentStep((s) => s + 1);
|
|
57
|
+
} else {
|
|
58
|
+
// Final step — redirect to admin
|
|
59
|
+
router.push("/admin/pages");
|
|
60
|
+
router.refresh();
|
|
61
|
+
}
|
|
62
|
+
}, [currentStep, router]);
|
|
63
|
+
|
|
64
|
+
const handleBack = useCallback(() => {
|
|
65
|
+
if (currentStep > 0) {
|
|
66
|
+
setCurrentStep((s) => s - 1);
|
|
67
|
+
}
|
|
68
|
+
}, [currentStep]);
|
|
69
|
+
|
|
70
|
+
const handleSkip = useCallback(() => {
|
|
71
|
+
router.push("/admin/pages");
|
|
72
|
+
router.refresh();
|
|
73
|
+
}, [router]);
|
|
74
|
+
|
|
75
|
+
const StepComponent = STEPS[currentStep].component;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
className="min-h-screen bg-[#f8f8f8] flex flex-col"
|
|
80
|
+
style={{ fontFamily: "Inter, system-ui, sans-serif" }}
|
|
81
|
+
>
|
|
82
|
+
{/* Top bar */}
|
|
83
|
+
<header className="flex items-center justify-between px-8 py-4 border-b border-black/[0.06]">
|
|
84
|
+
<span className="text-xs font-semibold tracking-widest uppercase text-[#333]">
|
|
85
|
+
{config.name} <span className="text-[#999] font-normal">Setup</span>
|
|
86
|
+
</span>
|
|
87
|
+
<button
|
|
88
|
+
onClick={handleSkip}
|
|
89
|
+
className="text-xs text-[#999] hover:text-[#333] transition-colors"
|
|
90
|
+
>
|
|
91
|
+
Skip for now
|
|
92
|
+
</button>
|
|
93
|
+
</header>
|
|
94
|
+
|
|
95
|
+
{/* Stepper */}
|
|
96
|
+
<div className="flex justify-center pt-8 pb-4 px-8">
|
|
97
|
+
<div className="flex items-center gap-2">
|
|
98
|
+
{STEPS.map((step, i) => {
|
|
99
|
+
const isActive = i === currentStep;
|
|
100
|
+
const isDone = i < currentStep;
|
|
101
|
+
return (
|
|
102
|
+
<div key={step.id} className="flex items-center gap-2">
|
|
103
|
+
{/* Step dot + label */}
|
|
104
|
+
<div className="flex items-center gap-1.5">
|
|
105
|
+
<div
|
|
106
|
+
className={`w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-semibold transition-colors ${
|
|
107
|
+
isActive
|
|
108
|
+
? "bg-[#076bff] text-white"
|
|
109
|
+
: isDone
|
|
110
|
+
? "bg-[#076bff]/20 text-[#076bff]"
|
|
111
|
+
: "bg-black/[0.06] text-[#999]"
|
|
112
|
+
}`}
|
|
113
|
+
>
|
|
114
|
+
{isDone ? (
|
|
115
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
|
116
|
+
<polyline points="20 6 9 17 4 12" />
|
|
117
|
+
</svg>
|
|
118
|
+
) : (
|
|
119
|
+
i + 1
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
<span
|
|
123
|
+
className={`text-xs transition-colors ${
|
|
124
|
+
isActive
|
|
125
|
+
? "text-[#333] font-medium"
|
|
126
|
+
: isDone
|
|
127
|
+
? "text-[#076bff]"
|
|
128
|
+
: "text-[#999]"
|
|
129
|
+
}`}
|
|
130
|
+
>
|
|
131
|
+
{step.label}
|
|
132
|
+
</span>
|
|
133
|
+
</div>
|
|
134
|
+
{/* Connector line */}
|
|
135
|
+
{i < STEPS.length - 1 && (
|
|
136
|
+
<div
|
|
137
|
+
className={`w-8 h-px transition-colors ${
|
|
138
|
+
i < currentStep ? "bg-[#076bff]/30" : "bg-black/[0.08]"
|
|
139
|
+
}`}
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
})}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{/* Step content */}
|
|
149
|
+
<div className="flex-1 flex justify-center px-8 pb-12">
|
|
150
|
+
<div className="w-full max-w-lg">
|
|
151
|
+
<StepComponent
|
|
152
|
+
onNext={handleNext}
|
|
153
|
+
onBack={currentStep > 0 ? handleBack : undefined}
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Footer */}
|
|
159
|
+
<footer className="px-8 py-4 border-t border-black/[0.06] text-center">
|
|
160
|
+
<span className="text-[11px] text-[#bbb]">
|
|
161
|
+
Step {currentStep + 1} of {STEPS.length}
|
|
162
|
+
</span>
|
|
163
|
+
</footer>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|