@morphika/andami 0.5.1 → 0.5.3
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 +27 -2
- 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 +332 -320
- package/app/admin/navigation/page.tsx +255 -255
- package/app/admin/pages/[slug]/page.tsx +44 -27
- package/app/admin/pages/page.tsx +24 -19
- package/app/admin/projects/page.tsx +30 -21
- package/app/admin/setup/page.tsx +1 -1
- package/app/admin/styles/page.tsx +1 -1
- package/app/api/admin/assets/register/route.ts +51 -14
- package/app/api/admin/assets/registry/route.ts +4 -1
- package/app/api/admin/assets/relink/confirm/route.ts +4 -1
- package/app/api/admin/assets/relink/route.ts +4 -1
- package/app/api/admin/assets/scan/route.ts +4 -1
- package/app/api/admin/backups/restore-data/route.ts +4 -1
- package/app/api/admin/r2/connect/route.ts +4 -1
- package/app/api/admin/r2/delete/route.ts +4 -1
- package/app/api/admin/r2/rename/route.ts +4 -1
- package/app/api/admin/r2/upload-url/route.ts +4 -1
- package/app/api/admin/revalidate/route.ts +4 -1
- package/app/api/admin/storage/switch/route.ts +4 -1
- package/app/api/custom-sections/[id]/route.ts +5 -6
- package/components/admin/MetadataEditor.tsx +6 -6
- package/components/admin/PublishToggle.tsx +2 -2
- 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 +8 -6
- 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 +518 -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 +9 -8
- package/components/admin/styles/FontsEditor.tsx +9 -7
- 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 -286
- package/components/blocks/CoverSectionRenderer.tsx +7 -1
- package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
- package/components/blocks/SectionV2Renderer.tsx +8 -1
- package/components/builder/BlockCardIcons.tsx +316 -316
- package/components/builder/BlockTypePicker.tsx +1 -1
- package/components/builder/BubbleIcons.tsx +104 -0
- package/components/builder/BuilderCanvas.tsx +2 -0
- package/components/builder/CanvasMinimap.tsx +66 -49
- package/components/builder/CanvasToolbar.tsx +31 -41
- package/components/builder/CoverSectionCanvas.tsx +363 -363
- 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 +5 -3
- package/components/builder/SectionTypePicker.tsx +7 -5
- package/components/builder/SectionV2Canvas.tsx +1 -1
- package/components/builder/SectionV2Column.tsx +82 -68
- package/components/builder/SettingsPanel.tsx +21 -17
- package/components/builder/SortableBlock.tsx +93 -73
- package/components/builder/SortableRow.tsx +33 -35
- package/components/builder/VirtualAssetGrid.tsx +10 -4
- package/components/builder/asset-browser/R2BrowserContent.tsx +18 -14
- package/components/builder/blockStyles.tsx +192 -185
- 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 +75 -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 +98 -93
- package/components/builder/color-picker/UnifiedColorPicker.tsx +11 -6
- package/components/builder/editors/AudioBlockEditor.tsx +242 -242
- package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
- 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 +8 -6
- package/components/builder/editors/MarqueeBlockEditor.tsx +622 -0
- package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
- package/components/builder/editors/ProjectGridEditor.tsx +21 -16
- package/components/builder/editors/SpacerBlockEditor.tsx +29 -27
- package/components/builder/editors/StaggerSettings.tsx +109 -109
- package/components/builder/editors/TextBlockEditor.tsx +22 -17
- 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 +10 -8
- package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
- package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
- package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
- package/components/builder/live-preview/LiveImagePreview.tsx +4 -2
- 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 +293 -291
- package/components/builder/live-preview/RichTextBubbleMenu.tsx +10 -6
- package/components/builder/live-preview/shared.tsx +5 -2
- package/components/builder/settings-panel/AnimationTab.tsx +138 -138
- package/components/builder/settings-panel/BlockLayoutTab.tsx +11 -9
- package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
- package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +242 -0
- 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 +337 -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 +25 -20
- package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
- package/components/builder/settings-panel/index.ts +1 -0
- package/lib/animation/enter-types.ts +1 -0
- package/lib/animation/hover-effect-presets.ts +210 -210
- package/lib/animation/hover-effect-types.ts +1 -0
- package/lib/builder/block-registrations.ts +468 -417
- package/lib/builder/constants.ts +111 -111
- package/lib/builder/serializer/normalizers.ts +14 -0
- package/lib/builder/serializer/serializers.ts +27 -0
- package/lib/builder/store-sections.ts +23 -2
- package/lib/builder/types-slices.ts +428 -414
- package/lib/builder/types.ts +4 -1
- package/lib/config/index.ts +27 -27
- package/lib/sanity/queries.ts +48 -0
- package/lib/sanity/types.ts +112 -1
- package/lib/version.ts +1 -1
- package/package.json +7 -5
- package/sanity/schemas/blocks/audioBlock.ts +69 -69
- package/sanity/schemas/blocks/index.ts +12 -11
- package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
- package/sanity/schemas/index.ts +120 -117
- package/sanity/schemas/objects/coverSection.ts +32 -0
- package/sanity/schemas/objects/parallaxSlide.ts +32 -0
- package/sanity/schemas/pageSectionV2.ts +32 -0
- package/styles/admin.css +85 -85
- package/styles/animations.css +237 -237
- package/styles/base.css +114 -114
|
@@ -3,7 +3,7 @@ import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
|
3
3
|
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
4
4
|
import { assetRegistryQuery } from "../../../../../lib/sanity/queries";
|
|
5
5
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
6
|
-
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
6
|
+
import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
|
|
7
7
|
import { isBodyTooLarge, MAX_JSON_BODY_SIZE } from "../../../../../lib/security";
|
|
8
8
|
import { logger } from "../../../../../lib/logger";
|
|
9
9
|
|
|
@@ -50,6 +50,9 @@ export async function POST(request: NextRequest) {
|
|
|
50
50
|
if (!validateCsrf(request)) {
|
|
51
51
|
return csrfErrorResponse();
|
|
52
52
|
}
|
|
53
|
+
if (!hasJsonContentType(request)) {
|
|
54
|
+
return contentTypeErrorResponse();
|
|
55
|
+
}
|
|
53
56
|
if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
|
|
54
57
|
return NextResponse.json({ error: "Request body too large" }, { status: 413 });
|
|
55
58
|
}
|
|
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import { adminClient as client } from "../../../../../../lib/sanity/client";
|
|
3
3
|
import { writeClient } from "../../../../../../lib/sanity/writeClient";
|
|
4
4
|
import { isAdminAuthenticated } from "../../../../../../lib/auth";
|
|
5
|
-
import { validateCsrf, csrfErrorResponse } from "../../../../../../lib/csrf";
|
|
5
|
+
import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../../lib/csrf";
|
|
6
6
|
import { logger } from "../../../../../../lib/logger";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -74,6 +74,9 @@ export async function POST(request: NextRequest) {
|
|
|
74
74
|
if (!validateCsrf(request)) {
|
|
75
75
|
return csrfErrorResponse();
|
|
76
76
|
}
|
|
77
|
+
if (!hasJsonContentType(request)) {
|
|
78
|
+
return contentTypeErrorResponse();
|
|
79
|
+
}
|
|
77
80
|
|
|
78
81
|
try {
|
|
79
82
|
const body = await request.json();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
3
3
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
4
|
-
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
4
|
+
import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
|
|
5
5
|
import { getStorageAdapter } from "../../../../../lib/storage";
|
|
6
6
|
import { logger } from "../../../../../lib/logger";
|
|
7
7
|
import type { RegisteredAsset } from "../../../../../lib/sanity/types";
|
|
@@ -39,6 +39,9 @@ export async function POST(request: NextRequest) {
|
|
|
39
39
|
if (!validateCsrf(request)) {
|
|
40
40
|
return csrfErrorResponse();
|
|
41
41
|
}
|
|
42
|
+
if (!hasJsonContentType(request)) {
|
|
43
|
+
return contentTypeErrorResponse();
|
|
44
|
+
}
|
|
42
45
|
|
|
43
46
|
try {
|
|
44
47
|
const body = await request.json();
|
|
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
3
3
|
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
4
4
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
5
|
-
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
5
|
+
import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
|
|
6
6
|
import { isBodyTooLarge, MAX_JSON_BODY_SIZE } from "../../../../../lib/security";
|
|
7
7
|
import { getStorageAdapter, getActiveProvider } from "../../../../../lib/storage";
|
|
8
8
|
import { logger } from "../../../../../lib/logger";
|
|
@@ -29,6 +29,9 @@ export async function POST(request: NextRequest) {
|
|
|
29
29
|
if (!validateCsrf(request)) {
|
|
30
30
|
return csrfErrorResponse();
|
|
31
31
|
}
|
|
32
|
+
if (!hasJsonContentType(request)) {
|
|
33
|
+
return contentTypeErrorResponse();
|
|
34
|
+
}
|
|
32
35
|
if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
|
|
33
36
|
return NextResponse.json({ error: "Request body too large" }, { status: 413 });
|
|
34
37
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
3
|
-
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
3
|
+
import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
|
|
4
4
|
import {
|
|
5
5
|
checkRateLimit,
|
|
6
6
|
jsonError,
|
|
@@ -92,6 +92,9 @@ export async function POST(request: NextRequest) {
|
|
|
92
92
|
if (!validateCsrf(request)) {
|
|
93
93
|
return csrfErrorResponse();
|
|
94
94
|
}
|
|
95
|
+
if (!hasJsonContentType(request)) {
|
|
96
|
+
return contentTypeErrorResponse();
|
|
97
|
+
}
|
|
95
98
|
|
|
96
99
|
// M-2: reject oversize bodies up front so we don't spend CPU on
|
|
97
100
|
// `request.json()` for a payload we'll refuse anyway. Vercel's platform
|
|
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import { S3Client, HeadBucketCommand, PutBucketCorsCommand } from "@aws-sdk/client-s3";
|
|
3
3
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
4
4
|
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
5
|
-
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
5
|
+
import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
|
|
6
6
|
import { encryptToken, isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError } from "../../../../../lib/security";
|
|
7
7
|
import { logger } from "../../../../../lib/logger";
|
|
8
8
|
import { invalidateProviderConfigCache } from "../../../../../lib/storage";
|
|
@@ -25,6 +25,9 @@ export async function POST(request: NextRequest) {
|
|
|
25
25
|
if (!validateCsrf(request)) {
|
|
26
26
|
return csrfErrorResponse();
|
|
27
27
|
}
|
|
28
|
+
if (!hasJsonContentType(request)) {
|
|
29
|
+
return contentTypeErrorResponse();
|
|
30
|
+
}
|
|
28
31
|
if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
|
|
29
32
|
return jsonError("Request body too large", 413);
|
|
30
33
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { S3Client, DeleteObjectCommand, ListObjectsV2Command, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
3
3
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
4
|
-
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
4
|
+
import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
|
|
5
5
|
import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, checkRateLimit } from "../../../../../lib/security";
|
|
6
6
|
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
7
7
|
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
@@ -27,6 +27,9 @@ export async function POST(request: NextRequest) {
|
|
|
27
27
|
if (!validateCsrf(request)) {
|
|
28
28
|
return csrfErrorResponse();
|
|
29
29
|
}
|
|
30
|
+
if (!hasJsonContentType(request)) {
|
|
31
|
+
return contentTypeErrorResponse();
|
|
32
|
+
}
|
|
30
33
|
if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
|
|
31
34
|
return jsonError("Request body too large", 413);
|
|
32
35
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { S3Client, CopyObjectCommand, DeleteObjectCommand, ListObjectsV2Command, HeadObjectCommand } from "@aws-sdk/client-s3";
|
|
3
3
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
4
|
-
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
4
|
+
import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
|
|
5
5
|
import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, checkRateLimit } from "../../../../../lib/security";
|
|
6
6
|
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
7
7
|
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
@@ -29,6 +29,9 @@ export async function POST(request: NextRequest) {
|
|
|
29
29
|
if (!validateCsrf(request)) {
|
|
30
30
|
return csrfErrorResponse();
|
|
31
31
|
}
|
|
32
|
+
if (!hasJsonContentType(request)) {
|
|
33
|
+
return contentTypeErrorResponse();
|
|
34
|
+
}
|
|
32
35
|
if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
|
|
33
36
|
return jsonError("Request body too large", 413);
|
|
34
37
|
}
|
|
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
3
3
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
4
4
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
5
|
-
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
5
|
+
import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
|
|
6
6
|
import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, isValidUploadKey, checkRateLimit } from "../../../../../lib/security";
|
|
7
7
|
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
8
8
|
import { decryptToken } from "../../../../../lib/security";
|
|
@@ -27,6 +27,9 @@ export async function POST(request: NextRequest) {
|
|
|
27
27
|
if (!validateCsrf(request)) {
|
|
28
28
|
return csrfErrorResponse();
|
|
29
29
|
}
|
|
30
|
+
if (!hasJsonContentType(request)) {
|
|
31
|
+
return contentTypeErrorResponse();
|
|
32
|
+
}
|
|
30
33
|
if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
|
|
31
34
|
return jsonError("Request body too large", 413);
|
|
32
35
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { revalidatePath } from "next/cache";
|
|
3
3
|
import { isAdminAuthenticated } from "../../../../lib/auth";
|
|
4
|
-
import { validateCsrf, csrfErrorResponse } from "../../../../lib/csrf";
|
|
4
|
+
import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../lib/csrf";
|
|
5
5
|
import { logger } from "../../../../lib/logger";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -23,6 +23,9 @@ export async function POST(request: NextRequest) {
|
|
|
23
23
|
if (!validateCsrf(request)) {
|
|
24
24
|
return csrfErrorResponse();
|
|
25
25
|
}
|
|
26
|
+
if (!hasJsonContentType(request)) {
|
|
27
|
+
return contentTypeErrorResponse();
|
|
28
|
+
}
|
|
26
29
|
|
|
27
30
|
try {
|
|
28
31
|
const body = await request.json().catch(() => ({}));
|
|
@@ -3,7 +3,7 @@ import { revalidatePath } from "next/cache";
|
|
|
3
3
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
4
4
|
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
5
5
|
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
6
|
-
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
6
|
+
import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
|
|
7
7
|
import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError } from "../../../../../lib/security";
|
|
8
8
|
import { invalidateProviderConfigCache } from "../../../../../lib/storage";
|
|
9
9
|
import { auditLog } from "../../../../../lib/audit";
|
|
@@ -36,6 +36,9 @@ export async function POST(request: NextRequest) {
|
|
|
36
36
|
if (!validateCsrf(request)) {
|
|
37
37
|
return csrfErrorResponse();
|
|
38
38
|
}
|
|
39
|
+
if (!hasJsonContentType(request)) {
|
|
40
|
+
return contentTypeErrorResponse();
|
|
41
|
+
}
|
|
39
42
|
if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
|
|
40
43
|
return jsonError("Request body too large", 413);
|
|
41
44
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import {
|
|
2
|
+
import { client } from "../../../../lib/sanity/client";
|
|
3
3
|
import { customSectionByIdQuery } from "../../../../lib/sanity/queries";
|
|
4
4
|
import { logger } from "../../../../lib/logger";
|
|
5
5
|
|
|
@@ -11,10 +11,9 @@ type RouteContext = { params: Promise<{ id: string }> };
|
|
|
11
11
|
* Returns only the `section` field (PageSectionV2 data) for rendering
|
|
12
12
|
* custom section instances on the public site.
|
|
13
13
|
*
|
|
14
|
-
* Uses
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* Cache-Control headers — this avoids stale CDN responses after edits.
|
|
14
|
+
* Uses the public `client` (useCdn: true) to hit Sanity's CDN (~2ms) instead
|
|
15
|
+
* of the API (~200ms). Freshness is handled by `revalidatePath()` in the
|
|
16
|
+
* admin PATCH endpoint, which purges this endpoint's ISR cache on save.
|
|
18
17
|
*/
|
|
19
18
|
export async function GET(_request: NextRequest, context: RouteContext) {
|
|
20
19
|
try {
|
|
@@ -24,7 +23,7 @@ export async function GET(_request: NextRequest, context: RouteContext) {
|
|
|
24
23
|
return NextResponse.json({ error: "Missing section ID" }, { status: 400 });
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
const result = await
|
|
26
|
+
const result = await client.fetch(customSectionByIdQuery, { id });
|
|
28
27
|
if (!result?.section) {
|
|
29
28
|
return NextResponse.json({ error: "Custom section not found" }, { status: 404 });
|
|
30
29
|
}
|
|
@@ -71,7 +71,7 @@ export default function MetadataEditor({
|
|
|
71
71
|
value={title}
|
|
72
72
|
onChange={(e) => setTitle(e.target.value)}
|
|
73
73
|
placeholder={getSiteConfig().defaults.metaTitle}
|
|
74
|
-
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#
|
|
74
|
+
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none"
|
|
75
75
|
/>
|
|
76
76
|
<p className="text-xs text-neutral-400">
|
|
77
77
|
Shown in browser tabs and search results when no page-specific title is
|
|
@@ -89,7 +89,7 @@ export default function MetadataEditor({
|
|
|
89
89
|
onChange={(e) => setDescription(e.target.value)}
|
|
90
90
|
placeholder="Motion graphics studio based in Barcelona..."
|
|
91
91
|
rows={3}
|
|
92
|
-
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#
|
|
92
|
+
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none resize-none"
|
|
93
93
|
/>
|
|
94
94
|
<div className="flex justify-between">
|
|
95
95
|
<p className="text-xs text-neutral-400">
|
|
@@ -115,7 +115,7 @@ export default function MetadataEditor({
|
|
|
115
115
|
value={ogImage}
|
|
116
116
|
onChange={(e) => setOgImage(e.target.value)}
|
|
117
117
|
placeholder="meta/og-image.jpg"
|
|
118
|
-
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#
|
|
118
|
+
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none"
|
|
119
119
|
/>
|
|
120
120
|
<p className="text-xs text-neutral-400">
|
|
121
121
|
Relative path to the default image shown when sharing on social media.
|
|
@@ -133,7 +133,7 @@ export default function MetadataEditor({
|
|
|
133
133
|
value={favicon}
|
|
134
134
|
onChange={(e) => setFavicon(e.target.value)}
|
|
135
135
|
placeholder="meta/favicon.ico"
|
|
136
|
-
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#
|
|
136
|
+
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none"
|
|
137
137
|
/>
|
|
138
138
|
<p className="text-xs text-neutral-400">
|
|
139
139
|
Relative path to the favicon. Resolved via the asset seed URL.
|
|
@@ -150,7 +150,7 @@ export default function MetadataEditor({
|
|
|
150
150
|
value={analyticsId}
|
|
151
151
|
onChange={(e) => setAnalyticsId(e.target.value)}
|
|
152
152
|
placeholder="G-XXXXXXXXXX or plausible domain"
|
|
153
|
-
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#
|
|
153
|
+
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none"
|
|
154
154
|
/>
|
|
155
155
|
<p className="text-xs text-neutral-400">
|
|
156
156
|
Google Analytics measurement ID or Plausible domain. Leave empty to
|
|
@@ -163,7 +163,7 @@ export default function MetadataEditor({
|
|
|
163
163
|
<button
|
|
164
164
|
onClick={handleSave}
|
|
165
165
|
disabled={saving}
|
|
166
|
-
className="rounded-lg bg-[#
|
|
166
|
+
className="rounded-lg bg-[#3580f9] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#2d6dd4] transition-colors disabled:opacity-50"
|
|
167
167
|
>
|
|
168
168
|
{saving ? "Saving..." : "Save Metadata"}
|
|
169
169
|
</button>
|
|
@@ -96,7 +96,7 @@ export default function PublishToggle(props: PublishToggleProps) {
|
|
|
96
96
|
cursor: "pointer",
|
|
97
97
|
whiteSpace: "nowrap",
|
|
98
98
|
transition: "background-color 150ms, color 150ms",
|
|
99
|
-
backgroundColor: isDraft ? "#
|
|
99
|
+
backgroundColor: isDraft ? "#d97706" : "transparent",
|
|
100
100
|
color: isDraft ? "#fff" : "#a3a3a3",
|
|
101
101
|
...(busy ? { opacity: 0.5 } : {}),
|
|
102
102
|
}}
|
|
@@ -118,7 +118,7 @@ export default function PublishToggle(props: PublishToggleProps) {
|
|
|
118
118
|
cursor: "pointer",
|
|
119
119
|
whiteSpace: "nowrap",
|
|
120
120
|
transition: "background-color 150ms, color 150ms",
|
|
121
|
-
backgroundColor: !isDraft ? "#
|
|
121
|
+
backgroundColor: !isDraft ? "#059669" : "transparent",
|
|
122
122
|
color: !isDraft ? "#fff" : "#a3a3a3",
|
|
123
123
|
...(busy ? { opacity: 0.5 } : {}),
|
|
124
124
|
}}
|
|
@@ -220,7 +220,7 @@ export default function NavBuilder({
|
|
|
220
220
|
className={`px-4 py-1.5 text-[11px] font-medium rounded-lg transition-all ${
|
|
221
221
|
saving || !hasChanges
|
|
222
222
|
? "bg-white/40 text-neutral-400 cursor-not-allowed"
|
|
223
|
-
: "bg-white text-[#
|
|
223
|
+
: "bg-white text-[#3580f9] shadow-sm hover:shadow-md"
|
|
224
224
|
}`}
|
|
225
225
|
>
|
|
226
226
|
{saving ? "Saving..." : "Save"}
|
|
@@ -87,7 +87,7 @@ function DroppableCell({
|
|
|
87
87
|
<div ref={setNodeRef} style={{ gridColumn: column, gridRow: 1 }}>
|
|
88
88
|
<NavGridCell column={column} onAddItem={onAddItem} />
|
|
89
89
|
{isDragOver && (
|
|
90
|
-
<div className="absolute inset-0 rounded-lg ring-2 ring-[#
|
|
90
|
+
<div className="absolute inset-0 rounded-lg ring-2 ring-[#3580f9] bg-[#3580f9]/10 pointer-events-none" />
|
|
91
91
|
)}
|
|
92
92
|
</div>
|
|
93
93
|
);
|
|
@@ -286,7 +286,7 @@ export default function NavBuilderGrid({
|
|
|
286
286
|
{/* Helper hints */}
|
|
287
287
|
<div className="mt-4 flex gap-4 text-[10px] text-neutral-400">
|
|
288
288
|
<span>
|
|
289
|
-
<span className="text-[#
|
|
289
|
+
<span className="text-[#3580f9]">+</span> Click empty column to add
|
|
290
290
|
</span>
|
|
291
291
|
<span>
|
|
292
292
|
<span className="text-neutral-500">⠇</span> Drag to move
|
|
@@ -301,7 +301,7 @@ export default function NavBuilderGrid({
|
|
|
301
301
|
<DragOverlay>
|
|
302
302
|
{activeItem ? (
|
|
303
303
|
<div
|
|
304
|
-
className="h-14 flex items-center px-3 gap-2 rounded-lg ring-2 ring-[#
|
|
304
|
+
className="h-14 flex items-center px-3 gap-2 rounded-lg ring-2 ring-[#3580f9] bg-white shadow-xl"
|
|
305
305
|
style={{ width: 120, opacity: 0.9 }}
|
|
306
306
|
>
|
|
307
307
|
<div className="text-xs font-mono text-neutral-900 uppercase tracking-wider truncate">
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
|
|
5
|
-
interface NavGridCellProps {
|
|
6
|
-
column: number;
|
|
7
|
-
onAddItem: (column: number) => void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export default function NavGridCell({ column, onAddItem }: NavGridCellProps) {
|
|
11
|
-
const [hovered, setHovered] = useState(false);
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<div
|
|
15
|
-
onMouseEnter={() => setHovered(true)}
|
|
16
|
-
onMouseLeave={() => setHovered(false)}
|
|
17
|
-
onClick={(e) => {
|
|
18
|
-
e.stopPropagation();
|
|
19
|
-
onAddItem(column);
|
|
20
|
-
}}
|
|
21
|
-
className={`h-14 flex items-center justify-center cursor-pointer transition-all rounded-lg border border-dashed ${
|
|
22
|
-
hovered
|
|
23
|
-
? "border-[#
|
|
24
|
-
: "border-neutral-200 bg-transparent"
|
|
25
|
-
}`}
|
|
26
|
-
data-nav-cell={column}
|
|
27
|
-
>
|
|
28
|
-
<div
|
|
29
|
-
className={`flex flex-col items-center gap-0.5 transition-all ${
|
|
30
|
-
hovered ? "text-[#
|
|
31
|
-
}`}
|
|
32
|
-
style={{
|
|
33
|
-
opacity: hovered ? 1 : 0.5,
|
|
34
|
-
transform: hovered ? "scale(1)" : "scale(0.9)",
|
|
35
|
-
}}
|
|
36
|
-
>
|
|
37
|
-
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
|
|
38
|
-
<path
|
|
39
|
-
d="M8 3v10M3 8h10"
|
|
40
|
-
stroke="currentColor"
|
|
41
|
-
strokeWidth="1.5"
|
|
42
|
-
strokeLinecap="round"
|
|
43
|
-
/>
|
|
44
|
-
</svg>
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
interface NavGridCellProps {
|
|
6
|
+
column: number;
|
|
7
|
+
onAddItem: (column: number) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function NavGridCell({ column, onAddItem }: NavGridCellProps) {
|
|
11
|
+
const [hovered, setHovered] = useState(false);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
onMouseEnter={() => setHovered(true)}
|
|
16
|
+
onMouseLeave={() => setHovered(false)}
|
|
17
|
+
onClick={(e) => {
|
|
18
|
+
e.stopPropagation();
|
|
19
|
+
onAddItem(column);
|
|
20
|
+
}}
|
|
21
|
+
className={`h-14 flex items-center justify-center cursor-pointer transition-all rounded-lg border border-dashed ${
|
|
22
|
+
hovered
|
|
23
|
+
? "border-[#3580f9]/50 bg-[#3580f9]/[0.03]"
|
|
24
|
+
: "border-neutral-200 bg-transparent"
|
|
25
|
+
}`}
|
|
26
|
+
data-nav-cell={column}
|
|
27
|
+
>
|
|
28
|
+
<div
|
|
29
|
+
className={`flex flex-col items-center gap-0.5 transition-all ${
|
|
30
|
+
hovered ? "text-[#3580f9]" : "text-neutral-300"
|
|
31
|
+
}`}
|
|
32
|
+
style={{
|
|
33
|
+
opacity: hovered ? 1 : 0.5,
|
|
34
|
+
transform: hovered ? "scale(1)" : "scale(0.9)",
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
|
|
38
|
+
<path
|
|
39
|
+
d="M8 3v10M3 8h10"
|
|
40
|
+
stroke="currentColor"
|
|
41
|
+
strokeWidth="1.5"
|
|
42
|
+
strokeLinecap="round"
|
|
43
|
+
/>
|
|
44
|
+
</svg>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useState, useRef, useCallback, useEffect } from "react";
|
|
4
4
|
import type { NavItem } from "../../../lib/sanity/types";
|
|
5
5
|
import { TOTAL_COLUMNS, getMaxSpan } from "./nav-builder-utils";
|
|
6
|
+
import { BubbleTooltip } from "../../builder/BubbleIcons";
|
|
6
7
|
|
|
7
8
|
interface NavGridItemProps {
|
|
8
9
|
item: NavItem;
|
|
@@ -94,7 +95,7 @@ export default function NavGridItem({
|
|
|
94
95
|
}}
|
|
95
96
|
className={`h-14 relative flex items-center px-3 gap-2 cursor-pointer rounded-lg transition-all border ${
|
|
96
97
|
isSelected
|
|
97
|
-
? "border-[#
|
|
98
|
+
? "border-[#3580f9] bg-[#3580f9]/5 shadow-sm"
|
|
98
99
|
: hovered
|
|
99
100
|
? "border-neutral-300 bg-neutral-50"
|
|
100
101
|
: "border-neutral-200 bg-white"
|
|
@@ -141,7 +142,7 @@ export default function NavGridItem({
|
|
|
141
142
|
<div
|
|
142
143
|
className="absolute -top-2 -right-2 text-[9px] font-semibold px-1.5 py-0 rounded text-white"
|
|
143
144
|
style={{
|
|
144
|
-
background: isSelected ? "#
|
|
145
|
+
background: isSelected ? "#3580f9" : "#a3a3a3",
|
|
145
146
|
}}
|
|
146
147
|
>
|
|
147
148
|
{item.column_span}/{TOTAL_COLUMNS}
|
|
@@ -155,8 +156,8 @@ export default function NavGridItem({
|
|
|
155
156
|
e.stopPropagation();
|
|
156
157
|
onDelete(item._key);
|
|
157
158
|
}}
|
|
158
|
-
className="absolute -top-2 -left-2 w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transition-colors shadow-sm"
|
|
159
|
-
|
|
159
|
+
className="group/bb absolute -top-2 -left-2 w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transition-colors shadow-sm"
|
|
160
|
+
aria-label="Remove item"
|
|
160
161
|
>
|
|
161
162
|
<svg width="10" height="10" viewBox="0 0 10 10">
|
|
162
163
|
<path
|
|
@@ -166,6 +167,7 @@ export default function NavGridItem({
|
|
|
166
167
|
strokeLinecap="round"
|
|
167
168
|
/>
|
|
168
169
|
</svg>
|
|
170
|
+
<BubbleTooltip>Remove item</BubbleTooltip>
|
|
169
171
|
</button>
|
|
170
172
|
)}
|
|
171
173
|
|
|
@@ -175,9 +177,9 @@ export default function NavGridItem({
|
|
|
175
177
|
className="absolute -right-1 top-1/2 -translate-y-1/2 w-2 h-6 rounded cursor-col-resize transition-all"
|
|
176
178
|
style={{
|
|
177
179
|
background: resizing
|
|
178
|
-
? "#
|
|
180
|
+
? "#3580f9"
|
|
179
181
|
: isSelected
|
|
180
|
-
? "#
|
|
182
|
+
? "#3580f9"
|
|
181
183
|
: hovered
|
|
182
184
|
? "#a3a3a3"
|
|
183
185
|
: "transparent",
|