@morphika/andami 0.1.3 → 0.1.5
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/app/(site)/[slug]/page.tsx +2 -2
- package/app/(site)/layout.tsx +1 -0
- package/app/(site)/page.tsx +2 -2
- package/app/(site)/preview/page.tsx +4 -4
- package/app/(site)/work/[slug]/page.tsx +2 -2
- package/app/admin/layout.tsx +2 -2
- package/app/admin/login/page.tsx +5 -5
- package/app/admin/navigation/page.tsx +255 -157
- package/app/api/admin/assets/relink/confirm/route.ts +1 -1
- package/app/api/admin/pages/[slug]/route.ts +1 -1
- package/app/api/admin/settings/route.ts +40 -15
- package/app/api/admin/setup/complete/route.ts +1 -1
- package/app/api/admin/setup/route.ts +6 -3
- package/components/admin/index.ts +7 -0
- package/components/admin/nav-builder/NavGeneralSettings.tsx +11 -15
- package/components/admin/nav-builder/NavItemSettings.tsx +29 -5
- package/components/admin/nav-builder/NavLivePreview.tsx +4 -1
- package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -0
- package/components/admin/nav-builder/NavMobileSettings.tsx +223 -0
- package/components/admin/nav-builder/index.ts +2 -0
- package/components/blocks/BlockRenderer.tsx +65 -13
- package/components/blocks/ButtonBlockRenderer.tsx +29 -6
- package/components/blocks/CoverBlockRenderer.tsx +36 -14
- package/components/blocks/ImageBlockRenderer.tsx +5 -3
- package/components/blocks/ImageGridBlockRenderer.tsx +13 -6
- package/components/blocks/PageRenderer.tsx +4 -2
- package/components/blocks/ProjectGridBlockRenderer.tsx +18 -3
- package/components/blocks/SectionRenderer.tsx +9 -8
- package/components/blocks/SectionV2Renderer.tsx +8 -8
- package/components/blocks/SpacerBlockRenderer.tsx +4 -2
- package/components/blocks/TextBlockRenderer.tsx +9 -4
- package/components/builder/BuilderCanvas.tsx +10 -4
- package/components/builder/ColorPicker.tsx +51 -243
- package/components/builder/ColorSwatchPicker.tsx +214 -274
- package/components/builder/DndWrapper.tsx +5 -2
- package/components/builder/SectionV2Canvas.tsx +15 -4
- package/components/builder/asset-browser/useAssetBrowser.ts +9 -1
- package/components/builder/color-picker/AlphaSlider.tsx +141 -0
- package/components/builder/color-picker/AngleControl.tsx +138 -0
- package/components/builder/color-picker/ColorInputs.tsx +105 -0
- package/components/builder/color-picker/EyedropperButton.tsx +74 -0
- package/components/builder/color-picker/GradientBar.tsx +222 -0
- package/components/builder/color-picker/GradientPreview.tsx +53 -0
- package/components/builder/color-picker/HueSlider.tsx +124 -0
- package/components/builder/color-picker/MeshCanvas.tsx +172 -0
- package/components/builder/color-picker/MeshPointEditor.tsx +133 -0
- package/components/builder/color-picker/MeshPointList.tsx +200 -0
- package/components/builder/color-picker/PositionControl.tsx +158 -0
- package/components/builder/color-picker/SaturationCanvas.tsx +142 -0
- package/components/builder/color-picker/StopEditor.tsx +178 -0
- package/components/builder/color-picker/SwatchBar.tsx +93 -0
- package/components/builder/color-picker/UnifiedColorPicker.tsx +713 -0
- package/components/builder/color-picker/index.ts +62 -0
- package/components/builder/color-picker/types.ts +115 -0
- package/components/builder/color-picker/utils.ts +138 -0
- package/components/builder/editors/CoverBlockEditor.tsx +86 -32
- package/components/builder/editors/ProjectGridEditor.tsx +51 -4
- package/components/builder/hooks/useColumnDrag.ts +25 -27
- package/components/builder/settings-panel/BlockLayoutTab.tsx +29 -7
- package/components/builder/settings-panel/LayoutTab.tsx +382 -310
- package/components/builder/settings-panel/PageSettings.tsx +6 -4
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +392 -312
- package/components/builder/settings-panel/SectionV2Settings.tsx +65 -35
- package/components/ui/Navbar.tsx +95 -25
- package/components/ui/PortfolioTracker.tsx +3 -3
- package/lib/assets.ts +1 -1
- package/lib/auth.ts +1 -1
- package/lib/builder/gradient-presets.ts +128 -0
- package/lib/builder/layout-styles.ts +16 -10
- package/lib/builder/serializer.ts +1 -0
- package/lib/builder/store-blocks.ts +48 -61
- package/lib/builder/store-helpers.ts +31 -14
- package/lib/builder/store.ts +59 -41
- package/lib/builder/types.ts +14 -0
- package/lib/color-utils.ts +200 -0
- package/lib/revalidate.ts +2 -2
- package/lib/sanity/queries.ts +4 -3
- package/lib/sanity/types.ts +76 -1
- package/lib/setup/detect.ts +1 -1
- package/package.json +8 -2
- package/sanity/schemas/siteSettings.ts +34 -0
- package/styles/base.css +3 -3
- package/app/globals.css +0 -7
|
@@ -27,8 +27,8 @@ async function getPageBySlug(slug: string): Promise<Page | null> {
|
|
|
27
27
|
|
|
28
28
|
export async function generateStaticParams() {
|
|
29
29
|
try {
|
|
30
|
-
const slugs
|
|
31
|
-
return slugs.map((slug) => ({ slug }));
|
|
30
|
+
const slugs = await client.fetch<string[] | null>(allPageSlugsQuery);
|
|
31
|
+
return (slugs ?? []).filter(Boolean).map((slug) => ({ slug }));
|
|
32
32
|
} catch {
|
|
33
33
|
return [];
|
|
34
34
|
}
|
package/app/(site)/layout.tsx
CHANGED
|
@@ -39,6 +39,7 @@ export default async function SiteLayout({
|
|
|
39
39
|
<Navbar
|
|
40
40
|
navItems={settings?.nav_items}
|
|
41
41
|
design={settings?.nav_design}
|
|
42
|
+
mobileDesign={settings?.nav_mobile_design}
|
|
42
43
|
/>
|
|
43
44
|
<main className="flex-1">{children}</main>
|
|
44
45
|
{cfg.features.customCursor && <CustomCursor />}
|
package/app/(site)/page.tsx
CHANGED
|
@@ -24,10 +24,10 @@ export default async function HomePage() {
|
|
|
24
24
|
return (
|
|
25
25
|
<main className="flex min-h-screen items-center justify-center bg-brand-dark">
|
|
26
26
|
<div className="text-center">
|
|
27
|
-
<h1 className="font-
|
|
27
|
+
<h1 className="font-sans text-2xl uppercase tracking-widest text-brand-accent">
|
|
28
28
|
{cfg.name}
|
|
29
29
|
</h1>
|
|
30
|
-
<p className="mt-4 font-
|
|
30
|
+
<p className="mt-4 font-sans text-sm uppercase tracking-wider text-brand-muted">
|
|
31
31
|
Coming Soon
|
|
32
32
|
</p>
|
|
33
33
|
</div>
|
|
@@ -53,7 +53,7 @@ function PreviewContent() {
|
|
|
53
53
|
if (loading) {
|
|
54
54
|
return (
|
|
55
55
|
<div className="flex min-h-[50vh] items-center justify-center">
|
|
56
|
-
<p className="font-
|
|
56
|
+
<p className="font-sans text-sm text-neutral-400 animate-pulse">
|
|
57
57
|
Loading preview...
|
|
58
58
|
</p>
|
|
59
59
|
</div>
|
|
@@ -64,10 +64,10 @@ function PreviewContent() {
|
|
|
64
64
|
return (
|
|
65
65
|
<div className="flex min-h-[50vh] items-center justify-center">
|
|
66
66
|
<div className="text-center">
|
|
67
|
-
<p className="font-
|
|
67
|
+
<p className="font-sans text-sm text-[var(--admin-error)]">
|
|
68
68
|
{error || "Page not found"}
|
|
69
69
|
</p>
|
|
70
|
-
<p className="mt-2 font-
|
|
70
|
+
<p className="mt-2 font-sans text-xs text-neutral-400">
|
|
71
71
|
Make sure the page has been saved at least once.
|
|
72
72
|
</p>
|
|
73
73
|
</div>
|
|
@@ -87,7 +87,7 @@ export default function PreviewPage() {
|
|
|
87
87
|
<Suspense
|
|
88
88
|
fallback={
|
|
89
89
|
<div className="flex min-h-[50vh] items-center justify-center">
|
|
90
|
-
<p className="font-
|
|
90
|
+
<p className="font-sans text-sm text-neutral-400 animate-pulse">
|
|
91
91
|
Loading preview...
|
|
92
92
|
</p>
|
|
93
93
|
</div>
|
|
@@ -28,8 +28,8 @@ async function getProjectPage(slug: string): Promise<Page | null> {
|
|
|
28
28
|
|
|
29
29
|
export async function generateStaticParams() {
|
|
30
30
|
try {
|
|
31
|
-
const slugs = await client.fetch<string[]>(allProjectSlugsQuery);
|
|
32
|
-
return slugs.map((slug) => ({ slug }));
|
|
31
|
+
const slugs = await client.fetch<string[] | null>(allProjectSlugsQuery);
|
|
32
|
+
return (slugs ?? []).filter(Boolean).map((slug) => ({ slug }));
|
|
33
33
|
} catch {
|
|
34
34
|
return [];
|
|
35
35
|
}
|
package/app/admin/layout.tsx
CHANGED
|
@@ -161,8 +161,8 @@ export default function AdminLayout({
|
|
|
161
161
|
<div className="flex h-14 items-center justify-between px-4 border-b border-white/[0.06]">
|
|
162
162
|
{sidebarOpen && (
|
|
163
163
|
<span className="text-[10px] font-semibold tracking-widest uppercase text-white/90 leading-tight">
|
|
164
|
-
{getSiteConfig().
|
|
165
|
-
<span className="text-white/50 font-normal">
|
|
164
|
+
{getSiteConfig().adminTitle || "Morphika Andami"}<br />
|
|
165
|
+
<span className="text-white/50 font-normal">{getSiteConfig().name}</span>
|
|
166
166
|
</span>
|
|
167
167
|
)}
|
|
168
168
|
<button
|
package/app/admin/login/page.tsx
CHANGED
|
@@ -42,10 +42,10 @@ function LoginForm() {
|
|
|
42
42
|
<div className="flex min-h-screen items-center justify-center bg-[#0a0a0a]">
|
|
43
43
|
<div className="w-full max-w-sm px-6">
|
|
44
44
|
<div className="mb-8 text-center">
|
|
45
|
-
<h1 className="font-
|
|
45
|
+
<h1 className="font-sans text-xl uppercase tracking-widest text-white">
|
|
46
46
|
{getSiteConfig().adminTitle}
|
|
47
47
|
</h1>
|
|
48
|
-
<p className="mt-2 font-
|
|
48
|
+
<p className="mt-2 font-sans text-xs uppercase tracking-wider text-neutral-500">
|
|
49
49
|
Enter password to continue
|
|
50
50
|
</p>
|
|
51
51
|
</div>
|
|
@@ -58,18 +58,18 @@ function LoginForm() {
|
|
|
58
58
|
onChange={(e) => setPassword(e.target.value)}
|
|
59
59
|
placeholder="Password"
|
|
60
60
|
autoFocus
|
|
61
|
-
className="w-full rounded-none border border-neutral-700 bg-transparent px-4 py-3 font-
|
|
61
|
+
className="w-full rounded-none border border-neutral-700 bg-transparent px-4 py-3 font-sans text-sm text-white placeholder-neutral-600 outline-none transition-colors focus:border-neutral-400"
|
|
62
62
|
/>
|
|
63
63
|
</div>
|
|
64
64
|
|
|
65
65
|
{error && (
|
|
66
|
-
<p className="font-
|
|
66
|
+
<p className="font-sans text-xs text-red-500">{error}</p>
|
|
67
67
|
)}
|
|
68
68
|
|
|
69
69
|
<button
|
|
70
70
|
type="submit"
|
|
71
71
|
disabled={isLoading || !password}
|
|
72
|
-
className="w-full border border-white bg-white px-4 py-3 font-
|
|
72
|
+
className="w-full border border-white bg-white px-4 py-3 font-sans text-sm uppercase tracking-wider text-black transition-all hover:bg-transparent hover:text-white disabled:opacity-40 disabled:cursor-not-allowed"
|
|
73
73
|
>
|
|
74
74
|
{isLoading ? "..." : "Login"}
|
|
75
75
|
</button>
|
|
@@ -1,157 +1,255 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useCallback } from "react";
|
|
4
|
-
import { csrfHeaders } from "../../../lib/csrf-client";
|
|
5
|
-
import { NavBuilder } from "../../../components/admin/nav-builder";
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { csrfHeaders } from "../../../lib/csrf-client";
|
|
5
|
+
import { NavBuilder } from "../../../components/admin/nav-builder";
|
|
6
|
+
import NavMobileSettings from "../../../components/admin/nav-builder/NavMobileSettings";
|
|
7
|
+
import { revalidateSite } from "../../../lib/revalidate";
|
|
8
|
+
import type { NavItem, NavDesign, MobileNavDesign, SiteSettings } from "../../../lib/sanity/types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* /admin/navigation — Navigation editing.
|
|
12
|
+
* Session 129: Footer tab removed — footers are now custom sections placed freely by the user.
|
|
13
|
+
* Session 158: Added "Mobile Menu" tab for independent mobile menu styles.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type NavTabId = "desktop" | "mobile";
|
|
17
|
+
|
|
18
|
+
const TABS: { id: NavTabId; label: string }[] = [
|
|
19
|
+
{ id: "desktop", label: "Desktop" },
|
|
20
|
+
{ id: "mobile", label: "Mobile Menu" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export default function AdminNavigationPage() {
|
|
24
|
+
const [settings, setSettings] = useState<SiteSettings | null>(null);
|
|
25
|
+
const [loading, setLoading] = useState(true);
|
|
26
|
+
const [activeTab, setActiveTab] = useState<NavTabId>("desktop");
|
|
27
|
+
const [savingNav, setSavingNav] = useState(false);
|
|
28
|
+
const [savingMobile, setSavingMobile] = useState(false);
|
|
29
|
+
const [mobileDesign, setMobileDesign] = useState<MobileNavDesign>({});
|
|
30
|
+
const [mobileHasChanges, setMobileHasChanges] = useState(false);
|
|
31
|
+
const [message, setMessage] = useState<{
|
|
32
|
+
type: "success" | "error";
|
|
33
|
+
text: string;
|
|
34
|
+
} | null>(null);
|
|
35
|
+
|
|
36
|
+
// ── Load settings ──
|
|
37
|
+
const fetchSettings = useCallback(async () => {
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch("/api/admin/settings");
|
|
40
|
+
if (res.ok) {
|
|
41
|
+
const data = await res.json();
|
|
42
|
+
setSettings(data.settings || null);
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
setMessage({ type: "error", text: "Failed to load navigation" });
|
|
46
|
+
} finally {
|
|
47
|
+
setLoading(false);
|
|
48
|
+
}
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
fetchSettings();
|
|
53
|
+
}, [fetchSettings]);
|
|
54
|
+
|
|
55
|
+
// Sync mobile design from loaded settings
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (settings?.nav_mobile_design) {
|
|
58
|
+
setMobileDesign(settings.nav_mobile_design);
|
|
59
|
+
}
|
|
60
|
+
}, [settings]);
|
|
61
|
+
|
|
62
|
+
// ── Auto-dismiss messages ──
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!message) return;
|
|
65
|
+
const timer = setTimeout(() => setMessage(null), 5000);
|
|
66
|
+
return () => clearTimeout(timer);
|
|
67
|
+
}, [message]);
|
|
68
|
+
|
|
69
|
+
// ── Save handlers ──
|
|
70
|
+
const handleSaveNavBuilder = async (navItems: NavItem[], navDesign: NavDesign) => {
|
|
71
|
+
setSavingNav(true);
|
|
72
|
+
setMessage(null);
|
|
73
|
+
try {
|
|
74
|
+
// Sanitize items for Sanity
|
|
75
|
+
const sanitizedItems = navItems.map((item) => ({
|
|
76
|
+
_key: item._key,
|
|
77
|
+
type: item.type || "menu-item",
|
|
78
|
+
label: item.label,
|
|
79
|
+
...(item.logo_image ? { logo_image: item.logo_image } : {}),
|
|
80
|
+
link_type: item.link_type,
|
|
81
|
+
...(item.link_type === "internal" && item.internal_page
|
|
82
|
+
? { internal_page: { _ref: item.internal_page._id } }
|
|
83
|
+
: {}),
|
|
84
|
+
...(item.link_type === "external"
|
|
85
|
+
? { external_url: item.external_url }
|
|
86
|
+
: {}),
|
|
87
|
+
...(item.link_type === "content" ? {
|
|
88
|
+
...(item.content_type ? { content_type: item.content_type } : {}),
|
|
89
|
+
...(item.content_asset ? { content_asset: item.content_asset } : {}),
|
|
90
|
+
...(item.content_url ? { content_url: item.content_url } : {}),
|
|
91
|
+
} : {}),
|
|
92
|
+
visible: item.visible,
|
|
93
|
+
grid_column: item.grid_column,
|
|
94
|
+
column_span: item.column_span || 1,
|
|
95
|
+
...(item.style_overrides && Object.keys(item.style_overrides).length > 0
|
|
96
|
+
? { style_overrides: item.style_overrides }
|
|
97
|
+
: {}),
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
// Save nav items
|
|
101
|
+
const navRes = await fetch("/api/admin/settings", {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { "Content-Type": "application/json", ...csrfHeaders() },
|
|
104
|
+
body: JSON.stringify({ section: "navigation", data: { nav_items: sanitizedItems } }),
|
|
105
|
+
});
|
|
106
|
+
if (!navRes.ok) {
|
|
107
|
+
const errData = await navRes.json();
|
|
108
|
+
throw new Error(errData.error || "Save items failed");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Save nav design
|
|
112
|
+
const designRes = await fetch("/api/admin/settings", {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: { "Content-Type": "application/json", ...csrfHeaders() },
|
|
115
|
+
body: JSON.stringify({ section: "nav_design", data: { nav_design: navDesign } }),
|
|
116
|
+
});
|
|
117
|
+
if (!designRes.ok) {
|
|
118
|
+
const errData = await designRes.json();
|
|
119
|
+
throw new Error(errData.error || "Save design failed");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setMessage({ type: "success", text: "Navigation saved" });
|
|
123
|
+
|
|
124
|
+
// Refresh settings
|
|
125
|
+
const settingsRes = await fetch("/api/admin/settings");
|
|
126
|
+
if (settingsRes.ok) {
|
|
127
|
+
const settingsData = await settingsRes.json();
|
|
128
|
+
setSettings(settingsData.settings || null);
|
|
129
|
+
}
|
|
130
|
+
revalidateSite();
|
|
131
|
+
} catch (err) {
|
|
132
|
+
setMessage({
|
|
133
|
+
type: "error",
|
|
134
|
+
text: err instanceof Error ? err.message : "Save failed",
|
|
135
|
+
});
|
|
136
|
+
} finally {
|
|
137
|
+
setSavingNav(false);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// ── Mobile menu design handlers ──
|
|
142
|
+
const handleMobileDesignChange = useCallback((newDesign: MobileNavDesign) => {
|
|
143
|
+
setMobileDesign(newDesign);
|
|
144
|
+
setMobileHasChanges(true);
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
const handleSaveMobileDesign = useCallback(async () => {
|
|
148
|
+
setSavingMobile(true);
|
|
149
|
+
setMessage(null);
|
|
150
|
+
try {
|
|
151
|
+
const res = await fetch("/api/admin/settings", {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: { "Content-Type": "application/json", ...csrfHeaders() },
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
section: "nav_mobile_design",
|
|
156
|
+
data: { nav_mobile_design: mobileDesign },
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
const errData = await res.json();
|
|
161
|
+
throw new Error(errData.error || "Save mobile design failed");
|
|
162
|
+
}
|
|
163
|
+
setMobileHasChanges(false);
|
|
164
|
+
setMessage({ type: "success", text: "Mobile menu saved" });
|
|
165
|
+
|
|
166
|
+
// Refresh settings
|
|
167
|
+
const settingsRes = await fetch("/api/admin/settings");
|
|
168
|
+
if (settingsRes.ok) {
|
|
169
|
+
const settingsData = await settingsRes.json();
|
|
170
|
+
setSettings(settingsData.settings || null);
|
|
171
|
+
}
|
|
172
|
+
revalidateSite();
|
|
173
|
+
} catch (err) {
|
|
174
|
+
setMessage({
|
|
175
|
+
type: "error",
|
|
176
|
+
text: err instanceof Error ? err.message : "Save failed",
|
|
177
|
+
});
|
|
178
|
+
} finally {
|
|
179
|
+
setSavingMobile(false);
|
|
180
|
+
}
|
|
181
|
+
}, [mobileDesign]);
|
|
182
|
+
|
|
183
|
+
if (loading) {
|
|
184
|
+
return (
|
|
185
|
+
<div className="flex items-center justify-center py-20">
|
|
186
|
+
<span className="text-sm text-neutral-400 animate-pulse">
|
|
187
|
+
Loading navigation...
|
|
188
|
+
</span>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div className="space-y-6">
|
|
195
|
+
<h1 className="text-2xl font-semibold text-neutral-900">
|
|
196
|
+
Navigation
|
|
197
|
+
</h1>
|
|
198
|
+
|
|
199
|
+
{/* Tab navigation — matches admin Customize style */}
|
|
200
|
+
<div className="flex items-center gap-1 border-b border-neutral-200">
|
|
201
|
+
{TABS.map((tab) => {
|
|
202
|
+
const isActive = activeTab === tab.id;
|
|
203
|
+
return (
|
|
204
|
+
<button
|
|
205
|
+
key={tab.id}
|
|
206
|
+
onClick={() => setActiveTab(tab.id)}
|
|
207
|
+
className={`flex items-center gap-1.5 px-4 py-2.5 text-[13px] font-medium transition-colors border-b-2 -mb-px ${
|
|
208
|
+
isActive
|
|
209
|
+
? "text-neutral-900 border-[#076bff]"
|
|
210
|
+
: "text-neutral-400 border-transparent hover:text-neutral-600"
|
|
211
|
+
}`}
|
|
212
|
+
>
|
|
213
|
+
{tab.label}
|
|
214
|
+
</button>
|
|
215
|
+
);
|
|
216
|
+
})}
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{/* Toast message */}
|
|
220
|
+
{message && (
|
|
221
|
+
<div
|
|
222
|
+
className={`p-3 rounded-xl border transition-opacity ${
|
|
223
|
+
message.type === "success"
|
|
224
|
+
? "border-green-200 bg-green-50 text-green-700"
|
|
225
|
+
: "border-red-200 bg-red-50 text-red-700"
|
|
226
|
+
}`}
|
|
227
|
+
>
|
|
228
|
+
<p className="text-sm">{message.text}</p>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{/* Tab content */}
|
|
233
|
+
{activeTab === "desktop" && (
|
|
234
|
+
<NavBuilder
|
|
235
|
+
initialItems={settings?.nav_items || []}
|
|
236
|
+
initialDesign={settings?.nav_design || {}}
|
|
237
|
+
onSave={handleSaveNavBuilder}
|
|
238
|
+
saving={savingNav}
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{activeTab === "mobile" && (
|
|
243
|
+
<NavMobileSettings
|
|
244
|
+
design={mobileDesign}
|
|
245
|
+
desktopDesign={settings?.nav_design || {}}
|
|
246
|
+
items={settings?.nav_items || []}
|
|
247
|
+
onChange={handleMobileDesignChange}
|
|
248
|
+
onSave={handleSaveMobileDesign}
|
|
249
|
+
saving={savingMobile}
|
|
250
|
+
hasChanges={mobileHasChanges}
|
|
251
|
+
/>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
@@ -210,7 +210,7 @@ export async function POST(request: NextRequest) {
|
|
|
210
210
|
// Also update siteSettings seed_url if it changed
|
|
211
211
|
if (new_seed_url && new_seed_url !== oldSeed) {
|
|
212
212
|
const settings = await client.fetch(
|
|
213
|
-
`*[
|
|
213
|
+
`*[_id == "siteSettings"][0]._id`
|
|
214
214
|
);
|
|
215
215
|
if (settings) {
|
|
216
216
|
transaction.patch(settings, {
|
|
@@ -577,7 +577,7 @@ export async function DELETE(
|
|
|
577
577
|
if (referencing > 0) {
|
|
578
578
|
// Remove navigation references to this page before deleting
|
|
579
579
|
const siteSettings = await client.fetch(
|
|
580
|
-
`*[
|
|
580
|
+
`*[_id == "siteSettings"][0]{ _id, nav_items }`
|
|
581
581
|
);
|
|
582
582
|
if (siteSettings?.nav_items?.length) {
|
|
583
583
|
const filtered = siteSettings.nav_items.filter((item: RawNavItem) =>
|