@morphika/andami 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/(site)/[slug]/page.tsx +5 -2
- package/app/(site)/layout.tsx +4 -2
- package/app/(site)/work/[slug]/page.tsx +5 -2
- package/app/admin/layout.tsx +2 -1
- package/app/api/admin/assets/health/route.ts +1 -1
- package/app/api/admin/assets/register/route.ts +1 -1
- package/app/api/admin/assets/registry/route.ts +1 -1
- package/app/api/admin/assets/relink/confirm/route.ts +1 -1
- package/app/api/admin/assets/relink/route.ts +1 -1
- package/app/api/admin/assets/scan/route.ts +1 -1
- package/app/api/admin/custom-sections/[slug]/route.ts +1 -1
- package/app/api/admin/custom-sections/route.ts +1 -1
- package/app/api/admin/database/route.ts +1 -1
- package/app/api/admin/pages/[slug]/duplicate/route.ts +1 -1
- package/app/api/admin/pages/[slug]/route.ts +1 -1
- package/app/api/admin/pages/[slug]/set-home/route.ts +1 -1
- package/app/api/admin/pages/route.ts +1 -1
- package/app/api/admin/preview/route.ts +1 -1
- package/app/api/admin/r2/delete/route.ts +1 -1
- package/app/api/admin/r2/rename/route.ts +1 -1
- package/app/api/admin/r2/status/route.ts +1 -1
- package/app/api/admin/r2/upload-url/route.ts +1 -1
- package/app/api/admin/settings/route.ts +1 -1
- package/app/api/admin/setup/complete/route.ts +1 -1
- package/app/api/admin/setup/route.ts +1 -1
- package/app/api/admin/storage/switch/route.ts +1 -1
- package/app/api/admin/styles/route.ts +1 -1
- package/components/blocks/BlockRenderer.tsx +1 -0
- package/components/blocks/SectionV2Renderer.tsx +1 -1
- package/components/blocks/TextBlockRenderer.tsx +4 -1
- package/components/ui/Navbar.tsx +9 -7
- package/lib/sanity/client.ts +16 -0
- package/lib/storage/index.ts +22 -4
- package/lib/version.ts +6 -0
- package/package.json +1 -1
- package/styles/base.css +11 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cache } from "react";
|
|
1
2
|
import { notFound } from "next/navigation";
|
|
2
3
|
import type { Metadata } from "next";
|
|
3
4
|
import { client } from "../../../lib/sanity/client";
|
|
@@ -16,14 +17,16 @@ interface PageProps {
|
|
|
16
17
|
params: Promise<{ slug: string }>;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
// CPU Audit Fix 3: React cache() deduplicates this call within a single
|
|
21
|
+
// render pass — generateMetadata() and the page component share one fetch.
|
|
22
|
+
const getPageBySlug = cache(async (slug: string): Promise<Page | null> => {
|
|
20
23
|
try {
|
|
21
24
|
return await client.fetch<Page>(publishedPageBySlugQuery, { slug });
|
|
22
25
|
} catch (error) {
|
|
23
26
|
console.error(`[Page] Failed to fetch page "${slug}":`, error);
|
|
24
27
|
return null;
|
|
25
28
|
}
|
|
26
|
-
}
|
|
29
|
+
});
|
|
27
30
|
|
|
28
31
|
export async function generateStaticParams() {
|
|
29
32
|
try {
|
package/app/(site)/layout.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cache } from "react";
|
|
1
2
|
import { client } from "../../lib/sanity/client";
|
|
2
3
|
import { siteSettingsQuery } from "../../lib/sanity/queries";
|
|
3
4
|
import type { SiteSettings } from "../../lib/sanity/types";
|
|
@@ -11,14 +12,15 @@ import CustomCursor from "../../components/ui/CustomCursor";
|
|
|
11
12
|
import ScrollToTop from "../../components/ui/ScrollToTop";
|
|
12
13
|
import PortfolioTracker from "../../components/ui/PortfolioTracker";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
// CPU Audit Fix 3: React cache() deduplicates within a render pass.
|
|
16
|
+
const getSiteSettings = cache(async (): Promise<SiteSettings | null> => {
|
|
15
17
|
try {
|
|
16
18
|
return await client.fetch<SiteSettings>(siteSettingsQuery);
|
|
17
19
|
} catch (error) {
|
|
18
20
|
console.error("[SiteLayout] Failed to fetch site settings:", error);
|
|
19
21
|
return null;
|
|
20
22
|
}
|
|
21
|
-
}
|
|
23
|
+
});
|
|
22
24
|
|
|
23
25
|
export default async function SiteLayout({
|
|
24
26
|
children,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cache } from "react";
|
|
1
2
|
import { notFound } from "next/navigation";
|
|
2
3
|
import type { Metadata } from "next";
|
|
3
4
|
import { client } from "../../../../lib/sanity/client";
|
|
@@ -17,14 +18,16 @@ interface ProjectPageProps {
|
|
|
17
18
|
params: Promise<{ slug: string }>;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
// CPU Audit Fix 3: React cache() deduplicates this call within a single
|
|
22
|
+
// render pass — generateMetadata() and the page component share one fetch.
|
|
23
|
+
const getProjectPage = cache(async (slug: string): Promise<Page | null> => {
|
|
21
24
|
try {
|
|
22
25
|
return await client.fetch<Page>(publishedProjectBySlugQuery, { slug });
|
|
23
26
|
} catch (error) {
|
|
24
27
|
console.error(`[Project] Failed to fetch project "${slug}":`, error);
|
|
25
28
|
return null;
|
|
26
29
|
}
|
|
27
|
-
}
|
|
30
|
+
});
|
|
28
31
|
|
|
29
32
|
export async function generateStaticParams() {
|
|
30
33
|
try {
|
package/app/admin/layout.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { usePathname, useRouter } from "next/navigation";
|
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { useState, useEffect, useRef } from "react";
|
|
6
6
|
import { getSiteConfig } from "../../lib/config";
|
|
7
|
+
import { ANDAMI_VERSION } from "../../lib/version";
|
|
7
8
|
|
|
8
9
|
// ============================================
|
|
9
10
|
// Navigation Configuration — flat list, no sections
|
|
@@ -161,7 +162,7 @@ export default function AdminLayout({
|
|
|
161
162
|
<div className="flex h-14 items-center justify-between px-4 border-b border-white/[0.06]">
|
|
162
163
|
{sidebarOpen && (
|
|
163
164
|
<span className="text-[10px] font-semibold tracking-widest uppercase text-white/90 leading-tight">
|
|
164
|
-
|
|
165
|
+
Morphika Andami <span className="text-white/40 font-normal">v{ANDAMI_VERSION}</span><br />
|
|
165
166
|
<span className="text-white/50 font-normal">{getSiteConfig().name}</span>
|
|
166
167
|
</span>
|
|
167
168
|
)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
-
import { client } from "../../../../../lib/sanity/client";
|
|
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
5
|
import { logger } from "../../../../../lib/logger";
|
|
@@ -3,7 +3,7 @@ import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
|
3
3
|
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
4
4
|
import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath } from "../../../../../lib/security";
|
|
5
5
|
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
6
|
-
import { client } from "../../../../../lib/sanity/client";
|
|
6
|
+
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
7
7
|
import { isMediaFile, getMimeType } from "../../../../../lib/storage/types";
|
|
8
8
|
import { auditLog } from "../../../../../lib/audit";
|
|
9
9
|
import { logger } from "../../../../../lib/logger";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { client } from "../../../../../lib/sanity/client";
|
|
2
|
+
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";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { client } from "../../../../../../lib/sanity/client";
|
|
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
5
|
import { validateCsrf, csrfErrorResponse } from "../../../../../../lib/csrf";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { client } from "../../../../../lib/sanity/client";
|
|
2
|
+
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
3
3
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
4
4
|
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
5
5
|
import { getStorageAdapter } from "../../../../../lib/storage";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { client } from "../../../../../lib/sanity/client";
|
|
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
5
|
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { revalidatePath } from "next/cache";
|
|
3
|
-
import { client } from "../../../../../lib/sanity/client";
|
|
3
|
+
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
4
4
|
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
5
5
|
import { customSectionBySlugQuery, pagesUsingCustomSectionQuery } from "../../../../../lib/sanity/queries";
|
|
6
6
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { client } from "../../../../lib/sanity/client";
|
|
2
|
+
import { adminClient as client } from "../../../../lib/sanity/client";
|
|
3
3
|
import { writeClient } from "../../../../lib/sanity/writeClient";
|
|
4
4
|
import { allCustomSectionsQuery } from "../../../../lib/sanity/queries";
|
|
5
5
|
import { isAdminAuthenticated } from "../../../../lib/auth";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { isAdminAuthenticated } from "../../../../lib/auth";
|
|
3
|
-
import { client } from "../../../../lib/sanity/client";
|
|
3
|
+
import { adminClient as client } from "../../../../lib/sanity/client";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* GET /api/admin/database — Sanity connection status & stats
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { revalidatePath } from "next/cache";
|
|
3
|
-
import { client } from "../../../../../../lib/sanity/client";
|
|
3
|
+
import { adminClient as client } from "../../../../../../lib/sanity/client";
|
|
4
4
|
import { writeClient } from "../../../../../../lib/sanity/writeClient";
|
|
5
5
|
import { pageBySlugQuery } from "../../../../../../lib/sanity/queries";
|
|
6
6
|
import { isAdminAuthenticated } from "../../../../../../lib/auth";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { revalidatePath } from "next/cache";
|
|
3
|
-
import { client } from "../../../../../lib/sanity/client";
|
|
3
|
+
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
4
4
|
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
5
5
|
import { pageBySlugQuery } from "../../../../../lib/sanity/queries";
|
|
6
6
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { revalidatePath } from "next/cache";
|
|
3
|
-
import { client } from "../../../../../../lib/sanity/client";
|
|
3
|
+
import { adminClient as client } from "../../../../../../lib/sanity/client";
|
|
4
4
|
import { writeClient } from "../../../../../../lib/sanity/writeClient";
|
|
5
5
|
import { isAdminAuthenticated } from "../../../../../../lib/auth";
|
|
6
6
|
import { validateCsrf, csrfErrorResponse } from "../../../../../../lib/csrf";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { client } from "../../../../lib/sanity/client";
|
|
2
|
+
import { adminClient as client } from "../../../../lib/sanity/client";
|
|
3
3
|
import { writeClient } from "../../../../lib/sanity/writeClient";
|
|
4
4
|
import { allPagesQuery } from "../../../../lib/sanity/queries";
|
|
5
5
|
import { isAdminAuthenticated } from "../../../../lib/auth";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { client } from "../../../../lib/sanity/client";
|
|
2
|
+
import { adminClient as client } from "../../../../lib/sanity/client";
|
|
3
3
|
import { pageBySlugQuery } from "../../../../lib/sanity/queries";
|
|
4
4
|
import { isAdminAuthenticated } from "../../../../lib/auth";
|
|
5
5
|
import { logger } from "../../../../lib/logger";
|
|
@@ -3,7 +3,7 @@ import { S3Client, DeleteObjectCommand, ListObjectsV2Command, DeleteObjectsComma
|
|
|
3
3
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
4
4
|
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
5
5
|
import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, checkRateLimit } from "../../../../../lib/security";
|
|
6
|
-
import { client } from "../../../../../lib/sanity/client";
|
|
6
|
+
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
7
7
|
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
8
8
|
import { decryptToken } from "../../../../../lib/security";
|
|
9
9
|
import { auditLog } from "../../../../../lib/audit";
|
|
@@ -3,7 +3,7 @@ import { S3Client, CopyObjectCommand, DeleteObjectCommand, ListObjectsV2Command,
|
|
|
3
3
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
4
4
|
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
5
5
|
import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, checkRateLimit } from "../../../../../lib/security";
|
|
6
|
-
import { client } from "../../../../../lib/sanity/client";
|
|
6
|
+
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
7
7
|
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
8
8
|
import { decryptToken } from "../../../../../lib/security";
|
|
9
9
|
import { auditLog } from "../../../../../lib/audit";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
|
|
3
3
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
4
|
-
import { client } from "../../../../../lib/sanity/client";
|
|
4
|
+
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
5
5
|
import { decryptToken } from "../../../../../lib/security";
|
|
6
6
|
import { logger } from "../../../../../lib/logger";
|
|
7
7
|
|
|
@@ -4,7 +4,7 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
|
4
4
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
5
5
|
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
6
6
|
import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, checkRateLimit } from "../../../../../lib/security";
|
|
7
|
-
import { client } from "../../../../../lib/sanity/client";
|
|
7
|
+
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
8
8
|
import { decryptToken } from "../../../../../lib/security";
|
|
9
9
|
import { getMimeType, isMediaFile } from "../../../../../lib/storage/types";
|
|
10
10
|
import { logger } from "../../../../../lib/logger";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { isAdminAuthenticated } from "../../../../lib/auth";
|
|
3
|
-
import { client } from "../../../../lib/sanity/client";
|
|
3
|
+
import { adminClient as client } from "../../../../lib/sanity/client";
|
|
4
4
|
import { writeClient } from "../../../../lib/sanity/writeClient";
|
|
5
5
|
import { siteSettingsQuery } from "../../../../lib/sanity/queries";
|
|
6
6
|
import { isSafeUrl, isBodyTooLarge, MAX_JSON_BODY_SIZE } from "../../../../lib/security";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
3
3
|
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
4
|
-
import { client } from "../../../../../lib/sanity/client";
|
|
4
|
+
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
5
5
|
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
6
6
|
import { auditLog } from "../../../../../lib/audit";
|
|
7
7
|
import { logger } from "../../../../../lib/logger";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { isAdminAuthenticated } from "../../../../lib/auth";
|
|
3
3
|
import { getSetupStatus } from "../../../../lib/setup/detect";
|
|
4
|
-
import { client } from "../../../../lib/sanity/client";
|
|
4
|
+
import { adminClient as client } from "../../../../lib/sanity/client";
|
|
5
5
|
import { writeClient } from "../../../../lib/sanity/writeClient";
|
|
6
6
|
import { validateCsrf, csrfErrorResponse } from "../../../../lib/csrf";
|
|
7
7
|
|
|
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import { revalidatePath } from "next/cache";
|
|
3
3
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
4
4
|
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
5
|
-
import { client } from "../../../../../lib/sanity/client";
|
|
5
|
+
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
6
6
|
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
7
7
|
import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError } from "../../../../../lib/security";
|
|
8
8
|
import { invalidateProviderConfigCache } from "../../../../../lib/storage";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { isAdminAuthenticated } from "../../../../lib/auth";
|
|
3
|
-
import { client } from "../../../../lib/sanity/client";
|
|
3
|
+
import { adminClient as client } from "../../../../lib/sanity/client";
|
|
4
4
|
import { writeClient } from "../../../../lib/sanity/writeClient";
|
|
5
5
|
import { siteStylesQuery } from "../../../../lib/sanity/queries";
|
|
6
6
|
import { validateCsrf, csrfErrorResponse } from "../../../../lib/csrf";
|
|
@@ -281,7 +281,7 @@ export default function SectionV2Renderer({ section, pageEnterAnimation }: Secti
|
|
|
281
281
|
const alignStyles = hasBlockAlignment(blockLayout) ? getBlockAlignmentStyles(blockLayout) : undefined;
|
|
282
282
|
const hasHAlign = blockLayout?.align_h && blockLayout.align_h !== "left";
|
|
283
283
|
return (
|
|
284
|
-
<div key={block._key} className={`blk-wrap-${block._key}`} style={{ ...(!hasHAlign ? { width: "100%" } : {}), minWidth: 0, ...alignStyles }}>
|
|
284
|
+
<div key={block._key} className={`blk-wrap-${block._key}`} style={{ ...(!hasHAlign ? { width: "100%" } : { width: "auto", maxWidth: "100%" }), minWidth: 0, ...alignStyles }}>
|
|
285
285
|
<BlockRenderer
|
|
286
286
|
block={block}
|
|
287
287
|
columnEnterAnimation={col.enter_animation}
|
|
@@ -85,7 +85,10 @@ export default function TextBlockRenderer({ block }: { block: TextBlock }) {
|
|
|
85
85
|
const { className, style } = getTextBlockStyles(block);
|
|
86
86
|
|
|
87
87
|
return (
|
|
88
|
-
<div
|
|
88
|
+
<div
|
|
89
|
+
className={`${className} space-y-[0.75em]`}
|
|
90
|
+
style={{ overflowWrap: "break-word", wordBreak: "break-word", minWidth: 0, ...style }}
|
|
91
|
+
>
|
|
89
92
|
<PortableText value={block.text} />
|
|
90
93
|
</div>
|
|
91
94
|
);
|
package/components/ui/Navbar.tsx
CHANGED
|
@@ -537,8 +537,10 @@ export default function Navbar({
|
|
|
537
537
|
</div>
|
|
538
538
|
|
|
539
539
|
{/* Mobile: simple flex layout with hamburger — uses independent mobile styles */}
|
|
540
|
+
{/* BG color and icon color only apply when menu is open (overlay visible). */}
|
|
541
|
+
{/* When closed, the bar inherits desktop navbar colors for visual continuity. */}
|
|
540
542
|
<div
|
|
541
|
-
className={`flex lg:hidden items-center justify-between ${mobileBarColorClass}`}
|
|
543
|
+
className={`flex lg:hidden items-center justify-between ${isMenuOpen ? mobileBarColorClass : ""} transition-colors duration-300`}
|
|
542
544
|
style={{
|
|
543
545
|
maxWidth: "var(--grid-width, 1445px)",
|
|
544
546
|
marginLeft: "auto",
|
|
@@ -547,16 +549,16 @@ export default function Navbar({
|
|
|
547
549
|
paddingRight: `${mobilePaddingH}px`,
|
|
548
550
|
paddingTop: `${mobilePaddingV}px`,
|
|
549
551
|
paddingBottom: `${mobilePaddingV}px`,
|
|
550
|
-
...mobileNavBgStyle,
|
|
551
|
-
...mobileBarColorStyle,
|
|
552
|
+
...(isMenuOpen ? mobileNavBgStyle : {}),
|
|
553
|
+
...(isMenuOpen ? mobileBarColorStyle : {}),
|
|
552
554
|
}}
|
|
553
555
|
>
|
|
554
556
|
<Link
|
|
555
557
|
href="/"
|
|
556
|
-
className={`tracking-normal ${mobileBarColorClass} transition-colors duration-
|
|
558
|
+
className={`tracking-normal ${isMenuOpen && mobileBarColorClass ? mobileBarColorClass : ""} transition-colors duration-300 hover:opacity-80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`}
|
|
557
559
|
style={{
|
|
558
560
|
...linkStyle,
|
|
559
|
-
...(mobileBarIsHex ? { color: "inherit" } : {}),
|
|
561
|
+
...(isMenuOpen && mobileBarIsHex ? { color: "inherit" } : {}),
|
|
560
562
|
}}
|
|
561
563
|
>
|
|
562
564
|
{logoLabel}
|
|
@@ -565,11 +567,11 @@ export default function Navbar({
|
|
|
565
567
|
<button
|
|
566
568
|
ref={hamburgerButtonRef}
|
|
567
569
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
568
|
-
className={`flex flex-col justify-center items-center gap-[5px] w-8 h-8 ${mobileBarColorClass} focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`}
|
|
570
|
+
className={`flex flex-col justify-center items-center gap-[5px] w-8 h-8 ${isMenuOpen ? mobileBarColorClass : ""} transition-colors duration-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`}
|
|
569
571
|
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
|
570
572
|
aria-expanded={isMenuOpen}
|
|
571
573
|
aria-controls="mobile-nav-menu"
|
|
572
|
-
style={mobileBarIsHex ? { color: mobileBarColor } : undefined}
|
|
574
|
+
style={isMenuOpen && mobileBarIsHex ? { color: mobileBarColor } : undefined}
|
|
573
575
|
>
|
|
574
576
|
<span
|
|
575
577
|
className={`block w-5 h-[1.5px] bg-current transition-all duration-300 ${
|
package/lib/sanity/client.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { createClient } from "next-sanity";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Public read client — uses Sanity CDN for fast edge-cached responses.
|
|
5
|
+
* Stale window: ~120s (acceptable since ISR revalidates every 1h).
|
|
6
|
+
* Used by: public pages, public API routes, asset proxy.
|
|
7
|
+
*/
|
|
3
8
|
export const client = createClient({
|
|
9
|
+
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
|
|
10
|
+
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
|
|
11
|
+
apiVersion: "2024-01-01",
|
|
12
|
+
useCdn: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Admin read client — bypasses CDN for always-fresh data.
|
|
17
|
+
* Used by: admin API routes that read content for editing.
|
|
18
|
+
*/
|
|
19
|
+
export const adminClient = createClient({
|
|
4
20
|
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
|
|
5
21
|
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
|
|
6
22
|
apiVersion: "2024-01-01",
|
package/lib/storage/index.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* const url = await adapter.resolveUrl("projects/hero.jpg");
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { client } from "../../lib/sanity/client";
|
|
17
|
+
import { adminClient as client } from "../../lib/sanity/client";
|
|
18
18
|
import { R2Adapter } from "./r2-adapter";
|
|
19
19
|
import { logger } from "../../lib/logger";
|
|
20
20
|
import type { StorageAdapter, StorageProvider, StorageProviderConfig } from "./types";
|
|
@@ -120,7 +120,7 @@ export async function getAllProviderStatuses(): Promise<{
|
|
|
120
120
|
/**
|
|
121
121
|
* In-memory cache for the R2 public URL.
|
|
122
122
|
* Used by the asset proxy routes to resolve R2 redirects without
|
|
123
|
-
* hitting Sanity on every request. TTL:
|
|
123
|
+
* hitting Sanity on every request. TTL: 30 minutes.
|
|
124
124
|
*/
|
|
125
125
|
let r2ConfigCache: {
|
|
126
126
|
provider: StorageProvider;
|
|
@@ -131,7 +131,9 @@ let r2ConfigCache: {
|
|
|
131
131
|
// #4: In-flight promise deduplication — prevents cache stampede race condition
|
|
132
132
|
let inFlightFetch: Promise<ProviderConfig> | null = null;
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
// CPU Audit Fix 4: Increased from 5min to 30min — R2 bucket URL rarely changes,
|
|
135
|
+
// and invalidateProviderConfigCache() handles immediate resets on config changes.
|
|
136
|
+
const PROVIDER_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
135
137
|
|
|
136
138
|
export interface ProviderConfig {
|
|
137
139
|
provider: StorageProvider;
|
|
@@ -143,7 +145,11 @@ export interface ProviderConfig {
|
|
|
143
145
|
*
|
|
144
146
|
* Used by /api/assets and /api/admin/assets/file to decide
|
|
145
147
|
* how to redirect asset requests.
|
|
146
|
-
*
|
|
148
|
+
*
|
|
149
|
+
* Resolution order (CPU Audit Fix 2):
|
|
150
|
+
* 1. In-memory cache (30-minute TTL)
|
|
151
|
+
* 2. NEXT_PUBLIC_R2_BUCKET_URL env var (zero-cost, no Sanity query)
|
|
152
|
+
* 3. Sanity assetRegistry query (fallback)
|
|
147
153
|
*
|
|
148
154
|
* #4: Uses promise chaining to deduplicate concurrent requests.
|
|
149
155
|
* #23: Adds AbortController timeout on Sanity fetch.
|
|
@@ -154,6 +160,18 @@ export async function getCachedProviderConfig(): Promise<ProviderConfig> {
|
|
|
154
160
|
return r2ConfigCache;
|
|
155
161
|
}
|
|
156
162
|
|
|
163
|
+
// CPU Audit Fix 2: Check env var first — avoids Sanity query entirely.
|
|
164
|
+
// The R2 bucket URL almost never changes; env var is the fastest path.
|
|
165
|
+
const envBucketUrl = process.env.NEXT_PUBLIC_R2_BUCKET_URL;
|
|
166
|
+
if (envBucketUrl) {
|
|
167
|
+
const config: ProviderConfig = {
|
|
168
|
+
provider: "r2" as StorageProvider,
|
|
169
|
+
r2BucketUrl: envBucketUrl,
|
|
170
|
+
};
|
|
171
|
+
r2ConfigCache = { ...config, fetchedAt: Date.now() };
|
|
172
|
+
return config;
|
|
173
|
+
}
|
|
174
|
+
|
|
157
175
|
// #4: If there's already an in-flight fetch, reuse it instead of starting a new one
|
|
158
176
|
if (inFlightFetch) return inFlightFetch;
|
|
159
177
|
|
package/lib/version.ts
ADDED
package/package.json
CHANGED
package/styles/base.css
CHANGED
|
@@ -57,6 +57,17 @@ body {
|
|
|
57
57
|
overflow-x: clip;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/* Prevent text overflow in grid columns on narrow viewports */
|
|
61
|
+
[data-site] p,
|
|
62
|
+
[data-site] h1,
|
|
63
|
+
[data-site] h2,
|
|
64
|
+
[data-site] h3,
|
|
65
|
+
[data-site] h4,
|
|
66
|
+
[data-site] span {
|
|
67
|
+
overflow-wrap: break-word;
|
|
68
|
+
word-break: break-word;
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
[data-custom-cursor] {
|
|
61
72
|
cursor: none;
|
|
62
73
|
}
|