@morphika/webframe 0.1.0
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 +46 -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 +210 -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,588 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
import { csrfHeaders } from "../../../lib/csrf-client";
|
|
7
|
+
import type { PageListItem } from "../../../lib/sanity/types";
|
|
8
|
+
import PublishToggle from "../../../components/admin/PublishToggle";
|
|
9
|
+
import { EditIcon, DuplicateIcon, DeleteIcon, PreviewIcon } from "../../../components/admin/icons";
|
|
10
|
+
|
|
11
|
+
function HomeIcon({ active }: { active: boolean }) {
|
|
12
|
+
return (
|
|
13
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill={active ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.5">
|
|
14
|
+
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
|
15
|
+
<polyline points="9 22 9 12 15 12 15 22" />
|
|
16
|
+
</svg>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ============================================
|
|
21
|
+
// Create Page Modal
|
|
22
|
+
// ============================================
|
|
23
|
+
|
|
24
|
+
function CreatePageModal({
|
|
25
|
+
open,
|
|
26
|
+
onClose,
|
|
27
|
+
onCreated,
|
|
28
|
+
}: {
|
|
29
|
+
open: boolean;
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
onCreated: () => void;
|
|
32
|
+
}) {
|
|
33
|
+
const [title, setTitle] = useState("");
|
|
34
|
+
const [slug, setSlug] = useState("");
|
|
35
|
+
const [error, setError] = useState("");
|
|
36
|
+
const [creating, setCreating] = useState(false);
|
|
37
|
+
const [autoSlug, setAutoSlug] = useState(true);
|
|
38
|
+
|
|
39
|
+
// Reset state when modal opens/closes
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (open) {
|
|
42
|
+
setTitle("");
|
|
43
|
+
setSlug("");
|
|
44
|
+
setError("");
|
|
45
|
+
setAutoSlug(true);
|
|
46
|
+
}
|
|
47
|
+
}, [open]);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!open) return;
|
|
51
|
+
const handleKey = (e: KeyboardEvent) => {
|
|
52
|
+
if (e.key === "Escape") onClose();
|
|
53
|
+
};
|
|
54
|
+
window.addEventListener("keydown", handleKey);
|
|
55
|
+
return () => window.removeEventListener("keydown", handleKey);
|
|
56
|
+
}, [open, onClose]);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (autoSlug && title) {
|
|
60
|
+
setSlug(
|
|
61
|
+
title
|
|
62
|
+
.toLowerCase()
|
|
63
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
64
|
+
.replace(/^-|-$/g, "")
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}, [title, autoSlug]);
|
|
68
|
+
|
|
69
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
if (!title || !slug) {
|
|
72
|
+
setError("Title and slug are required");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
setCreating(true);
|
|
76
|
+
setError("");
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch("/api/admin/pages", {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: { "Content-Type": "application/json", ...csrfHeaders() },
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
title,
|
|
83
|
+
slug,
|
|
84
|
+
page_type: "page",
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
const data = await res.json();
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
setError(data.error || "Failed to create page");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
setTitle("");
|
|
93
|
+
setSlug("");
|
|
94
|
+
setAutoSlug(true);
|
|
95
|
+
onCreated();
|
|
96
|
+
onClose();
|
|
97
|
+
} catch {
|
|
98
|
+
setError("Network error");
|
|
99
|
+
} finally {
|
|
100
|
+
setCreating(false);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (!open) return null;
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-6">
|
|
108
|
+
<div className="rounded-2xl border border-neutral-200/80 bg-white shadow-2xl w-full max-w-md">
|
|
109
|
+
<div className="p-6">
|
|
110
|
+
<h2 className="text-sm font-medium uppercase tracking-wider text-neutral-900 mb-1">
|
|
111
|
+
New Page
|
|
112
|
+
</h2>
|
|
113
|
+
<p className="text-xs text-neutral-400 mb-5">
|
|
114
|
+
Add sections and blocks after creating the page
|
|
115
|
+
</p>
|
|
116
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
117
|
+
<div>
|
|
118
|
+
<label className="block text-xs font-medium uppercase tracking-wider text-neutral-500 mb-1">
|
|
119
|
+
Title
|
|
120
|
+
</label>
|
|
121
|
+
<input
|
|
122
|
+
type="text"
|
|
123
|
+
value={title}
|
|
124
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
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-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10"
|
|
127
|
+
autoFocus
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
<div>
|
|
131
|
+
<label className="block text-xs font-medium uppercase tracking-wider text-neutral-500 mb-1">
|
|
132
|
+
Slug
|
|
133
|
+
</label>
|
|
134
|
+
<input
|
|
135
|
+
type="text"
|
|
136
|
+
value={slug}
|
|
137
|
+
onChange={(e) => { setSlug(e.target.value); setAutoSlug(false); }}
|
|
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-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10"
|
|
140
|
+
/>
|
|
141
|
+
<p className="text-xs text-neutral-400 mt-1">URL: /{slug || "page-slug"}</p>
|
|
142
|
+
</div>
|
|
143
|
+
{error && <p className="text-xs text-[var(--admin-error)]">{error}</p>}
|
|
144
|
+
<div className="flex gap-3 justify-end pt-2">
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onClick={onClose}
|
|
148
|
+
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"
|
|
149
|
+
>
|
|
150
|
+
Cancel
|
|
151
|
+
</button>
|
|
152
|
+
<button
|
|
153
|
+
type="submit"
|
|
154
|
+
disabled={creating}
|
|
155
|
+
className="rounded-lg bg-[#076bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors disabled:opacity-50"
|
|
156
|
+
>
|
|
157
|
+
{creating ? "Creating..." : "Create"}
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
</form>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============================================
|
|
168
|
+
// Delete Confirm Modal
|
|
169
|
+
// ============================================
|
|
170
|
+
|
|
171
|
+
function DeleteConfirmModal({
|
|
172
|
+
page,
|
|
173
|
+
onClose,
|
|
174
|
+
onDeleted,
|
|
175
|
+
}: {
|
|
176
|
+
page: PageListItem | null;
|
|
177
|
+
onClose: () => void;
|
|
178
|
+
onDeleted: () => void;
|
|
179
|
+
}) {
|
|
180
|
+
const [deleting, setDeleting] = useState(false);
|
|
181
|
+
const [error, setError] = useState("");
|
|
182
|
+
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (!page) return;
|
|
185
|
+
const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
|
186
|
+
window.addEventListener("keydown", handleKey);
|
|
187
|
+
return () => window.removeEventListener("keydown", handleKey);
|
|
188
|
+
}, [page, onClose]);
|
|
189
|
+
|
|
190
|
+
useEffect(() => { setError(""); }, [page]);
|
|
191
|
+
|
|
192
|
+
if (!page) return null;
|
|
193
|
+
|
|
194
|
+
const handleDelete = async () => {
|
|
195
|
+
setDeleting(true);
|
|
196
|
+
setError("");
|
|
197
|
+
try {
|
|
198
|
+
const res = await fetch(`/api/admin/pages/${page.slug.current}`, { method: "DELETE", headers: { ...csrfHeaders() } });
|
|
199
|
+
if (res.ok) { onDeleted(); onClose(); }
|
|
200
|
+
else {
|
|
201
|
+
const data = await res.json().catch(() => ({ error: "Delete failed" }));
|
|
202
|
+
setError(data.error || `Delete failed (${res.status})`);
|
|
203
|
+
}
|
|
204
|
+
} catch { setError("Network error"); }
|
|
205
|
+
finally { setDeleting(false); }
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
|
210
|
+
<div className="w-full max-w-sm rounded-2xl border border-neutral-200/80 bg-white p-6 shadow-2xl">
|
|
211
|
+
<h2 className="text-sm font-medium uppercase tracking-wider text-neutral-900 mb-3">Delete Page</h2>
|
|
212
|
+
<p className="text-xs text-neutral-500 mb-6">
|
|
213
|
+
Are you sure you want to delete “{page.title}”? This action cannot be undone.
|
|
214
|
+
</p>
|
|
215
|
+
{error && <p className="text-xs text-[var(--admin-error)] mb-4">{error}</p>}
|
|
216
|
+
<div className="flex gap-3 justify-end">
|
|
217
|
+
<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>
|
|
218
|
+
<button onClick={handleDelete} disabled={deleting} className="rounded-lg bg-[var(--admin-error)] px-5 py-2.5 text-sm font-medium text-white hover:bg-[var(--admin-error-dark)] transition-colors disabled:opacity-50">{deleting ? "Deleting..." : "Delete"}</button>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ============================================
|
|
226
|
+
// Edit Page Settings Modal
|
|
227
|
+
// ============================================
|
|
228
|
+
|
|
229
|
+
function EditPageModal({
|
|
230
|
+
page,
|
|
231
|
+
onClose,
|
|
232
|
+
onUpdated,
|
|
233
|
+
}: {
|
|
234
|
+
page: PageListItem | null;
|
|
235
|
+
onClose: () => void;
|
|
236
|
+
onUpdated: () => void;
|
|
237
|
+
}) {
|
|
238
|
+
const [title, setTitle] = useState("");
|
|
239
|
+
const [slug, setSlug] = useState("");
|
|
240
|
+
const [error, setError] = useState("");
|
|
241
|
+
const [saving, setSaving] = useState(false);
|
|
242
|
+
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
if (page) {
|
|
245
|
+
setTitle(page.title);
|
|
246
|
+
setSlug(page.slug.current);
|
|
247
|
+
setError("");
|
|
248
|
+
}
|
|
249
|
+
}, [page]);
|
|
250
|
+
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
if (!page) return;
|
|
253
|
+
const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
|
254
|
+
window.addEventListener("keydown", handleKey);
|
|
255
|
+
return () => window.removeEventListener("keydown", handleKey);
|
|
256
|
+
}, [page, onClose]);
|
|
257
|
+
|
|
258
|
+
if (!page) return null;
|
|
259
|
+
|
|
260
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
261
|
+
e.preventDefault();
|
|
262
|
+
if (!title.trim()) { setError("Title is required"); return; }
|
|
263
|
+
if (!slug.trim()) { setError("Slug is required"); return; }
|
|
264
|
+
|
|
265
|
+
setSaving(true);
|
|
266
|
+
setError("");
|
|
267
|
+
try {
|
|
268
|
+
const res = await fetch(`/api/admin/pages/${page.slug.current}`, {
|
|
269
|
+
method: "PATCH",
|
|
270
|
+
headers: { "Content-Type": "application/json", ...csrfHeaders() },
|
|
271
|
+
body: JSON.stringify({ title: title.trim(), newSlug: slug.trim() }),
|
|
272
|
+
});
|
|
273
|
+
const data = await res.json();
|
|
274
|
+
if (!res.ok) {
|
|
275
|
+
setError(data.error || "Failed to update");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
onUpdated();
|
|
279
|
+
onClose();
|
|
280
|
+
} catch {
|
|
281
|
+
setError("Network error");
|
|
282
|
+
} finally {
|
|
283
|
+
setSaving(false);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
|
289
|
+
<div className="w-full max-w-md rounded-2xl border border-neutral-200/80 bg-white p-6 shadow-2xl">
|
|
290
|
+
<h2 className="text-sm font-medium uppercase tracking-wider text-neutral-900 mb-4">
|
|
291
|
+
Page Settings
|
|
292
|
+
</h2>
|
|
293
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
294
|
+
<div>
|
|
295
|
+
<label className="block text-xs font-medium uppercase tracking-wider text-neutral-500 mb-1">Title</label>
|
|
296
|
+
<input
|
|
297
|
+
type="text"
|
|
298
|
+
value={title}
|
|
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-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10"
|
|
301
|
+
autoFocus
|
|
302
|
+
/>
|
|
303
|
+
</div>
|
|
304
|
+
<div>
|
|
305
|
+
<label className="block text-xs font-medium uppercase tracking-wider text-neutral-500 mb-1">Slug</label>
|
|
306
|
+
<input
|
|
307
|
+
type="text"
|
|
308
|
+
value={slug}
|
|
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-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10"
|
|
311
|
+
/>
|
|
312
|
+
<p className="text-xs text-neutral-400 mt-1">URL: /{slug || "page-slug"}</p>
|
|
313
|
+
</div>
|
|
314
|
+
{error && <p className="text-xs text-[var(--admin-error)]">{error}</p>}
|
|
315
|
+
<div className="flex gap-3 justify-end pt-2">
|
|
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-[#076bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors disabled:opacity-50">{saving ? "Saving..." : "Save"}</button>
|
|
318
|
+
</div>
|
|
319
|
+
</form>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ============================================
|
|
326
|
+
// Home Confirm Modal
|
|
327
|
+
// ============================================
|
|
328
|
+
|
|
329
|
+
function HomeConfirmModal({
|
|
330
|
+
page,
|
|
331
|
+
onClose,
|
|
332
|
+
onConfirmed,
|
|
333
|
+
}: {
|
|
334
|
+
page: PageListItem | null;
|
|
335
|
+
onClose: () => void;
|
|
336
|
+
onConfirmed: () => void;
|
|
337
|
+
}) {
|
|
338
|
+
const [setting, setSetting] = useState(false);
|
|
339
|
+
const [error, setError] = useState("");
|
|
340
|
+
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
if (!page) return;
|
|
343
|
+
const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
|
344
|
+
window.addEventListener("keydown", handleKey);
|
|
345
|
+
return () => window.removeEventListener("keydown", handleKey);
|
|
346
|
+
}, [page, onClose]);
|
|
347
|
+
|
|
348
|
+
useEffect(() => { setError(""); }, [page]);
|
|
349
|
+
|
|
350
|
+
if (!page) return null;
|
|
351
|
+
|
|
352
|
+
const handleConfirm = async () => {
|
|
353
|
+
setSetting(true);
|
|
354
|
+
setError("");
|
|
355
|
+
try {
|
|
356
|
+
const res = await fetch(`/api/admin/pages/${page.slug.current}/set-home`, {
|
|
357
|
+
method: "POST",
|
|
358
|
+
headers: { ...csrfHeaders() },
|
|
359
|
+
});
|
|
360
|
+
if (res.ok) {
|
|
361
|
+
onConfirmed();
|
|
362
|
+
onClose();
|
|
363
|
+
} else {
|
|
364
|
+
const data = await res.json().catch(() => ({ error: "Failed" }));
|
|
365
|
+
setError(data.error || `Failed (${res.status})`);
|
|
366
|
+
}
|
|
367
|
+
} catch {
|
|
368
|
+
setError("Network error");
|
|
369
|
+
} finally {
|
|
370
|
+
setSetting(false);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
return (
|
|
375
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
|
376
|
+
<div className="w-full max-w-sm rounded-2xl border border-neutral-200/80 bg-white p-6 shadow-2xl">
|
|
377
|
+
<h2 className="text-sm font-medium uppercase tracking-wider text-neutral-900 mb-3">Set as Home Page</h2>
|
|
378
|
+
<p className="text-xs text-neutral-500 mb-6">
|
|
379
|
+
Do you want to set “{page.title}” as the index home page? The current home page will be unset.
|
|
380
|
+
</p>
|
|
381
|
+
{error && <p className="text-xs text-[var(--admin-error)] mb-4">{error}</p>}
|
|
382
|
+
<div className="flex gap-3 justify-end">
|
|
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-[#076bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors disabled:opacity-50">{setting ? "Setting..." : "Set as Home"}</button>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ============================================
|
|
392
|
+
// Pages List Page (excludes projects)
|
|
393
|
+
// ============================================
|
|
394
|
+
|
|
395
|
+
export default function AdminPagesPage() {
|
|
396
|
+
const router = useRouter();
|
|
397
|
+
const [pages, setPages] = useState<PageListItem[]>([]);
|
|
398
|
+
const [loading, setLoading] = useState(true);
|
|
399
|
+
const [search, setSearch] = useState("");
|
|
400
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
401
|
+
const [deletingPage, setDeletingPage] = useState<PageListItem | null>(null);
|
|
402
|
+
const [editingPage, setEditingPage] = useState<PageListItem | null>(null);
|
|
403
|
+
const [homeConfirmPage, setHomeConfirmPage] = useState<PageListItem | null>(null);
|
|
404
|
+
const [duplicating, setDuplicating] = useState<string | null>(null);
|
|
405
|
+
const fetchPages = useCallback(async () => {
|
|
406
|
+
try {
|
|
407
|
+
const res = await fetch("/api/admin/pages");
|
|
408
|
+
const data = await res.json();
|
|
409
|
+
// Exclude projects — they have their own section
|
|
410
|
+
setPages((data.pages || []).filter((p: PageListItem) => p.page_type !== "project"));
|
|
411
|
+
} catch {
|
|
412
|
+
setPages([]);
|
|
413
|
+
} finally {
|
|
414
|
+
setLoading(false);
|
|
415
|
+
}
|
|
416
|
+
}, []);
|
|
417
|
+
|
|
418
|
+
useEffect(() => { fetchPages(); }, [fetchPages]);
|
|
419
|
+
|
|
420
|
+
const filtered = pages.filter((p) => {
|
|
421
|
+
if (!search) return true;
|
|
422
|
+
const q = search.toLowerCase();
|
|
423
|
+
return p.title.toLowerCase().includes(q) || p.slug.current.toLowerCase().includes(q);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
const handleDuplicate = async (page: PageListItem) => {
|
|
429
|
+
setDuplicating(page._id);
|
|
430
|
+
try {
|
|
431
|
+
const res = await fetch(`/api/admin/pages/${page.slug.current}/duplicate`, { method: "POST", headers: { ...csrfHeaders() } });
|
|
432
|
+
if (res.ok) {
|
|
433
|
+
await fetchPages();
|
|
434
|
+
} else {
|
|
435
|
+
const data = await res.json().catch(() => ({ error: "Duplicate failed" }));
|
|
436
|
+
console.error("Duplicate failed:", data.error || `HTTP ${res.status}`);
|
|
437
|
+
}
|
|
438
|
+
} catch (err) {
|
|
439
|
+
console.error("Duplicate operation failed:", err);
|
|
440
|
+
} finally {
|
|
441
|
+
setDuplicating(null);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const handleHomeClick = (page: PageListItem) => {
|
|
446
|
+
if (page.is_home) return; // Already home, do nothing
|
|
447
|
+
setHomeConfirmPage(page);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<div>
|
|
452
|
+
{/* Header */}
|
|
453
|
+
<div className="flex items-center justify-between mb-6">
|
|
454
|
+
<h1 className="text-2xl font-semibold text-neutral-900">
|
|
455
|
+
Pages
|
|
456
|
+
</h1>
|
|
457
|
+
<button
|
|
458
|
+
onClick={() => setShowCreate(true)}
|
|
459
|
+
className="rounded-lg bg-[#076bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors"
|
|
460
|
+
>
|
|
461
|
+
+ New Page
|
|
462
|
+
</button>
|
|
463
|
+
</div>
|
|
464
|
+
|
|
465
|
+
{/* Search */}
|
|
466
|
+
<div className="mb-6">
|
|
467
|
+
<input
|
|
468
|
+
type="text"
|
|
469
|
+
value={search}
|
|
470
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
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-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10 shadow-sm"
|
|
473
|
+
/>
|
|
474
|
+
</div>
|
|
475
|
+
|
|
476
|
+
{/* Page List */}
|
|
477
|
+
{loading ? (
|
|
478
|
+
<div className="flex items-center justify-center py-20">
|
|
479
|
+
<p className="text-xs text-neutral-400 animate-pulse">Loading pages...</p>
|
|
480
|
+
</div>
|
|
481
|
+
) : filtered.length === 0 ? (
|
|
482
|
+
<div className="flex flex-col items-center justify-center py-20 border border-dashed border-neutral-300 rounded">
|
|
483
|
+
<p className="text-sm text-neutral-500 mb-2">
|
|
484
|
+
{pages.length === 0 ? "No pages yet" : "No pages match your search"}
|
|
485
|
+
</p>
|
|
486
|
+
{pages.length === 0 && (
|
|
487
|
+
<button onClick={() => setShowCreate(true)} className="text-xs text-[#076bff] hover:underline">
|
|
488
|
+
Create your first page
|
|
489
|
+
</button>
|
|
490
|
+
)}
|
|
491
|
+
</div>
|
|
492
|
+
) : (
|
|
493
|
+
<div className="space-y-1">
|
|
494
|
+
{/* Table header */}
|
|
495
|
+
<div className="hidden md:grid grid-cols-[1fr_60px_160px_120px_160px] gap-4 px-4 py-2 border-b border-neutral-200">
|
|
496
|
+
<span className="text-xs font-medium uppercase tracking-wider text-neutral-400">Title</span>
|
|
497
|
+
<span className="text-xs font-medium uppercase tracking-wider text-neutral-400">Home</span>
|
|
498
|
+
<span className="text-xs font-medium uppercase tracking-wider text-neutral-400">Status</span>
|
|
499
|
+
<span className="text-xs font-medium uppercase tracking-wider text-neutral-400">Created</span>
|
|
500
|
+
<span className="text-xs font-medium uppercase tracking-wider text-neutral-400 text-right">Actions</span>
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
{filtered.map((page) => (
|
|
504
|
+
<div
|
|
505
|
+
key={page._id}
|
|
506
|
+
className="group flex flex-col gap-2 rounded-xl bg-white px-4 py-3 hover:bg-neutral-50/50 transition-colors border border-transparent hover:border-neutral-200 md:grid md:grid-cols-[1fr_60px_160px_120px_160px] md:gap-4 md:items-center"
|
|
507
|
+
>
|
|
508
|
+
{/* Title */}
|
|
509
|
+
<div className="cursor-pointer" onClick={() => router.push(`/admin/pages/${page.slug.current}`)}>
|
|
510
|
+
<p className="text-sm text-neutral-900 group-hover:text-[#076bff] transition-colors">
|
|
511
|
+
{page.title}
|
|
512
|
+
</p>
|
|
513
|
+
<p className="text-xs text-neutral-400">/{page.slug.current}</p>
|
|
514
|
+
</div>
|
|
515
|
+
|
|
516
|
+
{/* Home toggle */}
|
|
517
|
+
<button
|
|
518
|
+
onClick={(e) => { e.stopPropagation(); handleHomeClick(page); }}
|
|
519
|
+
className={`p-1.5 rounded w-fit transition-colors ${
|
|
520
|
+
page.is_home
|
|
521
|
+
? "text-amber-500 cursor-default"
|
|
522
|
+
: "text-neutral-300 hover:text-amber-400 cursor-pointer"
|
|
523
|
+
}`}
|
|
524
|
+
title={page.is_home ? "Home page" : "Set as home page"}
|
|
525
|
+
>
|
|
526
|
+
<HomeIcon active={!!page.is_home} />
|
|
527
|
+
</button>
|
|
528
|
+
|
|
529
|
+
{/* Status */}
|
|
530
|
+
<div className="flex items-center">
|
|
531
|
+
<PublishToggle
|
|
532
|
+
mode="api"
|
|
533
|
+
isDraft={!!page.draft_mode}
|
|
534
|
+
slug={page.slug.current}
|
|
535
|
+
onToggled={fetchPages}
|
|
536
|
+
/>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
{/* Date */}
|
|
540
|
+
<span className="text-xs text-neutral-500">
|
|
541
|
+
{page.published_at ? new Date(page.published_at).toLocaleDateString() : "\u2014"}
|
|
542
|
+
</span>
|
|
543
|
+
|
|
544
|
+
{/* Actions */}
|
|
545
|
+
<div className="flex items-center gap-1 justify-end">
|
|
546
|
+
<button
|
|
547
|
+
onClick={(e) => { e.stopPropagation(); setEditingPage(page); }}
|
|
548
|
+
className="p-1.5 rounded text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100 transition-colors"
|
|
549
|
+
title="Edit settings"
|
|
550
|
+
>
|
|
551
|
+
<EditIcon />
|
|
552
|
+
</button>
|
|
553
|
+
<button
|
|
554
|
+
onClick={(e) => { e.stopPropagation(); handleDuplicate(page); }}
|
|
555
|
+
disabled={duplicating === page._id}
|
|
556
|
+
className="p-1.5 rounded text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100 transition-colors disabled:opacity-30"
|
|
557
|
+
title="Duplicate"
|
|
558
|
+
>
|
|
559
|
+
<DuplicateIcon />
|
|
560
|
+
</button>
|
|
561
|
+
<button
|
|
562
|
+
onClick={(e) => { e.stopPropagation(); setDeletingPage(page); }}
|
|
563
|
+
className="p-1.5 rounded text-neutral-400 hover:text-red-500 hover:bg-red-50 transition-colors"
|
|
564
|
+
title="Delete"
|
|
565
|
+
>
|
|
566
|
+
<DeleteIcon />
|
|
567
|
+
</button>
|
|
568
|
+
<Link
|
|
569
|
+
href={page.is_home ? "/" : `/${page.slug.current}`}
|
|
570
|
+
target="_blank"
|
|
571
|
+
className="p-1.5 rounded text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100 transition-colors"
|
|
572
|
+
title="Preview"
|
|
573
|
+
>
|
|
574
|
+
<PreviewIcon />
|
|
575
|
+
</Link>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
))}
|
|
579
|
+
</div>
|
|
580
|
+
)}
|
|
581
|
+
|
|
582
|
+
<CreatePageModal open={showCreate} onClose={() => setShowCreate(false)} onCreated={fetchPages} />
|
|
583
|
+
<EditPageModal page={editingPage} onClose={() => setEditingPage(null)} onUpdated={fetchPages} />
|
|
584
|
+
<DeleteConfirmModal page={deletingPage} onClose={() => setDeletingPage(null)} onDeleted={fetchPages} />
|
|
585
|
+
<HomeConfirmModal page={homeConfirmPage} onClose={() => setHomeConfirmPage(null)} onConfirmed={fetchPages} />
|
|
586
|
+
</div>
|
|
587
|
+
);
|
|
588
|
+
}
|