diff --git a/dev_server.log b/dev_server.log new file mode 100644 index 0000000..c046acf --- /dev/null +++ b/dev_server.log @@ -0,0 +1,312 @@ + ⚠ Port 3000 is in use by an unknown process, using available port 3001 instead. +[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D` + ▲ Next.js 16.0.7 (Turbopack) + - Local: http://localhost:3001 + - Network: http://192.168.0.2:3001 + + ✓ Starting... + ⨯ Unable to acquire lock at /app/.next/dev/lock, is another instance of next dev running? + Suggestion: If you intended to restart next dev, terminate the other process, and then try again. +[baseline-browser-mapping] The data in this module is over two mon ○ Compiling /[variant] ... + ⨯ ./node_modules/@sanity/client/dist/index.browser.js:4:1 +Module not found: Can't resolve 'rxjs' +  2 | import { adapter, environment } from "get-it"; +  3 | import { retry, jsonRequest, jsonResponse, progress, observable } from "get-it/middleware"; +> 4 | import { Observable, defer, of, isObservable, mergeMap, from, lastValueFrom, shareReplay, catchError, concat, throwError, timer, tap, finalize, share, merge, map as map$1, firstValueFrom } from "rxjs"; +  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +  5 | import { isRecord, stegaClean } from "./_chunks-es/stegaClean.js"; +  6 | import { combineLatestWith, map, filter, finalize as finalize$1 } from "rxjs/operators"; +  7 | import { getVersionFromId, isDraftId, getVersionId, getDraftId, isVersionId, getPublishedId } from "@sanity/client/csm"; + + + +Import traces: + #1 [Server Component]: + ./node_modules/@sanity/client/dist/index.browser.js + ./src/sanity/lib/client.ts + ./src/sanity/lib/queries.ts + ./src/app/(portfolio)/[variant]/layout.tsx + + #2 [Server Component]: + ./node_modules/@sanity/client/dist/index.browser.js + ./src/sanity/lib/client.ts + ./src/sanity/lib/queries.ts + ./src/app/not-found.tsx + + #3 [Client Component Browser]: + ./node_modules/@sanity/client/dist/index.browser.js [Client Component Browser] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Client Component Browser] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Server Component] + ./node_modules/next-sanity/dist/live.js [Server Component] + ./src/sanity/lib/live.ts [Server Component] + ./src/app/(portfolio)/[variant]/layout.tsx [Server Component] + + #4 [Client Component Browser]: + ./node_modules/@sanity/client/dist/index.browser.js [Client Component Browser] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Client Component Browser] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Server Component] + ./node_modules/next-sanity/dist/live.js [Server Component] + ./src/sanity/lib/live.ts [Server Component] + ./src/sanity/lib/queries.ts [Server Component] + ./src/app/not-found.tsx [Server Component] + +https://nextjs.org/docs/messages/module-not-found + + + +./node_modules/@sanity/client/dist/index.js:4:1 +Module not found: Can't resolve 'rxjs' +  2 | import { adapter, environment } from "get-it"; +  3 | import { retry, jsonRequest, jsonResponse, progress, observable, debug, headers, agent } from "get-it/middleware"; +> 4 | import { Observable, defer, of, isObservable, mergeMap, from, lastValueFrom, shareReplay, catchError, concat, throwError, timer, tap, finalize, share, merge, map as map$1, firstValueFrom } from "rxjs"; +  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +  5 | import { isRecord } from "./_chunks-es/isRecord.js"; +  6 | import { stegaClean } from "./_chunks-es/stegaClean.js"; +  7 | import { combineLatestWith, map, filter, finalize as finalize$1 } from "rxjs/operators"; + + + +Import traces: + #1 [Client Component SSR]: + ./node_modules/@sanity/client/dist/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Server Component] + ./node_modules/next-sanity/dist/live.js [Server Component] + ./src/sanity/lib/live.ts [Server Component] + ./src/app/(portfolio)/[variant]/layout.tsx [Server Component] + + #2 [Client Component SSR]: + ./node_modules/@sanity/client/dist/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Server Component] + ./node_modules/next-sanity/dist/live.js [Server Component] + ./src/sanity/lib/live.ts [Server Component] + ./src/sanity/lib/queries.ts [Server Component] + ./src/app/not-found.tsx [Server Component] + +https://nextjs.org/docs/messages/module-not-found + + + +./node_modules/@sanity/comlink/dist/index.js:3:1 +Module not found: Can't resolve 'rxjs' +  1 | import { v4 } from "uuid"; +  2 | import { fromEventObservable, setup, sendTo, assign, fromCallback, createActor, enqueueActions, raise, emit, assertEvent, stopChild } from "xstate"; +> 3 | import { defer, fromEvent, map, pipe, filter, bufferCount, concatMap, take, EMPTY, takeUntil } from "rxjs"; +  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +  4 | const listenInputFromContext = (config) => ({ +  5 | context +  6 | }) => { + + + +Import traces: + #1 [Client Component Browser]: + ./node_modules/@sanity/comlink/dist/index.js [Client Component Browser] + ./node_modules/@sanity/presentation-comlink/dist/index.js [Client Component Browser] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Client Component Browser] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Server Component] + ./node_modules/next-sanity/dist/live.js [Server Component] + ./src/sanity/lib/live.ts [Server Component] + ./src/app/(portfolio)/[variant]/layout.tsx [Server Component] + + #2 [Client Component SSR]: + ./node_modules/@sanity/comlink/dist/index.js [Client Component SSR] + ./node_modules/@sanity/presentation-comlink/dist/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Server Component] + ./node_modules/next-sanity/dist/live.js [Server Component] + ./src/sanity/lib/live.ts [Server Component] + ./src/app/(portfolio)/[variant]/layout.tsx [Server Component] + + #3 [Client Component Browser]: + ./node_modules/@sanity/comlink/dist/index.js [Client Component Browser] + ./node_modules/@sanity/presentation-comlink/dist/index.js [Client Component Browser] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Client Component Browser] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Server Component] + ./node_modules/next-sanity/dist/live.js [Server Component] + ./src/sanity/lib/live.ts [Server Component] + ./src/sanity/lib/queries.ts [Server Component] + ./src/app/not-found.tsx [Server Component] + + #4 [Client Component SSR]: + ./node_modules/@sanity/comlink/dist/index.js [Client Component SSR] + ./node_modules/@sanity/presentation-comlink/dist/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Server Component] + ./node_modules/next-sanity/dist/live.js [Server Component] + ./src/sanity/lib/live.ts [Server Component] + ./src/sanity/lib/queries.ts [Server Component] + ./src/app/not-found.tsx [Server Component] + +https://nextjs.org/docs/messages/module-not-found + + + +./node_modules/next-sanity/node_modules/@sanity/mutate/dist/_unstable_machine.browser.js:4:1 +Module not found: Can't resolve 'rxjs' +  2 | import { groupBy } from "lodash-es"; +  3 | import { applyPatch } from "mendoza"; +> 4 | import { share, filter, merge, shareReplay, defer, observeOn, asapScheduler } from "rxjs"; +  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +  5 | import { setup, assign as assign$1, raise, spawnChild, stopChild, assertEvent, enqueueActions, sendParent, fromEventObservable, fromPromise } from "xstate"; +  6 | function getMutationDocumentId(mutation) { +  7 | if (mutation.type === "patch") + + + +Import trace: + Client Component Browser: + ./node_modules/next-sanity/node_modules/@sanity/mutate/dist/_unstable_machine.browser.js [Client Component Browser] + ./node_modules/next-sanity/node_modules/@sanity/visual-editing/dist/optimistic/index.js [Client Component Browser] + ./node_modules/next-sanity/node_modules/@sanity/visual-editing/dist/react/index.js [Client Component Browser] + ./node_modules/next-sanity/dist/VisualEditing.js [Client Component Browser] + ./node_modules/next-sanity/dist/visual-editing/client-component/index.js [Client Component Browser] + ./node_modules/next-sanity/dist/visual-editing/client-component/index.js [Server Component] + ./node_modules/next-sanity/dist/visual-editing/index.js [Server Component] + ./src/app/(portfolio)/[variant]/layout.tsx [Server Component] + +https://nextjs.org/docs/messages/module-not-found + + + +./node_modules/next-sanity/node_modules/@sanity/mutate/dist/_unstable_machine.js:3:1 +Module not found: Can't resolve 'rxjs' +  1 | import { rebase, applyMutations, commit, toTransactions, squashDMPStrings, squashMutationGroups } from "./_chunks-es/toTransactions.js"; +  2 | import { applyPatch } from "mendoza"; +> 3 | import { share, filter, merge, shareReplay, defer, observeOn, asapScheduler } from "rxjs"; +  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +  4 | import { setup, assign, raise, spawnChild, stopChild, assertEvent, enqueueActions, sendParent, fromEventObservable, fromPromise } from "xstate"; +  5 | import { encodeTransaction } from "./_chunks-es/encode.js"; +  6 | function createSharedListener(client) { + + + +Import trace: + Client Component SSR: + ./node_modules/next-sanity/node_modules/@sanity/mutate/dist/_unstable_machine.js [Client Component SSR] + ./node_modules/next-sanity/node_modules/@sanity/visual-editing/dist/optimistic/index.js [Client Component SSR] + ./node_modules/next-sanity/node_modules/@sanity/visual-editing/dist/react/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/VisualEditing.js [Client Component SSR] + ./node_modules/next-sanity/dist/visual-editing/client-component/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/visual-editing/client-component/index.js [Server Component] + ./node_modules/next-sanity/dist/visual-editing/index.js [Server Component] + ./src/app/(portfolio)/[variant]/layout.tsx [Server Component] + +https://nextjs.org/docs/messages/module-not-found + + + +./node_modules/next-sanity/node_modules/@sanity/visual-editing/dist/react/index.js:23:1 +Module not found: Can't resolve 'rxjs' +  21 | import { setActor } from "../_chunks-es/context.js"; +  22 | import { emptyActor } from "../_chunks-es/context.js"; +> 23 | import { ReplaySubject, Subject, merge } from "rxjs"; +  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +  24 | import { createDatasetMutator } from "../optimistic/index.js"; +  25 | import { createDocumentMutator } from "../optimistic/index.js"; +  26 | const History = (props) => { + + + +Import traces: + Client Component Browser: + ./node_modules/next-sanity/node_modules/@sanity/visual-editing/dist/react/index.js [Client Component Browser] + ./node_modules/next-sanity/dist/VisualEditing.js [Client Component Browser] + ./node_modules/next-sanity/dist/visual-editing/client-component/index.js [Client Component Browser] + ./node_modules/next-sanity/dist/visual-editing/client-component/index.js [Server Component] + ./node_modules/next-sanity/dist/visual-editing/index.js [Server Component] + ./src/app/(portfolio)/[variant]/layout.tsx [Server Component] + + Client Component SSR: + ./node_modules/next-sanity/node_modules/@sanity/visual-editing/dist/react/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/VisualEditing.js [Client Component SSR] + ./node_modules/next-sanity/dist/visual-editing/client-component/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/visual-editing/client-component/index.js [Server Component] + ./node_modules/next-sanity/dist/visual-editing/index.js [Server Component] + ./src/app/(portfolio)/[variant]/layout.tsx [Server Component] + +https://nextjs.org/docs/messages/module-not-found + + + +./node_modules/@sanity/client/dist/index.browser.js:6:1 +Module not found: Can't resolve 'rxjs/operators' +  4 | import { Observable, defer, of, isObservable, mergeMap, from, lastValueFrom, shareReplay, catchError, concat, throwError, timer, tap, finalize, share, merge, map as map$1, firstValueFrom } from "rxjs"; +  5 | import { isRecord, stegaClean } from "./_chunks-es/stegaClean.js"; +> 6 | import { combineLatestWith, map, filter, finalize as finalize$1 } from "rxjs/operators"; +  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +  7 | import { getVersionFromId, isDraftId, getVersionId, getDraftId, isVersionId, getPublishedId } from "@sanity/client/csm"; +  8 | import { customAlphabet } from "nanoid"; +  9 | const NEWLINE = /\r\n|[\n\r\u2028\u2029]/; + + + +Import traces: + #1 [Server Component]: + ./node_modules/@sanity/client/dist/index.browser.js + ./src/sanity/lib/client.ts + ./src/sanity/lib/queries.ts + ./src/app/(portfolio)/[variant]/layout.tsx + + #2 [Server Component]: + ./node_modules/@sanity/client/dist/index.browser.js + ./src/sanity/lib/client.ts + ./src/sanity/lib/queries.ts + ./src/app/not-found.tsx + + #3 [Client Component Browser]: + ./node_modules/@sanity/client/dist/index.browser.js [Client Component Browser] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Client Component Browser] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Server Component] + ./node_modules/next-sanity/dist/live.js [Server Component] + ./src/sanity/lib/live.ts [Server Component] + ./src/app/(portfolio)/[variant]/layout.tsx [Server Component] + + #4 [Client Component Browser]: + ./node_modules/@sanity/client/dist/index.browser.js [Client Component Browser] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Client Component Browser] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Server Component] + ./node_modules/next-sanity/dist/live.js [Server Component] + ./src/sanity/lib/live.ts [Server Component] + ./src/sanity/lib/queries.ts [Server Component] + ./src/app/not-found.tsx [Server Component] + +https://nextjs.org/docs/messages/module-not-found + + + +./node_modules/@sanity/client/dist/index.js:7:1 +Module not found: Can't resolve 'rxjs/operators' +  5 | import { isRecord } from "./_chunks-es/isRecord.js"; +  6 | import { stegaClean } from "./_chunks-es/stegaClean.js"; +> 7 | import { combineLatestWith, map, filter, finalize as finalize$1 } from "rxjs/operators"; +  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +  8 | import { getVersionFromId, isDraftId, getVersionId, getDraftId, isVersionId, getPublishedId } from "@sanity/client/csm"; +  9 | import { customAlphabet } from "nanoid"; +  10 | import { validateObject, validateInsert, requireDocumentId, validateDocumentId, requireDocumentType, printCreateVersionWithBaseIdWarning, resourceConfig, hasDataset, requestTag, printPreviewDraftsDeprecationWarning, validateApiPerspective, printCdnPreviewDraftsWarning, validateAssetType, resourceGuard, dataset, validateVersionIdMatch, defaultConfig, initConfig, printNoDefaultExport } from "./_chunks-es/config.js"; + + + +Import traces: + #1 [Client Component SSR]: + ./node_modules/@sanity/client/dist/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Server Component] + ./node_modules/next-sanity/dist/live.js [Server Component] + ./src/sanity/lib/live.ts [Server Component] + ./src/app/(portfolio)/[variant]/layout.tsx [Server Component] + + #2 [Client Component SSR]: + ./node_modules/@sanity/client/dist/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Client Component SSR] + ./node_modules/next-sanity/dist/live/client-components/live/index.js [Server Component] + ./node_modules/next-sanity/dist/live.js [Server Component] + ./src/sanity/lib/live.ts [Server Component] + ./src/sanity/lib/queries.ts [Server Component] + ./src/app/not-found.tsx [Server Component] + +https://nextjs.org/docs/messages/module-not-found + + + GET / 500 in 18.1s (compile: 17.7s, proxy.ts: 209ms, render: 249ms) diff --git a/package-lock.json b/package-lock.json index 9756616..ea9820f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "clsx": "^2.1.1", "modern-screenshot": "^4.6.8", "motion": "^12.23.24", - "next": "16.0.7", + "next": "^16.0.7", "next-mdx-remote": "^6.0.0", "next-sanity": "^11.4.2", "next-themes": "^0.4.6", @@ -17444,6 +17444,7 @@ "version": "16.0.7", "resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz", "integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", "license": "MIT", "dependencies": { "@next/env": "16.0.7", diff --git a/package.json b/package.json index 14bdde0..e119f65 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "clsx": "^2.1.1", "modern-screenshot": "^4.6.8", "motion": "^12.23.24", - "next": "16.0.7", + "next": "^16.0.7", "next-mdx-remote": "^6.0.0", "next-sanity": "^11.4.2", "next-themes": "^0.4.6", diff --git a/src/app/api/snapshot/route.ts b/src/app/api/snapshot/route.ts index 74e1c63..efaaca0 100644 --- a/src/app/api/snapshot/route.ts +++ b/src/app/api/snapshot/route.ts @@ -1,5 +1,4 @@ import { NextResponse } from "next/server"; -import puppeteer from "puppeteer-core"; import { puppeteerManager } from "@/utils/puppeteer-manager"; export const maxDuration = 60; @@ -31,67 +30,77 @@ export async function POST(req: Request) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - let browser: any = null; - let page: any = null; - try { - const { html, width, height, devicePixelRatio = 2 } = await req.json(); + const body = await req.json(); + const tasks = Array.isArray(body.tasks) ? body.tasks : [body]; - if (!html) { - return NextResponse.json({ error: "HTML content is required" }, { status: 400 }); + if (tasks.length === 0) { + return NextResponse.json({ error: "No tasks provided" }, { status: 400 }); } - // Connect to the persistent browser instance - const wsEndpoint = await puppeteerManager.getWsEndpoint(); - browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint }); - - page = await browser.newPage(); - - // Set viewport correctly for this specific snapshot - const safeWidth = Math.min(Math.max(width || 1280, 100), 3840); - const safeHeight = Math.min(Math.max(height || 720, 100), 2160); - const safeScale = Math.min(Math.max(devicePixelRatio || 2, 1), 3); - - await page.setViewport({ - width: safeWidth, - height: safeHeight, - deviceScaleFactor: safeScale, - }); - - // Performance: Disable JS - await page.setJavaScriptEnabled(false); - - // Wait for full load - await page.setContent(html, { waitUntil: "load" }); - - // Tiny delay for layout/font rendering - await new Promise(r => setTimeout(r, 100)); - - await page.evaluate(() => { - const htmlEl = document.documentElement; - const x = parseInt(htmlEl.getAttribute('data-scroll-x') || '0'); - const y = parseInt(htmlEl.getAttribute('data-scroll-y') || '0'); - window.scrollTo(x, y); - }); - - const buffer = await page.screenshot({ - type: "png", - fullPage: false, - }); + // Use the single, persistent browser instance + const browser = await puppeteerManager.getBrowser(); + + const snapshots = await Promise.all(tasks.map(async (task: any) => { + let page: any = null; + try { + const { html, width, height, devicePixelRatio = 2 } = task; + + if (!html) { + throw new Error("HTML content is required"); + } + + page = await browser.newPage(); + + // Set viewport correctly for this specific snapshot + const safeWidth = Math.min(Math.max(width || 1280, 100), 3840); + const safeHeight = Math.min(Math.max(height || 720, 100), 2160); + const safeScale = Math.min(Math.max(devicePixelRatio || 2, 1), 3); + + await page.setViewport({ + width: safeWidth, + height: safeHeight, + deviceScaleFactor: safeScale, + }); + + // Performance: Disable JS + await page.setJavaScriptEnabled(false); + + // Wait for full load + await page.setContent(html, { waitUntil: "load" }); + + // Tiny delay for layout/font rendering + await new Promise(r => setTimeout(r, 100)); + + await page.evaluate(() => { + const htmlEl = document.documentElement; + const x = parseInt(htmlEl.getAttribute('data-scroll-x') || '0'); + const y = parseInt(htmlEl.getAttribute('data-scroll-y') || '0'); + window.scrollTo(x, y); + }); + + const buffer = await page.screenshot({ + type: "png", + fullPage: false, + }); + + return `data:image/png;base64,${Buffer.from(buffer).toString("base64")}`; + } finally { + if (page) { + await page.close().catch(() => {}); + } + } + })); + + // If it was a single task (non-array input), return single snapshot for backward compatibility + if (!Array.isArray(body.tasks)) { + return NextResponse.json({ snapshot: snapshots[0] }); + } - const base64 = `data:image/png;base64,${buffer.toString("base64")}`; - return NextResponse.json({ snapshot: base64 }); + return NextResponse.json({ snapshots }); } catch (error: any) { console.error("Snapshot API error:", error); return NextResponse.json({ error: error.message }, { status: 500 }); - } finally { - if (page) { - await page.close().catch(() => {}); - } - if (browser) { - // We disconnect from the persistent browser, NOT close it - await browser.disconnect(); - } } } diff --git a/src/components/features/ThemeSwitcher/index.tsx b/src/components/features/ThemeSwitcher/index.tsx index ff287fd..5eae21a 100644 --- a/src/components/features/ThemeSwitcher/index.tsx +++ b/src/components/features/ThemeSwitcher/index.tsx @@ -16,7 +16,13 @@ type ThemeSwitcherProps = { }; const LoadingIcon = () => ( - + +); + +const SettingsIcon = () => ( + + + ); export function ThemeSwitcher({ @@ -25,9 +31,14 @@ export function ThemeSwitcher({ setWipeDirection, }: ThemeSwitcherProps) { const [mounted, setMounted] = useState(false); - + const [showDebug, setShowDebug] = useState(false); + const [forceFallback, setForceFallback] = useState({ + disablePuppeteer: false, + disableModernScreenshot: false, + }); + const { resolvedTheme } = useTheme(); - const { toggleTheme, snapshots, isCapturing, originalTheme, animationStyles } = useThemeWipe({ + const { toggleTheme, snapshots, isCapturing, isWarmingUp, originalTheme, animationStyles } = useThemeWipe({ wipeProgress, wipeDirection, setWipeDirection, @@ -35,10 +46,20 @@ export function ThemeSwitcher({ useEffect(() => { setMounted(true); - // Warm up the snapshot API on mount - fetch("/api/snapshot").catch(() => {}); + // Initialize global from state + if (typeof window !== "undefined") { + (window as any).FORCE_FALLBACK = forceFallback; + } }, []); + const updateFallback = (key: keyof typeof forceFallback, value: boolean) => { + const newState = { ...forceFallback, [key]: value }; + setForceFallback(newState); + if (typeof window !== "undefined") { + (window as any).FORCE_FALLBACK = newState; + } + }; + if (!mounted) { return ; } @@ -47,12 +68,49 @@ export function ThemeSwitcher({ return ( <> - +
+ + + {/* Debug Toggle Button */} + +
+ + {showDebug && createPortal( +
+

Debug Settings

+ + +
+ Current: {isWarmingUp ? 'Waking up...' : 'Ready'} +
+
, + document.body + )} {createPortal( ); -} \ No newline at end of file +} diff --git a/src/components/features/ThemeSwitcher/ui/WipeAnimationOverlay.tsx b/src/components/features/ThemeSwitcher/ui/WipeAnimationOverlay.tsx index 25cf973..53213aa 100644 --- a/src/components/features/ThemeSwitcher/ui/WipeAnimationOverlay.tsx +++ b/src/components/features/ThemeSwitcher/ui/WipeAnimationOverlay.tsx @@ -28,6 +28,11 @@ export function WipeAnimationOverlay({ className="fixed inset-0 z-10000" data-html2canvas-ignore="true" > + {/* Debug Method Text */} +
+ Method: {snapshots.method || 'unknown'} +
+ {/* Target Theme Snapshot (Bottom Layer - Revealed) */}
); -} \ No newline at end of file +} diff --git a/src/hooks/useThemeWipe.ts b/src/hooks/useThemeWipe.ts index 9fdb616..925a9a3 100644 --- a/src/hooks/useThemeWipe.ts +++ b/src/hooks/useThemeWipe.ts @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, Dispatch, SetStateAction } from "react"; +import { useState, useCallback, Dispatch, SetStateAction, useEffect } from "react"; import { useTheme } from "next-themes"; import { domToPng } from "modern-screenshot"; import { getFullPageHTML } from "@/utils/dom-serializer"; @@ -8,6 +8,14 @@ import { useWipeAnimation } from "@/hooks/useWipeAnimation"; import { Theme, WipeDirection } from "@/components/features/ThemeSwitcher/types"; import type { MotionValue } from "motion/react"; +// Global for developer overrides +if (typeof window !== "undefined") { + (window as any).FORCE_FALLBACK = (window as any).FORCE_FALLBACK || { + disablePuppeteer: false, + disableModernScreenshot: false, + }; +} + type UseThemeWipeProps = { wipeProgress: MotionValue; wipeDirection: WipeDirection | null; @@ -17,6 +25,7 @@ type UseThemeWipeProps = { export type Snapshots = { a: string; // Original theme b: string; // Target theme + method?: string; // Debug info }; export function useThemeWipe({ @@ -27,9 +36,17 @@ export function useThemeWipe({ const { setTheme, resolvedTheme } = useTheme(); const [snapshots, setSnapshots] = useState(null); const [isCapturing, setIsCapturing] = useState(false); + const [isWarmingUp, setIsWarmingUp] = useState(true); const [animationTargetTheme, setAnimationTargetTheme] = useState(null); const [originalTheme, setOriginalTheme] = useState(null); + useEffect(() => { + // Check warm-up status + fetch("/api/snapshot") + .then(() => setIsWarmingUp(false)) + .catch(() => setIsWarmingUp(false)); + }, []); + const setScrollLock = (isLocked: boolean) => { document.documentElement.style.overflow = isLocked ? 'hidden' : ''; document.documentElement.style.scrollbarGutter = isLocked ? 'stable' : ''; @@ -69,6 +86,8 @@ export function useThemeWipe({ }); const toggleTheme = useCallback(async () => { + if (isWarmingUp) return; + const currentTheme = resolvedTheme as Theme; const newTheme: Theme = currentTheme === "dark" ? "light" : "dark"; @@ -88,20 +107,36 @@ export function useThemeWipe({ const direction: WipeDirection = currentTheme === "dark" ? "bottom-up" : "top-down"; - const fetchSnapshot = async (themeOverride?: "light" | "dark") => { - const html = getFullPageHTML(themeOverride); + const fetchBatchedSnapshots = async (): Promise<[string, string]> => { + const htmlA = getFullPageHTML(); + const htmlB = getFullPageHTML(newTheme); + + const payload = { + tasks: [ + { + html: htmlA, + width: window.innerWidth, + height: window.innerHeight, + devicePixelRatio: window.devicePixelRatio, + }, + { + html: htmlB, + width: window.innerWidth, + height: window.innerHeight, + devicePixelRatio: window.devicePixelRatio, + } + ] + }; + const response = await fetch("/api/snapshot", { method: "POST", - body: JSON.stringify({ - html, - width: window.innerWidth, - height: window.innerHeight, - devicePixelRatio: window.devicePixelRatio, - }), + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), }); + const data = await response.json(); if (data.error) throw new Error(data.error); - return data.snapshot; + return data.snapshots as [string, string]; }; const captureWithModernScreenshot = async (): Promise => { @@ -130,7 +165,7 @@ export function useThemeWipe({ const a = await domToPng(document.documentElement, options); // Mask switch - setSnapshots({ a, b: a }); + setSnapshots({ a, b: a, method: 'modern-screenshot' }); await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))); // 2. Switch theme @@ -139,7 +174,7 @@ export function useThemeWipe({ // 3. Snapshot B (new) const b = await domToPng(document.documentElement, options); - return { a, b }; + return { a, b, method: 'modern-screenshot' }; }; const withTimeout = (promise: Promise, ms: number, errorMsg: string) => { @@ -149,28 +184,34 @@ export function useThemeWipe({ ]); }; + const forceFallback = (window as any).FORCE_FALLBACK || {}; + try { - // PHASE 1: Try Puppeteer (3s timeout) - console.log("Attempting Puppeteer snapshot..."); + // PHASE 1: Try Puppeteer + if (forceFallback.disablePuppeteer) throw new Error("Puppeteer manually disabled"); + + console.log("Attempting Puppeteer batched snapshots..."); const [snapshotA, snapshotB] = await withTimeout( - Promise.all([fetchSnapshot(), fetchSnapshot(newTheme)]), - 3000, + fetchBatchedSnapshots(), + 10000, // Increased timeout to 10s for batched "Puppeteer timeout" - ) as [string, string]; + ); - setSnapshots({ a: snapshotA, b: snapshotB }); + setSnapshots({ a: snapshotA, b: snapshotB, method: 'puppeteer' }); await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))); setTheme(newTheme); setWipeDirection(direction); } catch (e: any) { - console.warn("Puppeteer failed or timed out, falling back to modern-screenshot:", e.message); + console.warn("Puppeteer failed, falling back to modern-screenshot:", e.message); try { - // PHASE 2: Try modern-screenshot (2s timeout) + // PHASE 2: Try modern-screenshot + if (forceFallback.disableModernScreenshot) throw new Error("Modern-screenshot manually disabled"); + const snapshots = await withTimeout( captureWithModernScreenshot(), - 2000, + 7000, // Increased timeout "modern-screenshot timeout" ) as Snapshots; @@ -178,7 +219,7 @@ export function useThemeWipe({ setWipeDirection(direction); } catch (e2: any) { - console.warn("modern-screenshot failed or timed out, changing theme instantly:", e2.message); + console.warn("modern-screenshot failed, changing theme instantly:", e2.message); // PHASE 3: Fallback instantly setTheme(newTheme); @@ -193,12 +234,13 @@ export function useThemeWipe({ setIsCapturing(false); document.documentElement.classList.remove('disable-transitions'); } - }, [snapshots, isCapturing, resolvedTheme, setTheme, setWipeDirection, animationTargetTheme, wipeProgress]); + }, [snapshots, isCapturing, isWarmingUp, resolvedTheme, setTheme, setWipeDirection, animationTargetTheme, wipeProgress]); return { toggleTheme, snapshots, isCapturing, + isWarmingUp, originalTheme, animationStyles, };