next.js/packages/next/src/next-devtools/dev-overlay/components/instant-navs/instant-navs-panel.tsx
instant-navs-panel.tsx271 lines8.6 KB
import { useEffect, useState } from 'react'
import { useDevOverlayContext } from '../../../dev-overlay.browser'
import { ACTION_INSTANT_NAVS_RESET } from '../../shared'
import {
  useInstantNavCookieState,
  formatRoutePattern,
} from './instant-nav-cookie'
import './instant-navs-panel.css'

const COOKIE_NAME = 'next-instant-navigation-testing'

export function InstantNavsPanel() {
  const { state, dispatch } = useDevOverlayContext()

  // The cookie is the sole source of truth for the instant navigation
  // state, including the from-route URL for SPA captures.
  const cookieData = useInstantNavCookieState()

  // Whether the user has clicked "Start" to begin capturing a client
  // navigation. This is UI-only state — it controls which panel view
  // is shown while waiting for a navigation to occur. Once a capture
  // completes (state becomes 'spa' or 'mpa'), the waiting state
  // is no longer relevant.
  const [isWaitingForClientNav, setIsWaitingForClientNav] = useState(false)
  if (
    isWaitingForClientNav &&
    cookieData !== null &&
    (cookieData.state === 'spa' || cookieData.state === 'mpa')
  ) {
    setIsWaitingForClientNav(false)
  }

  // Cleanup on unmount: clear cookie and close the panel.
  useEffect(() => {
    return () => {
      if (typeof cookieStore !== 'undefined') {
        cookieStore.delete(COOKIE_NAME)
      }
      dispatch({ type: ACTION_INSTANT_NAVS_RESET })
    }
  }, [dispatch])

  function handleReload() {
    if (typeof cookieStore !== 'undefined') {
      cookieStore.set({
        name: COOKIE_NAME,
        value: JSON.stringify([0, `p${Math.random()}`]),
        path: '/',
      })
    }
    window.location.reload()
  }

  function handleStartClientNav() {
    if (typeof cookieStore !== 'undefined') {
      cookieStore.set({
        name: COOKIE_NAME,
        value: JSON.stringify([0, `p${Math.random()}`]),
        path: '/',
      })
    }
    setIsWaitingForClientNav(true)
  }

  function handleContinueRendering() {
    // Delete the cookie to release the lock. The CookieStore change
    // event triggers refreshOnInstantNavigationUnlock which does a
    // soft refresh to fetch dynamic data. The panel stays open so
    // the user can start another capture.
    if (typeof cookieStore !== 'undefined') {
      cookieStore.delete(COOKIE_NAME)
    }
  }

  function getShareUrl(): string {
    const url = new URL(state.page, window.location.origin)
    url.searchParams.set('__instant_nav', '1')
    if (cookieData !== null && cookieData.state === 'spa') {
      url.searchParams.set('from', formatRoutePattern(cookieData.fromTree))
      if (cookieData.toTree !== null) {
        url.searchParams.set('to', formatRoutePattern(cookieData.toTree))
      }
    }
    return url.toString()
  }

  // Derive the panel view from the cookie state.
  if (cookieData !== null && cookieData.state === 'spa') {
    const isRendering = state.renderingIndicator
    return (
      <div className="instant-nav-panel">
        <div className="instant-nav-content">
          <div className="instant-nav-section-header">
            <label>Client navigation</label>
          </div>
          <div className="instant-nav-urls">
            <div className="instant-nav-url-row">
              <span className="instant-nav-url-label">From:</span>
              <span className="instant-nav-url-value">
                {formatRoutePattern(cookieData.fromTree)}
              </span>
            </div>
            <div className="instant-nav-url-row">
              <span className="instant-nav-url-label">To:</span>
              <span className="instant-nav-url-value">
                {cookieData.toTree !== null && !isRendering ? (
                  formatRoutePattern(cookieData.toTree)
                ) : (
                  <span className="instant-nav-skeleton" />
                )}
              </span>
            </div>
          </div>
          <p className="instant-nav-helper-description">
            You're viewing the prefetched UI for the previous navigation to the
            current URL.
          </p>
        </div>
        <div className="instant-nav-footer">
          <span style={{ display: 'none' }}>
            <ShareButton getShareUrl={getShareUrl} />
          </span>
          <button
            className="instant-nav-footer-button"
            onClick={handleContinueRendering}
            type="button"
          >
            Continue rendering
          </button>
        </div>
      </div>
    )
  }

  if (cookieData !== null && cookieData.state === 'mpa') {
    return (
      <div className="instant-nav-panel">
        <div className="instant-nav-content">
          <div className="instant-nav-section-header">
            <label>Page load</label>
          </div>
          <div className="instant-nav-urls">
            <div className="instant-nav-url-row">
              <span className="instant-nav-url-label">Route:</span>
              <span className="instant-nav-url-value">
                {state.tree !== null && !state.renderingIndicator ? (
                  formatRoutePattern(state.tree)
                ) : (
                  <span className="instant-nav-skeleton" />
                )}
              </span>
            </div>
          </div>
          <p className="instant-nav-helper-description">
            You're viewing the pre-rendered static UI for the current URL.
          </p>
        </div>
        <div className="instant-nav-footer">
          <span style={{ display: 'none' }}>
            <ShareButton getShareUrl={getShareUrl} />
          </span>
          <button
            className="instant-nav-footer-button"
            onClick={handleContinueRendering}
            type="button"
          >
            Continue rendering
          </button>
        </div>
      </div>
    )
  }

  if (isWaitingForClientNav) {
    return (
      <div className="instant-nav-panel">
        <div className="instant-nav-content">
          <div className="instant-nav-section-header">
            <label>Client navigation</label>
          </div>
          <p className="instant-nav-helper-description">
            Click any link in your app to view the prefetched UI for that page.
          </p>
        </div>
      </div>
    )
  }

  // Default: no cookie or pending — show the action buttons.
  return (
    <div className="instant-nav-panel">
      <div className="instant-nav-section">
        <div className="instant-nav-section-header">
          <label>Page load</label>
          <p className="instant-nav-section-description">
            View the initial static UI for this page.
          </p>
        </div>
        <div className="instant-nav-section-control">
          <button
            className="action-button"
            onClick={handleReload}
            data-instant-nav-refresh
          >
            <RotateClockwise />
            <span>Reload</span>
          </button>
        </div>
      </div>
      <div className="instant-nav-section">
        <div className="instant-nav-section-header">
          <label>Client navigation</label>
          <p className="instant-nav-section-description">
            Freeze the next navigation to view the prefetched UI.
          </p>
        </div>
        <div className="instant-nav-section-control">
          <button
            className="action-button"
            onClick={handleStartClientNav}
            data-instant-nav-client
          >
            <span>Start</span>
          </button>
        </div>
      </div>
    </div>
  )
}

