@morphika/andami 0.5.0 → 0.5.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/README.md +151 -36
- package/app/admin/assets/page.tsx +6 -6
- package/app/admin/database/page.tsx +302 -302
- package/app/admin/error.tsx +53 -53
- package/app/admin/layout.tsx +320 -327
- package/app/admin/navigation/page.tsx +255 -255
- package/app/admin/pages/[slug]/page.tsx +6 -6
- package/app/admin/pages/page.tsx +11 -11
- package/app/admin/projects/page.tsx +14 -14
- package/app/admin/setup/page.tsx +1 -1
- package/app/admin/styles/page.tsx +1 -1
- package/components/admin/MetadataEditor.tsx +6 -6
- package/components/admin/nav-builder/NavBuilder.tsx +1 -1
- package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
- package/components/admin/nav-builder/NavGridCell.tsx +48 -48
- package/components/admin/nav-builder/NavGridItem.tsx +4 -4
- package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
- package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
- package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
- package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
- package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
- package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
- package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
- package/components/admin/setup-wizard/DoneStep.tsx +1 -1
- package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
- package/components/admin/setup-wizard/StorageStep.tsx +2 -2
- package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
- package/components/admin/styles/ColorsEditor.tsx +2 -2
- package/components/admin/styles/FontsEditor.tsx +6 -6
- package/components/admin/styles/GridLayoutEditor.tsx +9 -9
- package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
- package/components/admin/styles/TypographyEditor.tsx +6 -6
- package/components/admin/styles/shared.tsx +68 -68
- package/components/blocks/AudioBlockRenderer.tsx +286 -0
- package/components/blocks/BeforeAfterBlockRenderer.tsx +274 -0
- package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
- package/components/builder/BlockCardIcons.tsx +316 -227
- package/components/builder/BlockTypePicker.tsx +3 -1
- package/components/builder/BubbleIcons.tsx +90 -0
- package/components/builder/BuilderCanvas.tsx +2 -0
- package/components/builder/CanvasMinimap.tsx +2 -2
- package/components/builder/CoverSectionCanvas.tsx +363 -275
- package/components/builder/DeviceFrame.tsx +1 -1
- package/components/builder/DndWrapper.tsx +3 -3
- package/components/builder/InsertionLines.tsx +1 -1
- package/components/builder/SectionCardIcons.tsx +421 -320
- package/components/builder/SectionEditorBar.tsx +1 -1
- package/components/builder/SectionTypePicker.tsx +4 -4
- package/components/builder/SectionV2Canvas.tsx +20 -4
- package/components/builder/SectionV2Column.tsx +74 -68
- package/components/builder/SortableBlock.tsx +93 -73
- package/components/builder/SortableRow.tsx +27 -26
- package/components/builder/VirtualAssetGrid.tsx +2 -2
- package/components/builder/asset-browser/R2BrowserContent.tsx +34 -17
- package/components/builder/asset-browser/helpers.ts +4 -0
- package/components/builder/asset-browser/types.ts +2 -1
- package/components/builder/blockStyles.tsx +192 -173
- package/components/builder/color-picker/AlphaSlider.tsx +141 -141
- package/components/builder/color-picker/ColorInputs.tsx +105 -105
- package/components/builder/color-picker/EyedropperButton.tsx +74 -74
- package/components/builder/color-picker/HueSlider.tsx +124 -124
- package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
- package/components/builder/color-picker/SwatchBar.tsx +93 -93
- package/components/builder/editors/AudioBlockEditor.tsx +242 -0
- package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -0
- package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
- package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
- package/components/builder/editors/HoverEffectPicker.tsx +2 -2
- package/components/builder/editors/ImageBlockEditor.tsx +2 -2
- package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
- package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
- package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
- package/components/builder/editors/ProjectGridEditor.tsx +9 -9
- package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
- package/components/builder/editors/StaggerSettings.tsx +109 -109
- package/components/builder/editors/TextBlockEditor.tsx +3 -3
- package/components/builder/editors/TextStylePicker.tsx +1 -1
- package/components/builder/editors/VideoBlockEditor.tsx +2 -2
- package/components/builder/editors/index.ts +11 -10
- package/components/builder/editors/shared.tsx +7 -7
- package/components/builder/live-preview/LiveAudioPreview.tsx +120 -0
- package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +176 -0
- package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
- package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
- package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
- package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
- package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
- package/components/builder/settings-panel/AnimationTab.tsx +138 -138
- package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
- package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
- package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
- package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
- package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
- package/components/builder/settings-panel/PageSettings.tsx +3 -3
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
- package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
- package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
- package/lib/animation/enter-types.ts +3 -0
- package/lib/animation/hover-effect-presets.ts +210 -210
- package/lib/animation/hover-effect-types.ts +3 -0
- package/lib/builder/block-registrations.ts +468 -335
- package/lib/builder/constants.ts +111 -111
- package/lib/builder/store-sections.ts +2 -2
- package/lib/builder/types-slices.ts +414 -414
- package/lib/builder/types.ts +6 -1
- package/lib/config/index.ts +27 -27
- package/lib/sanity/types.ts +156 -1
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/audioBlock.ts +69 -0
- package/sanity/schemas/blocks/beforeAfterBlock.ts +121 -0
- package/sanity/schemas/blocks/index.ts +12 -9
- package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
- package/sanity/schemas/index.ts +120 -111
- package/styles/admin.css +85 -85
- package/styles/animations.css +237 -237
- package/styles/base.css +114 -114
|
@@ -1,255 +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 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-[#
|
|
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
|
-
}
|
|
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-[#3580f9]"
|
|
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
|
+
}
|
|
@@ -23,7 +23,7 @@ import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup, isCoverSecti
|
|
|
23
23
|
import SectionEditorBar from "../../../../components/builder/SectionEditorBar";
|
|
24
24
|
import CustomSectionInstanceCard from "../../../../components/builder/CustomSectionInstanceCard";
|
|
25
25
|
import { ColumnDragProvider } from "../../../../components/builder/ColumnDragContext";
|
|
26
|
-
import type { BlockType } from "../../../../lib/builder/types";
|
|
26
|
+
import type { BlockType, SectionBlockType } from "../../../../lib/builder/types";
|
|
27
27
|
import { findGaps } from "../../../../lib/builder/cascade";
|
|
28
28
|
import { isSectionBlockType } from "../../../../lib/builder/types";
|
|
29
29
|
import BlockLivePreview from "../../../../components/builder/BlockLivePreview";
|
|
@@ -415,10 +415,10 @@ export default function PageEditorPage() {
|
|
|
415
415
|
[store]
|
|
416
416
|
);
|
|
417
417
|
|
|
418
|
-
// Handle add page section (project grid / project carousel)
|
|
418
|
+
// Handle add page section (project grid / project carousel / marquee)
|
|
419
419
|
// afterRowKey=null → always appends to end of page (bottom "Add Section" button)
|
|
420
420
|
const handleAddSection = useCallback(
|
|
421
|
-
(blockType:
|
|
421
|
+
(blockType: SectionBlockType) => {
|
|
422
422
|
store.addSection(blockType, null);
|
|
423
423
|
setShowSectionPicker(false);
|
|
424
424
|
},
|
|
@@ -610,7 +610,7 @@ export default function PageEditorPage() {
|
|
|
610
610
|
<button
|
|
611
611
|
onClick={handleSave}
|
|
612
612
|
disabled={store.isSaving || !store.isDirty}
|
|
613
|
-
className="rounded-lg bg-[#
|
|
613
|
+
className="rounded-lg bg-[#3580f9] px-5 py-1.5 text-sm font-medium text-white hover:bg-[#2d6dd4] transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
614
614
|
>
|
|
615
615
|
{store.isSaving ? "Saving..." : "Save"}
|
|
616
616
|
</button>
|
|
@@ -652,7 +652,7 @@ export default function PageEditorPage() {
|
|
|
652
652
|
e.stopPropagation();
|
|
653
653
|
setShowSectionPicker(true);
|
|
654
654
|
}}
|
|
655
|
-
className="rounded-lg bg-[#
|
|
655
|
+
className="rounded-lg bg-[#3580f9] px-4 py-2 text-xs text-white hover:bg-[#2d6dd4] transition-colors"
|
|
656
656
|
>
|
|
657
657
|
+ Add First Section
|
|
658
658
|
</button>
|
|
@@ -818,7 +818,7 @@ export default function PageEditorPage() {
|
|
|
818
818
|
padding: "5px 16px",
|
|
819
819
|
background: "#e0daff",
|
|
820
820
|
color: "#7500d5",
|
|
821
|
-
border: "
|
|
821
|
+
border: "1.5px dashed #7500d5",
|
|
822
822
|
}}
|
|
823
823
|
>
|
|
824
824
|
+ Add Section
|
package/app/admin/pages/page.tsx
CHANGED
|
@@ -123,7 +123,7 @@ function CreatePageModal({
|
|
|
123
123
|
value={title}
|
|
124
124
|
onChange={(e) => setTitle(e.target.value)}
|
|
125
125
|
placeholder="Page title"
|
|
126
|
-
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#
|
|
126
|
+
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#3580f9] focus:outline-none focus:ring-2 focus:ring-[#3580f9]/10"
|
|
127
127
|
autoFocus
|
|
128
128
|
/>
|
|
129
129
|
</div>
|
|
@@ -136,7 +136,7 @@ function CreatePageModal({
|
|
|
136
136
|
value={slug}
|
|
137
137
|
onChange={(e) => { setSlug(e.target.value); setAutoSlug(false); }}
|
|
138
138
|
placeholder="page-slug"
|
|
139
|
-
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#
|
|
139
|
+
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#3580f9] focus:outline-none focus:ring-2 focus:ring-[#3580f9]/10"
|
|
140
140
|
/>
|
|
141
141
|
<p className="text-xs text-neutral-400 mt-1">URL: /{slug || "page-slug"}</p>
|
|
142
142
|
</div>
|
|
@@ -152,7 +152,7 @@ function CreatePageModal({
|
|
|
152
152
|
<button
|
|
153
153
|
type="submit"
|
|
154
154
|
disabled={creating}
|
|
155
|
-
className="rounded-lg bg-[#
|
|
155
|
+
className="rounded-lg bg-[#3580f9] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#2d6dd4] transition-colors disabled:opacity-50"
|
|
156
156
|
>
|
|
157
157
|
{creating ? "Creating..." : "Create"}
|
|
158
158
|
</button>
|
|
@@ -297,7 +297,7 @@ function EditPageModal({
|
|
|
297
297
|
type="text"
|
|
298
298
|
value={title}
|
|
299
299
|
onChange={(e) => setTitle(e.target.value)}
|
|
300
|
-
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#
|
|
300
|
+
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#3580f9] focus:outline-none focus:ring-2 focus:ring-[#3580f9]/10"
|
|
301
301
|
autoFocus
|
|
302
302
|
/>
|
|
303
303
|
</div>
|
|
@@ -307,14 +307,14 @@ function EditPageModal({
|
|
|
307
307
|
type="text"
|
|
308
308
|
value={slug}
|
|
309
309
|
onChange={(e) => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""))}
|
|
310
|
-
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#
|
|
310
|
+
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#3580f9] focus:outline-none focus:ring-2 focus:ring-[#3580f9]/10"
|
|
311
311
|
/>
|
|
312
312
|
<p className="text-xs text-neutral-400 mt-1">URL: /{slug || "page-slug"}</p>
|
|
313
313
|
</div>
|
|
314
314
|
{error && <p className="text-xs text-[var(--admin-error)]">{error}</p>}
|
|
315
315
|
<div className="flex gap-3 justify-end pt-2">
|
|
316
316
|
<button type="button" onClick={onClose} className="rounded-lg border border-neutral-200 px-5 py-2.5 text-sm text-neutral-500 hover:text-neutral-800 hover:border-neutral-300 transition-colors">Cancel</button>
|
|
317
|
-
<button type="submit" disabled={saving} className="rounded-lg bg-[#
|
|
317
|
+
<button type="submit" disabled={saving} className="rounded-lg bg-[#3580f9] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#2d6dd4] transition-colors disabled:opacity-50">{saving ? "Saving..." : "Save"}</button>
|
|
318
318
|
</div>
|
|
319
319
|
</form>
|
|
320
320
|
</div>
|
|
@@ -381,7 +381,7 @@ function HomeConfirmModal({
|
|
|
381
381
|
{error && <p className="text-xs text-[var(--admin-error)] mb-4">{error}</p>}
|
|
382
382
|
<div className="flex gap-3 justify-end">
|
|
383
383
|
<button onClick={onClose} className="rounded-lg border border-neutral-200 px-5 py-2.5 text-sm text-neutral-500 hover:text-neutral-800 hover:border-neutral-300 transition-colors">Cancel</button>
|
|
384
|
-
<button onClick={handleConfirm} disabled={setting} className="rounded-lg bg-[#
|
|
384
|
+
<button onClick={handleConfirm} disabled={setting} className="rounded-lg bg-[#3580f9] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#2d6dd4] transition-colors disabled:opacity-50">{setting ? "Setting..." : "Set as Home"}</button>
|
|
385
385
|
</div>
|
|
386
386
|
</div>
|
|
387
387
|
</div>
|
|
@@ -456,7 +456,7 @@ export default function AdminPagesPage() {
|
|
|
456
456
|
</h1>
|
|
457
457
|
<button
|
|
458
458
|
onClick={() => setShowCreate(true)}
|
|
459
|
-
className="rounded-lg bg-[#
|
|
459
|
+
className="rounded-lg bg-[#3580f9] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#2d6dd4] transition-colors"
|
|
460
460
|
>
|
|
461
461
|
+ New Page
|
|
462
462
|
</button>
|
|
@@ -469,7 +469,7 @@ export default function AdminPagesPage() {
|
|
|
469
469
|
value={search}
|
|
470
470
|
onChange={(e) => setSearch(e.target.value)}
|
|
471
471
|
placeholder="Search by title or slug..."
|
|
472
|
-
className="w-full max-w-md rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#
|
|
472
|
+
className="w-full max-w-md rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#3580f9] focus:outline-none focus:ring-2 focus:ring-[#3580f9]/10 shadow-sm"
|
|
473
473
|
/>
|
|
474
474
|
</div>
|
|
475
475
|
|
|
@@ -484,7 +484,7 @@ export default function AdminPagesPage() {
|
|
|
484
484
|
{pages.length === 0 ? "No pages yet" : "No pages match your search"}
|
|
485
485
|
</p>
|
|
486
486
|
{pages.length === 0 && (
|
|
487
|
-
<button onClick={() => setShowCreate(true)} className="text-xs text-[#
|
|
487
|
+
<button onClick={() => setShowCreate(true)} className="text-xs text-[#3580f9] hover:underline">
|
|
488
488
|
Create your first page
|
|
489
489
|
</button>
|
|
490
490
|
)}
|
|
@@ -507,7 +507,7 @@ export default function AdminPagesPage() {
|
|
|
507
507
|
>
|
|
508
508
|
{/* Title */}
|
|
509
509
|
<div className="cursor-pointer" onClick={() => router.push(`/admin/pages/${page.slug.current}`)}>
|
|
510
|
-
<p className="text-sm text-neutral-900 group-hover:text-[#
|
|
510
|
+
<p className="text-sm text-neutral-900 group-hover:text-[#3580f9] transition-colors">
|
|
511
511
|
{page.title}
|
|
512
512
|
</p>
|
|
513
513
|
<p className="text-xs text-neutral-400">/{page.slug.current}</p>
|