function ShareButton({ getShareUrl }: { getShareUrl: () => string }) {
  const [copied, setCopied] = useState(false)

  function handleClick() {
    navigator.clipboard.writeText(getShareUrl()).then(() => {
      setCopied(true)
      setTimeout(() => setCopied(false), 2000)
    })
  }

  return (
    <button
      className="instant-nav-footer-button"
      onClick={handleClick}
      type="button"
      data-instant-nav-share
    >
      {copied ? 'Copied!' : 'Share'}
    </button>
  )
}

function RotateClockwise() {
  return (
    <svg
      width="14"
      height="14"
      strokeLinejoin="round"
      viewBox="0 0 16 16"
      fill="none"
    >
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        d="M2.5 8C2.5 4.96643 4.97431 2.5 8.03548 2.5C10.5716 2.5 12.7064 4.19393 13.3628 6.5H10.75H10V8H10.75H15.25C15.6642 8 16 7.66421 16 7.25V2.75V2H14.5V2.75V5.23347C13.4215 2.74164 10.9316 1 8.03548 1C4.1539 1 1 4.13001 1 8C1 11.87 4.1539 15 8.03548 15C10.3763 15 12.4513 13.8617 13.7295 12.1122L14.172 11.5066L12.9609 10.6217L12.5184 11.2273C11.5117 12.6051 9.87945 13.5 8.03548 13.5C4.97431 13.5 2.5 11.0336 2.5 8Z"
        fill="currentColor"
      />
    </svg>
  )
}
Quest for Codev2.0.0
/
SIGN IN