Extract anyclip

This commit is contained in:
2026-01-20 14:34:18 +08:00
commit d4fe4800e6
1014 changed files with 97445 additions and 0 deletions

55
README.md Normal file
View File

@@ -0,0 +1,55 @@
# AnyClip Video Manager - Extracted Source
Source code extracted from sourcemaps of `videomanager.anyclip.com`.
## Overview
Next.js application for video content management, analytics, and publishing.
## Structure
```
├── src/
│ ├── modules/ # Feature modules (business logic)
│ ├── pages/ # Next.js page components
│ ├── client/ # Client-side utilities
│ ├── shared/ # Shared libraries
│ └── assets/
├── pages/ # Root Next.js pages (_app.tsx, _error.tsx)
├── client/ # Next.js client runtime
├── vendor/ # Bundled node_modules
└── sourcemaps/ # Original .map files
```
## Modules (`src/modules/`)
| Module | Description |
|--------|-------------|
| `analytics/` | Dashboards - monetization, video performance, custom reports |
| `editorial/` | Video editing - tagging, search, bulk actions, video details |
| `publishing/` | Content publishing and destination management |
| `marketplace/` | Marketplace accounts and dashboard |
| `xRay/` | X-Ray - campaigns, creatives, line items |
| `hubs/` | Content hubs management |
| `users/` | User management |
| `invitations/` | User invitation system |
| `forms/` | Form builder/management |
| `uploaderNew/` | Video upload functionality |
| `userRulesSettings/` | User rules and settings |
| `layout/` | App layout and Redux state |
| `common/` | Shared components - forms, tables, lists, tag selectors |
## Pages (`src/pages/`)
- `/analytics` - Analytics dashboard
- `/studio` - Studio interface
- `/personal-settings` - User settings
- `/hubs`, `/users`, `/invitations`, `/forms` - Management pages
- `/x-ray/*` - Campaign, creative, and line item analytics
## Tech Stack
- Next.js, React, TypeScript
- Redux (state management)
- Material-UI (components)
- Victory/D3 (charts)

12
client/add-base-path.ts Normal file
View File

@@ -0,0 +1,12 @@
import { addPathPrefix } from '../shared/lib/router/utils/add-path-prefix'
import { normalizePathTrailingSlash } from './normalize-trailing-slash'
const basePath = (process.env.__NEXT_ROUTER_BASEPATH as string) || ''
export function addBasePath(path: string, required?: boolean): string {
return normalizePathTrailingSlash(
process.env.__NEXT_MANUAL_CLIENT_BASE_PATH && !required
? path
: addPathPrefix(path, basePath)
)
}

13
client/add-locale.ts Normal file
View File

@@ -0,0 +1,13 @@
import type { addLocale as Fn } from '../shared/lib/router/utils/add-locale'
import { normalizePathTrailingSlash } from './normalize-trailing-slash'
export const addLocale: typeof Fn = (path, ...args) => {
if (process.env.__NEXT_I18N_SUPPORT) {
return normalizePathTrailingSlash(
(
require('../shared/lib/router/utils/add-locale') as typeof import('../shared/lib/router/utils/add-locale')
).addLocale(path, ...args)
)
}
return path
}

View File

@@ -0,0 +1,9 @@
import type { detectDomainLocale as Fn } from '../shared/lib/i18n/detect-domain-locale'
export const detectDomainLocale: typeof Fn = (...args) => {
if (process.env.__NEXT_I18N_SUPPORT) {
return (
require('../shared/lib/i18n/detect-domain-locale') as typeof import('../shared/lib/i18n/detect-domain-locale')
).detectDomainLocale(...args)
}
}

7
client/has-base-path.ts Normal file
View File

@@ -0,0 +1,7 @@
import { pathHasPrefix } from '../shared/lib/router/utils/path-has-prefix'
const basePath = (process.env.__NEXT_ROUTER_BASEPATH as string) || ''
export function hasBasePath(path: string): boolean {
return pathHasPrefix(path, basePath)
}

150
client/head-manager.ts Normal file
View File

@@ -0,0 +1,150 @@
import { setAttributesFromProps } from './set-attributes-from-props'
import type { JSX } from 'react'
function reactElementToDOM({ type, props }: JSX.Element): HTMLElement {
const el: HTMLElement = document.createElement(type)
setAttributesFromProps(el, props)
const { children, dangerouslySetInnerHTML } = props
if (dangerouslySetInnerHTML) {
el.innerHTML = dangerouslySetInnerHTML.__html || ''
} else if (children) {
el.textContent =
typeof children === 'string'
? children
: Array.isArray(children)
? children.join('')
: ''
}
return el
}
/**
* When a `nonce` is present on an element, browsers such as Chrome and Firefox strip it out of the
* actual HTML attributes for security reasons *when the element is added to the document*. Thus,
* given two equivalent elements that have nonces, `Element,isEqualNode()` will return false if one
* of those elements gets added to the document. Although the `element.nonce` property will be the
* same for both elements, the one that was added to the document will return an empty string for
* its nonce HTML attribute value.
*
* This custom `isEqualNode()` function therefore removes the nonce value from the `newTag` before
* comparing it to `oldTag`, restoring it afterwards.
*
* For more information, see:
* https://bugs.chromium.org/p/chromium/issues/detail?id=1211471#c12
*/
export function isEqualNode(oldTag: Element, newTag: Element) {
if (oldTag instanceof HTMLElement && newTag instanceof HTMLElement) {
const nonce = newTag.getAttribute('nonce')
// Only strip the nonce if `oldTag` has had it stripped. An element's nonce attribute will not
// be stripped if there is no content security policy response header that includes a nonce.
if (nonce && !oldTag.getAttribute('nonce')) {
const cloneTag = newTag.cloneNode(true) as typeof newTag
cloneTag.setAttribute('nonce', '')
cloneTag.nonce = nonce
return nonce === oldTag.nonce && oldTag.isEqualNode(cloneTag)
}
}
return oldTag.isEqualNode(newTag)
}
function updateElements(type: string, components: JSX.Element[]) {
const headEl = document.querySelector('head')
if (!headEl) return
const oldTags = new Set(headEl.querySelectorAll(`${type}[data-next-head]`))
if (type === 'meta') {
const metaCharset = headEl.querySelector('meta[charset]')
if (metaCharset !== null) {
oldTags.add(metaCharset)
}
}
const newTags: Element[] = []
for (let i = 0; i < components.length; i++) {
const component = components[i]
const newTag = reactElementToDOM(component)
newTag.setAttribute('data-next-head', '')
let isNew = true
for (const oldTag of oldTags) {
if (isEqualNode(oldTag, newTag)) {
oldTags.delete(oldTag)
isNew = false
break
}
}
if (isNew) {
newTags.push(newTag)
}
}
for (const oldTag of oldTags) {
oldTag.parentNode?.removeChild(oldTag)
}
for (const newTag of newTags) {
// meta[charset] must be first element so special case
if (
newTag.tagName.toLowerCase() === 'meta' &&
newTag.getAttribute('charset') !== null
) {
headEl.prepend(newTag)
}
headEl.appendChild(newTag)
}
}
export default function initHeadManager(): {
mountedInstances: Set<unknown>
updateHead: (head: JSX.Element[]) => void
} {
return {
mountedInstances: new Set(),
updateHead: (head: JSX.Element[]) => {
const tags: Record<string, JSX.Element[]> = {}
head.forEach((h) => {
if (
// If the font tag is loaded only on client navigation
// it won't be inlined. In this case revert to the original behavior
h.type === 'link' &&
h.props['data-optimized-fonts']
) {
if (
document.querySelector(`style[data-href="${h.props['data-href']}"]`)
) {
return
} else {
h.props.href = h.props['data-href']
h.props['data-href'] = undefined
}
}
const components = tags[h.type] || []
components.push(h)
tags[h.type] = components
})
const titleComponent = tags.title ? tags.title[0] : null
let title = ''
if (titleComponent) {
const { children } = titleComponent.props
title =
typeof children === 'string'
? children
: Array.isArray(children)
? children.join('')
: ''
}
if (title !== document.title) document.title = title
;['meta', 'base', 'link', 'style', 'script'].forEach((type) => {
updateElements(type, tags[type] || [])
})
},
}
}

1008
client/index.tsx Normal file

File diff suppressed because it is too large Load Diff

23
client/next.ts Normal file
View File

@@ -0,0 +1,23 @@
import './webpack'
import '../lib/require-instrumentation-client'
import { initialize, hydrate, version, router, emitter } from './'
declare global {
interface Window {
next: any
}
}
window.next = {
version,
// router is initialized later so it has to be live-binded
get router() {
return router
},
emitter,
}
initialize({})
.then(() => hydrate())
.catch(console.error)

View File

@@ -0,0 +1,25 @@
import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash'
import { parsePath } from '../shared/lib/router/utils/parse-path'
/**
* Normalizes the trailing slash of a path according to the `trailingSlash` option
* in `next.config.js`.
*/
export const normalizePathTrailingSlash = (path: string) => {
if (!path.startsWith('/') || process.env.__NEXT_MANUAL_TRAILING_SLASH) {
return path
}
const { pathname, query, hash } = parsePath(path)
if (process.env.__NEXT_TRAILING_SLASH) {
if (/\.[^/]+\/?$/.test(pathname)) {
return `${removeTrailingSlash(pathname)}${query}${hash}`
} else if (pathname.endsWith('/')) {
return `${pathname}${query}${hash}`
} else {
return `${pathname}/${query}${hash}`
}
}
return `${removeTrailingSlash(pathname)}${query}${hash}`
}

210
client/page-loader.ts Normal file
View File

@@ -0,0 +1,210 @@
import type { ComponentType } from 'react'
import type { RouteLoader } from './route-loader'
import type { MiddlewareMatcher } from '../build/analysis/get-page-static-info'
import { addBasePath } from './add-base-path'
import { interpolateAs } from '../shared/lib/router/utils/interpolate-as'
import getAssetPathFromRoute from '../shared/lib/router/utils/get-asset-path-from-route'
import { addLocale } from './add-locale'
import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic'
import { parseRelativeUrl } from '../shared/lib/router/utils/parse-relative-url'
import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash'
import { createRouteLoader, getClientBuildManifest } from './route-loader'
import {
DEV_CLIENT_PAGES_MANIFEST,
DEV_CLIENT_MIDDLEWARE_MANIFEST,
TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST,
} from '../shared/lib/constants'
declare global {
interface Window {
__DEV_MIDDLEWARE_MATCHERS?: MiddlewareMatcher[]
__DEV_PAGES_MANIFEST?: { pages: string[] }
__SSG_MANIFEST_CB?: () => void
__SSG_MANIFEST?: Set<string>
}
}
export type StyleSheetTuple = { href: string; text: string }
export type GoodPageCache = {
page: ComponentType
mod: any
styleSheets: StyleSheetTuple[]
}
export default class PageLoader {
private buildId: string
private assetPrefix: string
private promisedSsgManifest: Promise<Set<string>>
private promisedDevPagesManifest?: Promise<string[]>
private promisedMiddlewareMatchers?: Promise<MiddlewareMatcher[]>
public routeLoader: RouteLoader
constructor(buildId: string, assetPrefix: string) {
this.routeLoader = createRouteLoader(assetPrefix)
this.buildId = buildId
this.assetPrefix = assetPrefix
this.promisedSsgManifest = new Promise((resolve) => {
if (window.__SSG_MANIFEST) {
resolve(window.__SSG_MANIFEST)
} else {
window.__SSG_MANIFEST_CB = () => {
resolve(window.__SSG_MANIFEST!)
}
}
})
}
getPageList() {
if (process.env.NODE_ENV === 'production') {
return getClientBuildManifest().then((manifest) => manifest.sortedPages)
} else {
if (window.__DEV_PAGES_MANIFEST) {
return window.__DEV_PAGES_MANIFEST.pages
} else {
this.promisedDevPagesManifest ||= fetch(
`${this.assetPrefix}/_next/static/development/${DEV_CLIENT_PAGES_MANIFEST}`,
{ credentials: 'same-origin' }
)
.then((res) => res.json())
.then((manifest: { pages: string[] }) => {
window.__DEV_PAGES_MANIFEST = manifest
return manifest.pages
})
.catch((err) => {
console.log(`Failed to fetch devPagesManifest:`, err)
throw new Error(
`Failed to fetch _devPagesManifest.json. Is something blocking that network request?\n` +
'Read more: https://nextjs.org/docs/messages/failed-to-fetch-devpagesmanifest'
)
})
return this.promisedDevPagesManifest
}
}
}
getMiddleware() {
// Webpack production
if (
process.env.NODE_ENV === 'production' &&
process.env.__NEXT_MIDDLEWARE_MATCHERS
) {
const middlewareMatchers = process.env.__NEXT_MIDDLEWARE_MATCHERS
window.__MIDDLEWARE_MATCHERS = middlewareMatchers
? (middlewareMatchers as any as MiddlewareMatcher[])
: undefined
return window.__MIDDLEWARE_MATCHERS
// Turbopack production
} else if (process.env.NODE_ENV === 'production') {
if (window.__MIDDLEWARE_MATCHERS) {
return window.__MIDDLEWARE_MATCHERS
} else {
if (!this.promisedMiddlewareMatchers) {
// TODO: Decide what should happen when fetching fails instead of asserting
// @ts-ignore
this.promisedMiddlewareMatchers = fetch(
`${this.assetPrefix}/_next/static/${this.buildId}/${TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST}`,
{ credentials: 'same-origin' }
)
.then((res) => res.json())
.then((matchers: MiddlewareMatcher[]) => {
window.__MIDDLEWARE_MATCHERS = matchers
return matchers
})
.catch((err) => {
console.log(`Failed to fetch _devMiddlewareManifest`, err)
})
}
// TODO Remove this assertion as this could be undefined
return this.promisedMiddlewareMatchers!
}
// Development both Turbopack and Webpack
} else {
if (window.__DEV_MIDDLEWARE_MATCHERS) {
return window.__DEV_MIDDLEWARE_MATCHERS
} else {
if (!this.promisedMiddlewareMatchers) {
// TODO: Decide what should happen when fetching fails instead of asserting
// @ts-ignore
this.promisedMiddlewareMatchers = fetch(
`${this.assetPrefix}/_next/static/${this.buildId}/${DEV_CLIENT_MIDDLEWARE_MANIFEST}`,
{ credentials: 'same-origin' }
)
.then((res) => res.json())
.then((matchers: MiddlewareMatcher[]) => {
window.__DEV_MIDDLEWARE_MATCHERS = matchers
return matchers
})
.catch((err) => {
console.log(`Failed to fetch _devMiddlewareManifest`, err)
})
}
// TODO Remove this assertion as this could be undefined
return this.promisedMiddlewareMatchers!
}
}
}
getDataHref(params: {
asPath: string
href: string
locale?: string | false
skipInterpolation?: boolean
}): string {
const { asPath, href, locale } = params
const { pathname: hrefPathname, query, search } = parseRelativeUrl(href)
const { pathname: asPathname } = parseRelativeUrl(asPath)
const route = removeTrailingSlash(hrefPathname)
if (route[0] !== '/') {
throw new Error(`Route name should start with a "/", got "${route}"`)
}
const getHrefForSlug = (path: string) => {
const dataRoute = getAssetPathFromRoute(
removeTrailingSlash(addLocale(path, locale)),
'.json'
)
return addBasePath(
`/_next/data/${this.buildId}${dataRoute}${search}`,
true
)
}
return getHrefForSlug(
params.skipInterpolation
? asPathname
: isDynamicRoute(route)
? interpolateAs(hrefPathname, asPathname, query).result
: route
)
}
_isSsg(
/** the route (file-system path) */
route: string
): Promise<boolean> {
return this.promisedSsgManifest.then((manifest) => manifest.has(route))
}
loadPage(route: string): Promise<GoodPageCache> {
return this.routeLoader.loadRoute(route).then((res) => {
if ('component' in res) {
return {
page: res.component,
mod: res.exports,
styleSheets: res.styles.map((o) => ({
href: o.href,
text: o.content,
})),
}
}
throw res.error
})
}
prefetch(route: string): Promise<void> {
return this.routeLoader.prefetch(route)
}
}

View File

@@ -0,0 +1,18 @@
import { hasBasePath } from './has-base-path'
const basePath = (process.env.__NEXT_ROUTER_BASEPATH as string) || ''
export function removeBasePath(path: string): string {
if (process.env.__NEXT_MANUAL_CLIENT_BASE_PATH) {
if (!hasBasePath(path)) {
return path
}
}
// Can't trim the basePath if it has zero length!
if (basePath.length === 0) return path
path = path.slice(basePath.length)
if (!path.startsWith('/')) path = `/${path}`
return path
}

18
client/remove-locale.ts Normal file
View File

@@ -0,0 +1,18 @@
import { parsePath } from '../shared/lib/router/utils/parse-path'
export function removeLocale(path: string, locale?: string) {
if (process.env.__NEXT_I18N_SUPPORT) {
const { pathname } = parsePath(path)
const pathLower = pathname.toLowerCase()
const localeLower = locale?.toLowerCase()
return locale &&
(pathLower.startsWith(`/${localeLower}/`) ||
pathLower === `/${localeLower}`)
? `${pathname.length === locale.length + 1 ? `/` : ``}${path.slice(
locale.length + 1
)}`
: path
}
return path
}

View File

@@ -0,0 +1,23 @@
export const requestIdleCallback =
(typeof self !== 'undefined' &&
self.requestIdleCallback &&
self.requestIdleCallback.bind(window)) ||
function (cb: IdleRequestCallback): number {
let start = Date.now()
return self.setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start))
},
})
}, 1)
}
export const cancelIdleCallback =
(typeof self !== 'undefined' &&
self.cancelIdleCallback &&
self.cancelIdleCallback.bind(window)) ||
function (id: number) {
return clearTimeout(id)
}

138
client/resolve-href.ts Normal file
View File

@@ -0,0 +1,138 @@
import type { NextRouter, Url } from '../shared/lib/router/router'
import { searchParamsToUrlQuery } from '../shared/lib/router/utils/querystring'
import { formatWithValidation } from '../shared/lib/router/utils/format-url'
import { omit } from '../shared/lib/router/utils/omit'
import { normalizeRepeatedSlashes } from '../shared/lib/utils'
import { normalizePathTrailingSlash } from './normalize-trailing-slash'
import { isLocalURL } from '../shared/lib/router/utils/is-local-url'
import { isDynamicRoute } from '../shared/lib/router/utils'
import { interpolateAs } from '../shared/lib/router/utils/interpolate-as'
import { getRouteRegex } from '../shared/lib/router/utils/route-regex'
import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher'
/**
* Resolves a given hyperlink with a certain router state (basePath not included).
* Preserves absolute urls.
*/
export function resolveHref(
router: NextRouter,
href: Url,
resolveAs: true
): [string, string] | [string]
export function resolveHref(
router: NextRouter,
href: Url,
resolveAs?: false
): string
export function resolveHref(
router: NextRouter,
href: Url,
resolveAs?: boolean
): [string, string] | [string] | string {
// we use a dummy base url for relative urls
let base: URL
let urlAsString = typeof href === 'string' ? href : formatWithValidation(href)
// repeated slashes and backslashes in the URL are considered
// invalid and will never match a Next.js page/file
// https://www.rfc-editor.org/rfc/rfc3986.html#section-3.1
const urlProtoMatch = urlAsString.match(/^[a-z][a-z0-9+.-]*:\/\//i)
const urlAsStringNoProto = urlProtoMatch
? urlAsString.slice(urlProtoMatch[0].length)
: urlAsString
const urlParts = urlAsStringNoProto.split('?', 1)
if ((urlParts[0] || '').match(/(\/\/|\\)/)) {
console.error(
`Invalid href '${urlAsString}' passed to next/router in page: '${router.pathname}'. Repeated forward-slashes (//) or backslashes \\ are not valid in the href.`
)
const normalizedUrl = normalizeRepeatedSlashes(urlAsStringNoProto)
urlAsString = (urlProtoMatch ? urlProtoMatch[0] : '') + normalizedUrl
}
// Return because it cannot be routed by the Next.js router
if (!isLocalURL(urlAsString)) {
return (resolveAs ? [urlAsString] : urlAsString) as string
}
try {
let baseBase = urlAsString.startsWith('#') ? router.asPath : router.pathname
// If the provided href is only a query string, it is safer to use the asPath
// considering rewrites.
if (urlAsString.startsWith('?')) {
baseBase = router.asPath
// However, if is a dynamic route, we need to use the pathname to preserve the
// query interpolation and rewrites (router.pathname will look like "/[slug]").
if (isDynamicRoute(router.pathname)) {
baseBase = router.pathname
const routeRegex = getRouteRegex(router.pathname)
const match = getRouteMatcher(routeRegex)(router.asPath)
// For dynamic routes, if asPath doesn't match the pathname regex, it is a rewritten path.
// In this case, should use asPath to preserve the current URL.
if (!match) {
baseBase = router.asPath
}
// Note: There is an edge case where the pathname is dynamic, and also a rewrite path to the same segment.
// E.g. in "/[slug]" path, rewrite "/foo" -> "/bar"
// In this case, it will be treated as a non-rewritten path and possibly interpolate the query string.
// E.g., "/any?slug=foo" will become the content of "/foo", not rewritten as "/bar"
// This is currently a trade-off of not resolving rewrite paths on every Router/Link call,
// but using a lighter route regex pattern check.
}
}
base = new URL(baseBase, 'http://n')
} catch (_) {
// fallback to / for invalid asPath values e.g. //
base = new URL('/', 'http://n')
}
try {
const finalUrl = new URL(urlAsString, base)
finalUrl.pathname = normalizePathTrailingSlash(finalUrl.pathname)
let interpolatedAs = ''
if (
isDynamicRoute(finalUrl.pathname) &&
finalUrl.searchParams &&
resolveAs
) {
const query = searchParamsToUrlQuery(finalUrl.searchParams)
const { result, params } = interpolateAs(
finalUrl.pathname,
finalUrl.pathname,
query
)
if (result) {
interpolatedAs = formatWithValidation({
pathname: result,
hash: finalUrl.hash,
query: omit(query, params),
})
}
}
// if the origin didn't change, it means we received a relative href
const resolvedHref =
finalUrl.origin === base.origin
? finalUrl.href.slice(finalUrl.origin.length)
: finalUrl.href
return resolveAs
? [resolvedHref, interpolatedAs || resolvedHref]
: resolvedHref
} catch (_) {
return resolveAs ? [urlAsString] : urlAsString
}
}

View File

@@ -0,0 +1,65 @@
import React from 'react'
import { useRouter } from './router'
const nextjsRouteAnnouncerStyles: React.CSSProperties = {
border: 0,
clip: 'rect(0 0 0 0)',
height: '1px',
margin: '-1px',
overflow: 'hidden',
padding: 0,
position: 'absolute',
top: 0,
width: '1px',
// https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe
whiteSpace: 'nowrap',
wordWrap: 'normal',
}
export const RouteAnnouncer = () => {
const { asPath } = useRouter()
const [routeAnnouncement, setRouteAnnouncement] = React.useState('')
// Only announce the path change, but not for the first load because screen
// reader will do that automatically.
const previouslyLoadedPath = React.useRef(asPath)
// Every time the path changes, announce the new pages title following this
// priority: first the document title (from head), otherwise the first h1, or
// if none of these exist, then the pathname from the URL. This methodology is
// inspired by Marcy Suttons accessible client routing user testing. More
// information can be found here:
// https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/
React.useEffect(
() => {
// If the path hasn't change, we do nothing.
if (previouslyLoadedPath.current === asPath) return
previouslyLoadedPath.current = asPath
if (document.title) {
setRouteAnnouncement(document.title)
} else {
const pageHeader = document.querySelector('h1')
const content = pageHeader?.innerText ?? pageHeader?.textContent
setRouteAnnouncement(content || asPath)
}
},
// TODO: switch to pathname + query object of dynamic route requirements
[asPath]
)
return (
<p
aria-live="assertive" // Make the announcement immediately.
id="__next-route-announcer__"
role="alert"
style={nextjsRouteAnnouncerStyles}
>
{routeAnnouncement}
</p>
)
}
export default RouteAnnouncer

453
client/route-loader.ts Normal file
View File

@@ -0,0 +1,453 @@
import type { ComponentType } from 'react'
import type { MiddlewareMatcher } from '../build/analysis/get-page-static-info'
import getAssetPathFromRoute from '../shared/lib/router/utils/get-asset-path-from-route'
import { __unsafeCreateTrustedScriptURL } from './trusted-types'
import { requestIdleCallback } from './request-idle-callback'
import { getDeploymentIdQueryOrEmptyString } from '../build/deployment-id'
import { encodeURIPath } from '../shared/lib/encode-uri-path'
// 3.8s was arbitrarily chosen as it's what https://web.dev/interactive
// considers as "Good" time-to-interactive. We must assume something went
// wrong beyond this point, and then fall-back to a full page transition to
// show the user something of value.
const MS_MAX_IDLE_DELAY = 3800
declare global {
interface Window {
__BUILD_MANIFEST?: Record<string, string[]>
__BUILD_MANIFEST_CB?: Function
__MIDDLEWARE_MATCHERS?: MiddlewareMatcher[]
__MIDDLEWARE_MANIFEST_CB?: Function
__REACT_LOADABLE_MANIFEST?: any
__DYNAMIC_CSS_MANIFEST?: any
__RSC_MANIFEST?: any
__RSC_SERVER_MANIFEST?: any
__NEXT_FONT_MANIFEST?: any
__SUBRESOURCE_INTEGRITY_MANIFEST?: string
__INTERCEPTION_ROUTE_REWRITE_MANIFEST?: string
}
}
interface LoadedEntrypointSuccess {
component: ComponentType
exports: any
}
interface LoadedEntrypointFailure {
error: unknown
}
type RouteEntrypoint = LoadedEntrypointSuccess | LoadedEntrypointFailure
interface RouteStyleSheet {
href: string
content: string
}
interface LoadedRouteSuccess extends LoadedEntrypointSuccess {
styles: RouteStyleSheet[]
}
interface LoadedRouteFailure {
error: unknown
}
type RouteLoaderEntry = LoadedRouteSuccess | LoadedRouteFailure
interface Future<V> {
resolve: (entrypoint: V) => void
future: Promise<V>
}
function withFuture<T extends object>(
key: string,
map: Map<string, Future<T> | T>,
generator?: () => Promise<T>
): Promise<T> {
let entry = map.get(key)
if (entry) {
if ('future' in entry) {
return entry.future
}
return Promise.resolve(entry)
}
let resolver: (entrypoint: T) => void
const prom: Promise<T> = new Promise<T>((resolve) => {
resolver = resolve
})
map.set(key, { resolve: resolver!, future: prom })
return generator
? generator()
.then((value) => {
resolver(value)
return value
})
.catch((err) => {
map.delete(key)
throw err
})
: prom
}
export interface RouteLoader {
whenEntrypoint(route: string): Promise<RouteEntrypoint>
onEntrypoint(route: string, execute: () => unknown): void
loadRoute(route: string, prefetch?: boolean): Promise<RouteLoaderEntry>
prefetch(route: string): Promise<void>
}
const ASSET_LOAD_ERROR = Symbol('ASSET_LOAD_ERROR')
// TODO: unexport
export function markAssetError(err: Error): Error {
return Object.defineProperty(err, ASSET_LOAD_ERROR, {})
}
export function isAssetError(err?: Error): boolean | undefined {
return err && ASSET_LOAD_ERROR in err
}
function hasPrefetch(link?: HTMLLinkElement): boolean {
try {
link = document.createElement('link')
return (
// detect IE11 since it supports prefetch but isn't detected
// with relList.support
(!!window.MSInputMethodContext && !!(document as any).documentMode) ||
link.relList.supports('prefetch')
)
} catch {
return false
}
}
const canPrefetch: boolean = hasPrefetch()
const getAssetQueryString = () => {
return getDeploymentIdQueryOrEmptyString()
}
function prefetchViaDom(
href: string,
as: string,
link?: HTMLLinkElement
): Promise<any> {
return new Promise<void>((resolve, reject) => {
const selector = `
link[rel="prefetch"][href^="${href}"],
link[rel="preload"][href^="${href}"],
script[src^="${href}"]`
if (document.querySelector(selector)) {
return resolve()
}
link = document.createElement('link')
// The order of property assignment here is intentional:
if (as) link!.as = as
link!.rel = `prefetch`
link!.crossOrigin = process.env.__NEXT_CROSS_ORIGIN!
link!.onload = resolve as any
link!.onerror = () =>
reject(markAssetError(new Error(`Failed to prefetch: ${href}`)))
// `href` should always be last:
link!.href = href
document.head.appendChild(link)
})
}
function appendScript(
src: TrustedScriptURL | string,
script?: HTMLScriptElement
): Promise<unknown> {
return new Promise((resolve, reject) => {
script = document.createElement('script')
// The order of property assignment here is intentional.
// 1. Setup success/failure hooks in case the browser synchronously
// executes when `src` is set.
script.onload = resolve
script.onerror = () =>
reject(markAssetError(new Error(`Failed to load script: ${src}`)))
// 2. Configure the cross-origin attribute before setting `src` in case the
// browser begins to fetch.
script.crossOrigin = process.env.__NEXT_CROSS_ORIGIN!
// 3. Finally, set the source and inject into the DOM in case the child
// must be appended for fetching to start.
script.src = src as string
document.body.appendChild(script)
})
}
// We wait for pages to be built in dev before we start the route transition
// timeout to prevent an un-necessary hard navigation in development.
let devBuildPromise: Promise<void> | undefined
// Resolve a promise that times out after given amount of milliseconds.
function resolvePromiseWithTimeout<T>(
p: Promise<T>,
ms: number,
err: Error
): Promise<T> {
return new Promise((resolve, reject) => {
let cancelled = false
p.then((r) => {
// Resolved, cancel the timeout
cancelled = true
resolve(r)
}).catch(reject)
// We wrap these checks separately for better dead-code elimination in
// production bundles.
if (process.env.NODE_ENV === 'development') {
;(devBuildPromise || Promise.resolve()).then(() => {
requestIdleCallback(() =>
setTimeout(() => {
if (!cancelled) {
reject(err)
}
}, ms)
)
})
}
if (process.env.NODE_ENV !== 'development') {
requestIdleCallback(() =>
setTimeout(() => {
if (!cancelled) {
reject(err)
}
}, ms)
)
}
})
}
// TODO: stop exporting or cache the failure
// It'd be best to stop exporting this. It's an implementation detail. We're
// only exporting it for backwards compatibility with the `page-loader`.
// Only cache this response as a last resort if we cannot eliminate all other
// code branches that use the Build Manifest Callback and push them through
// the Route Loader interface.
export function getClientBuildManifest() {
if (self.__BUILD_MANIFEST) {
return Promise.resolve(self.__BUILD_MANIFEST)
}
const onBuildManifest = new Promise<Record<string, string[]>>((resolve) => {
// Mandatory because this is not concurrent safe:
const cb = self.__BUILD_MANIFEST_CB
self.__BUILD_MANIFEST_CB = () => {
resolve(self.__BUILD_MANIFEST!)
cb && cb()
}
})
return resolvePromiseWithTimeout(
onBuildManifest,
MS_MAX_IDLE_DELAY,
markAssetError(new Error('Failed to load client build manifest'))
)
}
interface RouteFiles {
scripts: (TrustedScriptURL | string)[]
css: string[]
}
function getFilesForRoute(
assetPrefix: string,
route: string
): Promise<RouteFiles> {
if (process.env.NODE_ENV === 'development') {
const scriptUrl =
assetPrefix +
'/_next/static/chunks/pages' +
encodeURIPath(getAssetPathFromRoute(route, '.js')) +
getAssetQueryString()
return Promise.resolve({
scripts: [__unsafeCreateTrustedScriptURL(scriptUrl)],
// Styles are handled by `style-loader` in development:
css: [],
})
}
return getClientBuildManifest().then((manifest) => {
if (!(route in manifest)) {
throw markAssetError(new Error(`Failed to lookup route: ${route}`))
}
const allFiles = manifest[route].map(
(entry) => assetPrefix + '/_next/' + encodeURIPath(entry)
)
return {
scripts: allFiles
.filter((v) => v.endsWith('.js'))
.map((v) => __unsafeCreateTrustedScriptURL(v) + getAssetQueryString()),
css: allFiles
.filter((v) => v.endsWith('.css'))
.map((v) => v + getAssetQueryString()),
}
})
}
export function createRouteLoader(assetPrefix: string): RouteLoader {
const entrypoints: Map<string, Future<RouteEntrypoint> | RouteEntrypoint> =
new Map()
const loadedScripts: Map<string, Promise<unknown>> = new Map()
const styleSheets: Map<string, Promise<RouteStyleSheet>> = new Map()
const routes: Map<string, Future<RouteLoaderEntry> | RouteLoaderEntry> =
new Map()
function maybeExecuteScript(
src: TrustedScriptURL | string
): Promise<unknown> {
// With HMR we might need to "reload" scripts when they are
// disposed and readded. Executing scripts twice has no functional
// differences
if (process.env.NODE_ENV !== 'development') {
let prom: Promise<unknown> | undefined = loadedScripts.get(src.toString())
if (prom) {
return prom
}
// Skip executing script if it's already in the DOM:
if (document.querySelector(`script[src^="${src}"]`)) {
return Promise.resolve()
}
loadedScripts.set(src.toString(), (prom = appendScript(src)))
return prom
} else {
return appendScript(src)
}
}
function fetchStyleSheet(href: string): Promise<RouteStyleSheet> {
let prom: Promise<RouteStyleSheet> | undefined = styleSheets.get(href)
if (prom) {
return prom
}
styleSheets.set(
href,
(prom = fetch(href, { credentials: 'same-origin' })
.then((res) => {
if (!res.ok) {
throw new Error(`Failed to load stylesheet: ${href}`)
}
return res.text().then((text) => ({ href: href, content: text }))
})
.catch((err) => {
throw markAssetError(err)
}))
)
return prom
}
return {
whenEntrypoint(route: string) {
return withFuture(route, entrypoints)
},
onEntrypoint(route: string, execute: undefined | (() => unknown)) {
;(execute
? Promise.resolve()
.then(() => execute())
.then(
(exports: any) => ({
component: (exports && exports.default) || exports,
exports: exports,
}),
(err) => ({ error: err })
)
: Promise.resolve(undefined)
).then((input: RouteEntrypoint | undefined) => {
const old = entrypoints.get(route)
if (old && 'resolve' in old) {
if (input) {
entrypoints.set(route, input)
old.resolve(input)
}
} else {
if (input) {
entrypoints.set(route, input)
} else {
entrypoints.delete(route)
}
// when this entrypoint has been resolved before
// the route is outdated and we want to invalidate
// this cache entry
routes.delete(route)
}
})
},
loadRoute(route: string, prefetch?: boolean) {
return withFuture<RouteLoaderEntry>(route, routes, () => {
let devBuildPromiseResolve: () => void
if (process.env.NODE_ENV === 'development') {
devBuildPromise = new Promise<void>((resolve) => {
devBuildPromiseResolve = resolve
})
}
return resolvePromiseWithTimeout(
getFilesForRoute(assetPrefix, route)
.then(({ scripts, css }) => {
return Promise.all([
entrypoints.has(route)
? []
: Promise.all(scripts.map(maybeExecuteScript)),
Promise.all(css.map(fetchStyleSheet)),
] as const)
})
.then((res) => {
return this.whenEntrypoint(route).then((entrypoint) => ({
entrypoint,
styles: res[1],
}))
}),
MS_MAX_IDLE_DELAY,
markAssetError(new Error(`Route did not complete loading: ${route}`))
)
.then(({ entrypoint, styles }) => {
const res: RouteLoaderEntry = Object.assign<
{ styles: RouteStyleSheet[] },
RouteEntrypoint
>({ styles: styles! }, entrypoint)
return 'error' in entrypoint ? entrypoint : res
})
.catch((err) => {
if (prefetch) {
// we don't want to cache errors during prefetch
throw err
}
return { error: err }
})
.finally(() => devBuildPromiseResolve?.())
})
},
prefetch(route: string): Promise<void> {
// https://github.com/GoogleChromeLabs/quicklink/blob/453a661fa1fa940e2d2e044452398e38c67a98fb/src/index.mjs#L115-L118
// License: Apache 2.0
let cn
if ((cn = (navigator as any).connection)) {
// Don't prefetch if using 2G or if Save-Data is enabled.
if (cn.saveData || /2g/.test(cn.effectiveType)) return Promise.resolve()
}
return getFilesForRoute(assetPrefix, route)
.then((output) =>
Promise.all(
canPrefetch
? output.scripts.map((script) =>
prefetchViaDom(script.toString(), 'script')
)
: []
)
)
.then(() => {
requestIdleCallback(() => this.loadRoute(route, true).catch(() => {}))
})
.catch(
// swallow prefetch errors
() => {}
)
},
}
}

195
client/router.ts Normal file
View File

@@ -0,0 +1,195 @@
/* global window */
import React from 'react'
import Router from '../shared/lib/router/router'
import type { NextRouter } from '../shared/lib/router/router'
import { RouterContext } from '../shared/lib/router-context.shared-runtime'
import isError from '../lib/is-error'
type SingletonRouterBase = {
router: Router | null
readyCallbacks: Array<() => any>
ready(cb: () => any): void
}
export { Router }
export type { NextRouter }
export type SingletonRouter = SingletonRouterBase & NextRouter
const singletonRouter: SingletonRouterBase = {
router: null, // holds the actual router instance
readyCallbacks: [],
ready(callback: () => void) {
if (this.router) return callback()
if (typeof window !== 'undefined') {
this.readyCallbacks.push(callback)
}
},
}
// Create public properties and methods of the router in the singletonRouter
const urlPropertyFields = [
'pathname',
'route',
'query',
'asPath',
'components',
'isFallback',
'basePath',
'locale',
'locales',
'defaultLocale',
'isReady',
'isPreview',
'isLocaleDomain',
'domainLocales',
] as const
const routerEvents = [
'routeChangeStart',
'beforeHistoryChange',
'routeChangeComplete',
'routeChangeError',
'hashChangeStart',
'hashChangeComplete',
] as const
export type RouterEvent = (typeof routerEvents)[number]
const coreMethodFields = [
'push',
'replace',
'reload',
'back',
'prefetch',
'beforePopState',
] as const
// Events is a static property on the router, the router doesn't have to be initialized to use it
Object.defineProperty(singletonRouter, 'events', {
get() {
return Router.events
},
})
function getRouter(): Router {
if (!singletonRouter.router) {
const message =
'No router instance found.\n' +
'You should only use "next/router" on the client side of your app.\n'
throw new Error(message)
}
return singletonRouter.router
}
urlPropertyFields.forEach((field) => {
// Here we need to use Object.defineProperty because we need to return
// the property assigned to the actual router
// The value might get changed as we change routes and this is the
// proper way to access it
Object.defineProperty(singletonRouter, field, {
get() {
const router = getRouter()
return router[field] as string
},
})
})
coreMethodFields.forEach((field) => {
// We don't really know the types here, so we add them later instead
;(singletonRouter as any)[field] = (...args: any[]) => {
const router = getRouter() as any
return router[field](...args)
}
})
routerEvents.forEach((event) => {
singletonRouter.ready(() => {
Router.events.on(event, (...args) => {
const eventField = `on${event.charAt(0).toUpperCase()}${event.substring(
1
)}`
const _singletonRouter = singletonRouter as any
if (_singletonRouter[eventField]) {
try {
_singletonRouter[eventField](...args)
} catch (err) {
console.error(`Error when running the Router event: ${eventField}`)
console.error(
isError(err) ? `${err.message}\n${err.stack}` : err + ''
)
}
}
})
})
})
// Export the singletonRouter and this is the public API.
export default singletonRouter as SingletonRouter
// Reexport the withRouter HOC
export { default as withRouter } from './with-router'
/**
* This hook gives access the [router object](https://nextjs.org/docs/pages/api-reference/functions/use-router#router-object)
* inside the [Pages Router](https://nextjs.org/docs/pages/building-your-application).
*
* Read more: [Next.js Docs: `useRouter`](https://nextjs.org/docs/pages/api-reference/functions/use-router)
*/
export function useRouter(): NextRouter {
const router = React.useContext(RouterContext)
if (!router) {
throw new Error(
'NextRouter was not mounted. https://nextjs.org/docs/messages/next-router-not-mounted'
)
}
return router
}
/**
* Create a router and assign it as the singleton instance.
* This is used in client side when we are initializing the app.
* This should **not** be used inside the server.
* @internal
*/
export function createRouter(
...args: ConstructorParameters<typeof Router>
): Router {
singletonRouter.router = new Router(...args)
singletonRouter.readyCallbacks.forEach((cb) => cb())
singletonRouter.readyCallbacks = []
return singletonRouter.router
}
/**
* This function is used to create the `withRouter` router instance
* @internal
*/
export function makePublicRouterInstance(router: Router): NextRouter {
const scopedRouter = router as any
const instance = {} as any
for (const property of urlPropertyFields) {
if (typeof scopedRouter[property] === 'object') {
instance[property] = Object.assign(
Array.isArray(scopedRouter[property]) ? [] : {},
scopedRouter[property]
) // makes sure query is not stateful
continue
}
instance[property] = scopedRouter[property]
}
// Events is a static property on the router, the router doesn't have to be initialized to use it
instance.events = Router.events
coreMethodFields.forEach((field) => {
instance[field] = (...args: any[]) => {
return scopedRouter[field](...args)
}
})
return instance
}

385
client/script.tsx Normal file
View File

@@ -0,0 +1,385 @@
'use client'
import ReactDOM from 'react-dom'
import React, { useEffect, useContext, useRef, type JSX } from 'react'
import type { ScriptHTMLAttributes } from 'react'
import { HeadManagerContext } from '../shared/lib/head-manager-context.shared-runtime'
import { setAttributesFromProps } from './set-attributes-from-props'
import { requestIdleCallback } from './request-idle-callback'
const ScriptCache = new Map()
const LoadCache = new Set()
export interface ScriptProps extends ScriptHTMLAttributes<HTMLScriptElement> {
strategy?: 'afterInteractive' | 'lazyOnload' | 'beforeInteractive' | 'worker'
id?: string
onLoad?: (e: any) => void
onReady?: () => void | null
onError?: (e: any) => void
children?: React.ReactNode
stylesheets?: string[]
}
/**
* @deprecated Use `ScriptProps` instead.
*/
export type Props = ScriptProps
const insertStylesheets = (stylesheets: string[]) => {
// Case 1: Styles for afterInteractive/lazyOnload with appDir injected via handleClientScriptLoad
//
// Using ReactDOM.preinit to feature detect appDir and inject styles
// Stylesheets might have already been loaded if initialized with Script component
// Re-inject styles here to handle scripts loaded via handleClientScriptLoad
// ReactDOM.preinit handles dedup and ensures the styles are loaded only once
if (ReactDOM.preinit) {
stylesheets.forEach((stylesheet: string) => {
ReactDOM.preinit(stylesheet, { as: 'style' })
})
return
}
// Case 2: Styles for afterInteractive/lazyOnload with pages injected via handleClientScriptLoad
//
// We use this function to load styles when appdir is not detected
// TODO: Use React float APIs to load styles once available for pages dir
if (typeof window !== 'undefined') {
let head = document.head
stylesheets.forEach((stylesheet: string) => {
let link = document.createElement('link')
link.type = 'text/css'
link.rel = 'stylesheet'
link.href = stylesheet
head.appendChild(link)
})
}
}
const loadScript = (props: ScriptProps): void => {
const {
src,
id,
onLoad = () => {},
onReady = null,
dangerouslySetInnerHTML,
children = '',
strategy = 'afterInteractive',
onError,
stylesheets,
} = props
const cacheKey = id || src
// Script has already loaded
if (cacheKey && LoadCache.has(cacheKey)) {
return
}
// Contents of this script are already loading/loaded
if (ScriptCache.has(src)) {
LoadCache.add(cacheKey)
// It is possible that multiple `next/script` components all have same "src", but has different "onLoad"
// This is to make sure the same remote script will only load once, but "onLoad" are executed in order
ScriptCache.get(src).then(onLoad, onError)
return
}
/** Execute after the script first loaded */
const afterLoad = () => {
// Run onReady for the first time after load event
if (onReady) {
onReady()
}
// add cacheKey to LoadCache when load successfully
LoadCache.add(cacheKey)
}
const el = document.createElement('script')
const loadPromise = new Promise<void>((resolve, reject) => {
el.addEventListener('load', function (e) {
resolve()
if (onLoad) {
onLoad.call(this, e)
}
afterLoad()
})
el.addEventListener('error', function (e) {
reject(e)
})
}).catch(function (e) {
if (onError) {
onError(e)
}
})
if (dangerouslySetInnerHTML) {
// Casting since lib.dom.d.ts doesn't have TrustedHTML yet.
el.innerHTML = (dangerouslySetInnerHTML.__html as string) || ''
afterLoad()
} else if (children) {
el.textContent =
typeof children === 'string'
? children
: Array.isArray(children)
? children.join('')
: ''
afterLoad()
} else if (src) {
el.src = src
// do not add cacheKey into LoadCache for remote script here
// cacheKey will be added to LoadCache when it is actually loaded (see loadPromise above)
ScriptCache.set(src, loadPromise)
}
setAttributesFromProps(el, props)
if (strategy === 'worker') {
el.setAttribute('type', 'text/partytown')
}
el.setAttribute('data-nscript', strategy)
// Load styles associated with this script
if (stylesheets) {
insertStylesheets(stylesheets)
}
document.body.appendChild(el)
}
export function handleClientScriptLoad(props: ScriptProps) {
const { strategy = 'afterInteractive' } = props
if (strategy === 'lazyOnload') {
window.addEventListener('load', () => {
requestIdleCallback(() => loadScript(props))
})
} else {
loadScript(props)
}
}
function loadLazyScript(props: ScriptProps) {
if (document.readyState === 'complete') {
requestIdleCallback(() => loadScript(props))
} else {
window.addEventListener('load', () => {
requestIdleCallback(() => loadScript(props))
})
}
}
function addBeforeInteractiveToCache() {
const scripts = [
...document.querySelectorAll('[data-nscript="beforeInteractive"]'),
...document.querySelectorAll('[data-nscript="beforePageRender"]'),
]
scripts.forEach((script) => {
const cacheKey = script.id || script.getAttribute('src')
LoadCache.add(cacheKey)
})
}
export function initScriptLoader(scriptLoaderItems: ScriptProps[]) {
scriptLoaderItems.forEach(handleClientScriptLoad)
addBeforeInteractiveToCache()
}
/**
* Load a third-party scripts in an optimized way.
*
* Read more: [Next.js Docs: `next/script`](https://nextjs.org/docs/app/api-reference/components/script)
*/
function Script(props: ScriptProps): JSX.Element | null {
const {
id,
src = '',
onLoad = () => {},
onReady = null,
strategy = 'afterInteractive',
onError,
stylesheets,
...restProps
} = props
// Context is available only during SSR
let { updateScripts, scripts, getIsSsr, appDir, nonce } =
useContext(HeadManagerContext)
// if a nonce is explicitly passed to the script tag, favor that over the automatic handling
nonce = restProps.nonce || nonce
/**
* - First mount:
* 1. The useEffect for onReady executes
* 2. hasOnReadyEffectCalled.current is false, but the script hasn't loaded yet (not in LoadCache)
* onReady is skipped, set hasOnReadyEffectCalled.current to true
* 3. The useEffect for loadScript executes
* 4. hasLoadScriptEffectCalled.current is false, loadScript executes
* Once the script is loaded, the onLoad and onReady will be called by then
* [If strict mode is enabled / is wrapped in <OffScreen /> component]
* 5. The useEffect for onReady executes again
* 6. hasOnReadyEffectCalled.current is true, so entire effect is skipped
* 7. The useEffect for loadScript executes again
* 8. hasLoadScriptEffectCalled.current is true, so entire effect is skipped
*
* - Second mount:
* 1. The useEffect for onReady executes
* 2. hasOnReadyEffectCalled.current is false, but the script has already loaded (found in LoadCache)
* onReady is called, set hasOnReadyEffectCalled.current to true
* 3. The useEffect for loadScript executes
* 4. The script is already loaded, loadScript bails out
* [If strict mode is enabled / is wrapped in <OffScreen /> component]
* 5. The useEffect for onReady executes again
* 6. hasOnReadyEffectCalled.current is true, so entire effect is skipped
* 7. The useEffect for loadScript executes again
* 8. hasLoadScriptEffectCalled.current is true, so entire effect is skipped
*/
const hasOnReadyEffectCalled = useRef(false)
useEffect(() => {
const cacheKey = id || src
if (!hasOnReadyEffectCalled.current) {
// Run onReady if script has loaded before but component is re-mounted
if (onReady && cacheKey && LoadCache.has(cacheKey)) {
onReady()
}
hasOnReadyEffectCalled.current = true
}
}, [onReady, id, src])
const hasLoadScriptEffectCalled = useRef(false)
useEffect(() => {
if (!hasLoadScriptEffectCalled.current) {
if (strategy === 'afterInteractive') {
loadScript(props)
} else if (strategy === 'lazyOnload') {
loadLazyScript(props)
}
hasLoadScriptEffectCalled.current = true
}
}, [props, strategy])
if (strategy === 'beforeInteractive' || strategy === 'worker') {
if (updateScripts) {
scripts[strategy] = (scripts[strategy] || []).concat([
{
id,
src,
onLoad,
onReady,
onError,
...restProps,
nonce,
},
])
updateScripts(scripts)
} else if (getIsSsr && getIsSsr()) {
// Script has already loaded during SSR
LoadCache.add(id || src)
} else if (getIsSsr && !getIsSsr()) {
loadScript({
...props,
nonce,
})
}
}
// For the app directory, we need React Float to preload these scripts.
if (appDir) {
// Injecting stylesheets here handles beforeInteractive and worker scripts correctly
// For other strategies injecting here ensures correct stylesheet order
// ReactDOM.preinit handles loading the styles in the correct order,
// also ensures the stylesheet is loaded only once and in a consistent manner
//
// Case 1: Styles for beforeInteractive/worker with appDir - handled here
// Case 2: Styles for beforeInteractive/worker with pages dir - Not handled yet
// Case 3: Styles for afterInteractive/lazyOnload with appDir - handled here
// Case 4: Styles for afterInteractive/lazyOnload with pages dir - handled in insertStylesheets function
if (stylesheets) {
stylesheets.forEach((styleSrc) => {
ReactDOM.preinit(styleSrc, { as: 'style' })
})
}
// Before interactive scripts need to be loaded by Next.js' runtime instead
// of native <script> tags, because they no longer have `defer`.
if (strategy === 'beforeInteractive') {
if (!src) {
// For inlined scripts, we put the content in `children`.
if (restProps.dangerouslySetInnerHTML) {
// Casting since lib.dom.d.ts doesn't have TrustedHTML yet.
restProps.children = restProps.dangerouslySetInnerHTML
.__html as string
delete restProps.dangerouslySetInnerHTML
}
return (
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `(self.__next_s=self.__next_s||[]).push(${JSON.stringify([
0,
{ ...restProps, id },
])})`,
}}
/>
)
} else {
// @ts-ignore
ReactDOM.preload(
src,
restProps.integrity
? {
as: 'script',
integrity: restProps.integrity,
nonce,
crossOrigin: restProps.crossOrigin,
}
: { as: 'script', nonce, crossOrigin: restProps.crossOrigin }
)
return (
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `(self.__next_s=self.__next_s||[]).push(${JSON.stringify([
src,
{ ...restProps, id },
])})`,
}}
/>
)
}
} else if (strategy === 'afterInteractive') {
if (src) {
// @ts-ignore
ReactDOM.preload(
src,
restProps.integrity
? {
as: 'script',
integrity: restProps.integrity,
nonce,
crossOrigin: restProps.crossOrigin,
}
: { as: 'script', nonce, crossOrigin: restProps.crossOrigin }
)
}
}
}
return null
}
Object.defineProperty(Script, '__nextScript', { value: true })
export default Script

View File

@@ -0,0 +1,59 @@
const DOMAttributeNames: Record<string, string> = {
acceptCharset: 'accept-charset',
className: 'class',
htmlFor: 'for',
httpEquiv: 'http-equiv',
noModule: 'noModule',
}
const ignoreProps = [
'onLoad',
'onReady',
'dangerouslySetInnerHTML',
'children',
'onError',
'strategy',
'stylesheets',
]
function isBooleanScriptAttribute(
attr: string
): attr is 'async' | 'defer' | 'noModule' {
return ['async', 'defer', 'noModule'].includes(attr)
}
export function setAttributesFromProps(el: HTMLElement, props: object) {
for (const [p, value] of Object.entries(props)) {
if (!props.hasOwnProperty(p)) continue
if (ignoreProps.includes(p)) continue
// we don't render undefined props to the DOM
if (value === undefined) {
continue
}
const attr = DOMAttributeNames[p] || p.toLowerCase()
if (el.tagName === 'SCRIPT' && isBooleanScriptAttribute(attr)) {
// Correctly assign boolean script attributes
// https://github.com/vercel/next.js/pull/20748
;(el as HTMLScriptElement)[attr] = !!value
} else {
el.setAttribute(attr, String(value))
}
// Remove falsy non-zero boolean attributes so they are correctly interpreted
// (e.g. if we set them to false, this coerces to the string "false", which the browser interprets as true)
if (
value === false ||
(el.tagName === 'SCRIPT' &&
isBooleanScriptAttribute(attr) &&
(!value || value === 'false'))
) {
// Call setAttribute before, as we need to set and unset the attribute to override force async:
// https://html.spec.whatwg.org/multipage/scripting.html#script-force-async
el.setAttribute(attr, '')
el.removeAttribute(attr)
}
}
}

37
client/trusted-types.ts Normal file
View File

@@ -0,0 +1,37 @@
/**
* Stores the Trusted Types Policy. Starts as undefined and can be set to null
* if Trusted Types is not supported in the browser.
*/
let policy: TrustedTypePolicy | null | undefined
/**
* Getter for the Trusted Types Policy. If it is undefined, it is instantiated
* here or set to null if Trusted Types is not supported in the browser.
*/
function getPolicy() {
if (typeof policy === 'undefined' && typeof window !== 'undefined') {
policy =
window.trustedTypes?.createPolicy('nextjs', {
createHTML: (input) => input,
createScript: (input) => input,
createScriptURL: (input) => input,
}) || null
}
return policy
}
/**
* Unsafely promote a string to a TrustedScriptURL, falling back to strings
* when Trusted Types are not available.
* This is a security-sensitive function; any use of this function
* must go through security review. In particular, it must be assured that the
* provided string will never cause an XSS vulnerability if used in a context
* that will cause a browser to load and execute a resource, e.g. when
* assigning to script.src.
*/
export function __unsafeCreateTrustedScriptURL(
url: string
): TrustedScriptURL | string {
return getPolicy()?.createScriptURL(url) || url
}

39
client/webpack.ts Normal file
View File

@@ -0,0 +1,39 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
declare const __webpack_require__: any
// eslint-disable-next-line @typescript-eslint/no-unused-vars
declare let __webpack_public_path__: string
import { getDeploymentIdQueryOrEmptyString } from '../build/deployment-id'
// If we have a deployment ID, we need to append it to the webpack chunk names
// I am keeping the process check explicit so this can be statically optimized
if (process.env.NEXT_DEPLOYMENT_ID) {
const suffix = getDeploymentIdQueryOrEmptyString()
// eslint-disable-next-line no-undef
const getChunkScriptFilename = __webpack_require__.u
// eslint-disable-next-line no-undef
__webpack_require__.u = (...args: any[]) =>
// We enode the chunk filename because our static server matches against and encoded
// filename path.
getChunkScriptFilename(...args) + suffix
// eslint-disable-next-line no-undef
const getChunkCssFilename = __webpack_require__.k
// eslint-disable-next-line no-undef
__webpack_require__.k = (...args: any[]) =>
getChunkCssFilename(...args) + suffix
// eslint-disable-next-line no-undef
const getMiniCssFilename = __webpack_require__.miniCssF
// eslint-disable-next-line no-undef
__webpack_require__.miniCssF = (...args: any[]) =>
getMiniCssFilename(...args) + suffix
}
// Ignore the module ID transform in client.
;(self as any).__next_set_public_path__ = (path: string) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__webpack_public_path__ = path
}
export {}

41
client/with-router.tsx Normal file
View File

@@ -0,0 +1,41 @@
import React, { type JSX } from 'react'
import type {
BaseContext,
NextComponentType,
NextPageContext,
} from '../shared/lib/utils'
import type { NextRouter } from './router'
import { useRouter } from './router'
export type WithRouterProps = {
router: NextRouter
}
export type ExcludeRouterProps<P> = Pick<
P,
Exclude<keyof P, keyof WithRouterProps>
>
export default function withRouter<
P extends WithRouterProps,
C extends BaseContext = NextPageContext,
>(
ComposedComponent: NextComponentType<C, any, P>
): React.ComponentType<ExcludeRouterProps<P>> {
function WithRouterWrapper(props: any): JSX.Element {
return <ComposedComponent router={useRouter()} {...props} />
}
WithRouterWrapper.getInitialProps = ComposedComponent.getInitialProps
// This is needed to allow checking for custom getInitialProps in _app
;(WithRouterWrapper as any).origGetInitialProps = (
ComposedComponent as any
).origGetInitialProps
if (process.env.NODE_ENV !== 'production') {
const name =
ComposedComponent.displayName || ComposedComponent.name || 'Unknown'
WithRouterWrapper.displayName = `withRouter(${name})`
}
return WithRouterWrapper
}

46
pages/_app.tsx Normal file
View File

@@ -0,0 +1,46 @@
import React from 'react'
import type {
AppContextType,
AppInitialProps,
AppPropsType,
NextWebVitalsMetric,
AppType,
} from '../shared/lib/utils'
import type { Router } from '../client/router'
import { loadGetInitialProps } from '../shared/lib/utils'
export type { AppInitialProps, AppType }
export type { NextWebVitalsMetric }
export type AppContext = AppContextType<Router>
export type AppProps<P = any> = AppPropsType<Router, P>
/**
* `App` component is used for initialize of pages. It allows for overwriting and full control of the `page` initialization.
* This allows for keeping state between navigation, custom error handling, injecting additional data.
*/
async function appGetInitialProps({
Component,
ctx,
}: AppContext): Promise<AppInitialProps> {
const pageProps = await loadGetInitialProps(Component, ctx)
return { pageProps }
}
export default class App<P = any, CP = {}, S = {}> extends React.Component<
P & AppProps<CP>,
S
> {
static origGetInitialProps = appGetInitialProps
static getInitialProps = appGetInitialProps
render() {
const { Component, pageProps } = this.props as AppProps<CP>
return <Component {...pageProps} />
}
}

156
pages/_error.tsx Normal file
View File

@@ -0,0 +1,156 @@
import React from 'react'
import Head from '../shared/lib/head'
import type { NextPageContext } from '../shared/lib/utils'
const statusCodes: { [code: number]: string } = {
400: 'Bad Request',
404: 'This page could not be found',
405: 'Method Not Allowed',
500: 'Internal Server Error',
}
export type ErrorProps = {
statusCode: number
hostname?: string
title?: string
withDarkMode?: boolean
}
function _getInitialProps({
req,
res,
err,
}: NextPageContext): Promise<ErrorProps> | ErrorProps {
const statusCode =
res && res.statusCode ? res.statusCode : err ? err.statusCode! : 404
let hostname
if (typeof window !== 'undefined') {
hostname = window.location.hostname
} else if (req) {
const { getRequestMeta } =
require('../server/request-meta') as typeof import('../server/request-meta')
const initUrl = getRequestMeta(req, 'initURL')
if (initUrl) {
const url = new URL(initUrl)
hostname = url.hostname
}
}
return { statusCode, hostname }
}
const styles: Record<string, React.CSSProperties> = {
error: {
// https://github.com/sindresorhus/modern-normalize/blob/main/modern-normalize.css#L38-L52
fontFamily:
'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
height: '100vh',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},
desc: {
lineHeight: '48px',
},
h1: {
display: 'inline-block',
margin: '0 20px 0 0',
paddingRight: 23,
fontSize: 24,
fontWeight: 500,
verticalAlign: 'top',
},
h2: {
fontSize: 14,
fontWeight: 400,
lineHeight: '28px',
},
wrap: {
display: 'inline-block',
},
}
/**
* `Error` component used for handling errors.
*/
export default class Error<P = {}> extends React.Component<P & ErrorProps> {
static displayName = 'ErrorPage'
static getInitialProps = _getInitialProps
static origGetInitialProps = _getInitialProps
render() {
const { statusCode, withDarkMode = true } = this.props
const title =
this.props.title ||
statusCodes[statusCode] ||
'An unexpected error has occurred'
return (
<div style={styles.error}>
<Head>
<title>
{statusCode
? `${statusCode}: ${title}`
: 'Application error: a client-side exception has occurred'}
</title>
</Head>
<div style={styles.desc}>
<style
dangerouslySetInnerHTML={{
/* CSS minified from
body { margin: 0; color: #000; background: #fff; }
.next-error-h1 {
border-right: 1px solid rgba(0, 0, 0, .3);
}
${
withDarkMode
? `@media (prefers-color-scheme: dark) {
body { color: #fff; background: #000; }
.next-error-h1 {
border-right: 1px solid rgba(255, 255, 255, .3);
}
}`
: ''
}
*/
__html: `body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}${
withDarkMode
? '@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}'
: ''
}`,
}}
/>
{statusCode ? (
<h1 className="next-error-h1" style={styles.h1}>
{statusCode}
</h1>
) : null}
<div style={styles.wrap}>
<h2 style={styles.h2}>
{this.props.title || statusCode ? (
title
) : (
<>
Application error: a client-side exception has occurred{' '}
{Boolean(this.props.hostname) && (
<>while loading {this.props.hostname}</>
)}{' '}
(see the browser console for more information)
</>
)}
.
</h2>
</div>
</div>
</div>
)
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"static/chunks/pages/demand-f7a7d01fb4629a28.js","mappings":"+EACA,4CACA,UACA,WACA,OAAe,EAAQ,KAAqC,CAC5D,EACA,SAFsB,uGCCP,SAASA,IACtB,MACE,UAACC,EAAAA,CAAMA,CAAAA,UACL,UAACC,EAAAA,CAAQA,CAAAA,CAAAA,IAGf","sources":["webpack://_N_E/?30a6","webpack://_N_E/./src/pages/demand/index.jsx"],"sourcesContent":["\n (window.__NEXT_P = window.__NEXT_P || []).push([\n \"/demand\",\n function () {\n return require(\"private-next-pages/demand/index.jsx\");\n }\n ]);\n if(module.hot) {\n module.hot.dispose(function () {\n window.__NEXT_P.push([\"/demand\"])\n });\n }\n ","import React from 'react';\n\nimport Layout from '@/modules/layout';\nimport Accounts from '@/modules/marketplace/accounts';\n\nexport default function Home() {\n return (\n <Layout>\n <Accounts />\n </Layout>\n );\n}\n"],"names":["Home","Layout","Accounts"],"sourceRoot":"","ignoreList":[]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
src/assets/img/empty.svg Normal file
View File

@@ -0,0 +1 @@
export default {"src":"/_next/static/media/empty.cdfa4b17.svg","height":173,"width":173,"blurWidth":0,"blurHeight":0};

View File

@@ -0,0 +1 @@
export default {"src":"/_next/static/media/logo-symbol.650de8de.png","height":128,"width":129,"blurDataURL":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAMAAADz0U65AAAAJ1BMVEVMaXH////////////////////////////////////////////////c+C/6AAAADHRSTlMADEsbbzRgJJ/W67YnuZu8AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAL0lEQVR4nGNgQABGZg5uZhDNxMLGxcnIwMDMwsrEysMEZrCBGXApBkZ2Dm52hGYAGlIAxcwQGZ8AAAAASUVORK5CYII=","blurWidth":8,"blurHeight":8};

View File

@@ -0,0 +1 @@
export default {"src":"/_next/static/media/logo-text.6e2d1f50.png","height":58,"width":357,"blurDataURL":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAABCAMAAADU3h9xAAAADFBMVEX////////////////1pQ5zAAAABHRSTlNbTEBqiwFd3gAAAAlwSFlzAAALEwAACxMBAJqcGAAAABFJREFUeJxjYGZgZGBkYmAEAAAyAAlbjYepAAAAAElFTkSuQmCC","blurWidth":8,"blurHeight":1};

View File

@@ -0,0 +1 @@
export default {"src":"/_next/static/media/no-image-portrait.aa1451b2.svg","height":600,"width":406,"blurWidth":0,"blurHeight":0};

View File

@@ -0,0 +1,61 @@
export const HTTPAccessErrorStatus = {
NOT_FOUND: 404,
FORBIDDEN: 403,
UNAUTHORIZED: 401,
}
const ALLOWED_CODES = new Set(Object.values(HTTPAccessErrorStatus))
export const HTTP_ERROR_FALLBACK_ERROR_CODE = 'NEXT_HTTP_ERROR_FALLBACK'
export type HTTPAccessFallbackError = Error & {
digest: `${typeof HTTP_ERROR_FALLBACK_ERROR_CODE};${string}`
}
/**
* Checks an error to determine if it's an error generated by
* the HTTP navigation APIs `notFound()`, `forbidden()` or `unauthorized()`.
*
* @param error the error that may reference a HTTP access error
* @returns true if the error is a HTTP access error
*/
export function isHTTPAccessFallbackError(
error: unknown
): error is HTTPAccessFallbackError {
if (
typeof error !== 'object' ||
error === null ||
!('digest' in error) ||
typeof error.digest !== 'string'
) {
return false
}
const [prefix, httpStatus] = error.digest.split(';')
return (
prefix === HTTP_ERROR_FALLBACK_ERROR_CODE &&
ALLOWED_CODES.has(Number(httpStatus))
)
}
export function getAccessFallbackHTTPStatus(
error: HTTPAccessFallbackError
): number {
const httpStatus = error.digest.split(';')[1]
return Number(httpStatus)
}
export function getAccessFallbackErrorTypeByStatus(
status: number
): 'not-found' | 'forbidden' | 'unauthorized' | undefined {
switch (status) {
case 401:
return 'unauthorized'
case 403:
return 'forbidden'
case 404:
return 'not-found'
default:
return
}
}

View File

@@ -0,0 +1,16 @@
import {
isHTTPAccessFallbackError,
type HTTPAccessFallbackError,
} from './http-access-fallback/http-access-fallback'
import { isRedirectError, type RedirectError } from './redirect-error'
/**
* Returns true if the error is a navigation signal error. These errors are
* thrown by user code to perform navigation operations and interrupt the React
* render.
*/
export function isNextRouterError(
error: unknown
): error is RedirectError | HTTPAccessFallbackError {
return isRedirectError(error) || isHTTPAccessFallbackError(error)
}

View File

@@ -0,0 +1,45 @@
import { RedirectStatusCode } from './redirect-status-code'
export const REDIRECT_ERROR_CODE = 'NEXT_REDIRECT'
export enum RedirectType {
push = 'push',
replace = 'replace',
}
export type RedirectError = Error & {
digest: `${typeof REDIRECT_ERROR_CODE};${RedirectType};${string};${RedirectStatusCode};`
}
/**
* Checks an error to determine if it's an error generated by the
* `redirect(url)` helper.
*
* @param error the error that may reference a redirect error
* @returns true if the error is a redirect error
*/
export function isRedirectError(error: unknown): error is RedirectError {
if (
typeof error !== 'object' ||
error === null ||
!('digest' in error) ||
typeof error.digest !== 'string'
) {
return false
}
const digest = error.digest.split(';')
const [errorCode, type] = digest
const destination = digest.slice(2, -2).join(';')
const status = digest.at(-2)
const statusCode = Number(status)
return (
errorCode === REDIRECT_ERROR_CODE &&
(type === 'replace' || type === 'push') &&
typeof destination === 'string' &&
!isNaN(statusCode) &&
statusCode in RedirectStatusCode
)
}

View File

@@ -0,0 +1,5 @@
export enum RedirectStatusCode {
SeeOther = 303,
TemporaryRedirect = 307,
PermanentRedirect = 308,
}

View File

@@ -0,0 +1,22 @@
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
type PortalProps = {
children: React.ReactNode
type: string
}
export const Portal = ({ children, type }: PortalProps) => {
const [portalNode, setPortalNode] = useState<HTMLElement | null>(null)
useEffect(() => {
const element = document.createElement(type)
document.body.appendChild(element)
setPortalNode(element)
return () => {
document.body.removeChild(element)
}
}, [type])
return portalNode ? createPortal(children, portalNode) : null
}

View File

@@ -0,0 +1,31 @@
// This module can be shared between both pages router and app router
import type { HydrationOptions } from 'react-dom/client'
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
import isError from '../../lib/is-error'
import { reportGlobalError } from './report-global-error'
const recoverableErrors = new WeakSet<Error>()
export function isRecoverableError(error: Error): boolean {
return recoverableErrors.has(error)
}
export const onRecoverableError: HydrationOptions['onRecoverableError'] = (
error
) => {
// x-ref: https://github.com/facebook/react/pull/28736
let cause = isError(error) && 'cause' in error ? error.cause : error
// Skip certain custom errors which are not expected to be reported on client
if (isBailoutToCSRError(cause)) return
if (process.env.NODE_ENV !== 'production') {
const { decorateDevError } =
require('../../next-devtools/userspace/app/errors/stitched-error') as typeof import('../../next-devtools/userspace/app/errors/stitched-error')
const causeError = decorateDevError(cause)
recoverableErrors.add(causeError)
cause = causeError
}
reportGlobalError(cause)
}

View File

@@ -0,0 +1,9 @@
export const reportGlobalError =
typeof reportError === 'function'
? // In modern browsers, reportError will dispatch an error event,
// emulating an uncaught JavaScript error.
reportError
: (error: unknown) => {
// TODO: Dispatch error event
globalThis.console.error(error)
}

View File

@@ -0,0 +1,79 @@
import mitt from '../../shared/lib/mitt'
import type { MittEmitter } from '../../shared/lib/mitt'
export type SpanOptions = {
startTime?: number
attributes?: Record<string, unknown>
}
export type SpanState =
| {
state: 'inprogress'
}
| {
state: 'ended'
endTime: number
}
interface ISpan {
name: string
startTime: number
attributes: Record<string, unknown>
state: SpanState
end(endTime?: number): void
}
class Span implements ISpan {
name: string
startTime: number
onSpanEnd: (span: Span) => void
state: SpanState
attributes: Record<string, unknown>
constructor(
name: string,
options: SpanOptions,
onSpanEnd: (span: Span) => void
) {
this.name = name
this.attributes = options.attributes ?? {}
this.startTime = options.startTime ?? Date.now()
this.onSpanEnd = onSpanEnd
this.state = { state: 'inprogress' }
}
end(endTime?: number) {
if (this.state.state === 'ended') {
throw new Error('Span has already ended')
}
this.state = {
state: 'ended',
endTime: endTime ?? Date.now(),
}
this.onSpanEnd(this)
}
}
class Tracer {
_emitter: MittEmitter<string> = mitt()
private handleSpanEnd = (span: Span) => {
this._emitter.emit('spanend', span)
}
startSpan(name: string, options: SpanOptions) {
return new Span(name, options, this.handleSpanEnd)
}
onSpanEnd(cb: (span: ISpan) => void): () => void {
this._emitter.on('spanend', cb)
return () => {
this._emitter.off('spanend', cb)
}
}
}
export type { ISpan as Span }
export default new Tracer()

View File

@@ -0,0 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import dayjs from 'dayjs';
import timezonePlugin from 'dayjs/plugin/timezone';
import utcPlugin from 'dayjs/plugin/utc';
import { abbreviateNumber } from '@/modules/@common/helpers/number';
import { getUserTimezoneSelector } from '@/modules/@common/user/redux/selectors';
import styles from './CustomTooltip.module.scss';
dayjs.extend(utcPlugin);
dayjs.extend(timezonePlugin);
function CustomTooltip({ tooltipDateFormatter = null, ...props }) {
const timezone = useSelector(getUserTimezoneSelector);
return props.active && props.payload?.length ? (
<div className={styles.Tooltip} style={{ backgroundColor: props.backgroundColor }}>
{props.payload.map((item, index) => {
if (item.color !== 'transparent') {
return (
<div key={`${index + 1}-${item.value}`}>
{tooltipDateFormatter && item?.payload?.time && (
<div>{tooltipDateFormatter(dayjs(item.payload.time).tz(timezone))}</div>
)}
{abbreviateNumber(item.value)}
</div>
);
}
return null;
})}
</div>
) : null;
}
CustomTooltip.propTypes = {
active: PropTypes.bool.isRequired,
payload: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
backgroundColor: PropTypes.string.isRequired,
tooltipDateFormatter: PropTypes.func,
};
export default CustomTooltip;

View File

@@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
module.exports = {"Tooltip":"CustomTooltip_Tooltip__TDEyZ"};

View File

@@ -0,0 +1,62 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Area, AreaChart, ResponsiveContainer, Tooltip } from 'recharts';
import { useTheme } from '@mui/material/styles';
import CustomTooltip from './CustomTooltip';
function AreaGraph({ gradient = 'areaGradient', tooltipDateFormatter = null, ...props }) {
const theme = useTheme();
return (
<ResponsiveContainer height="99.99%" width="99.99%">
<AreaChart
data={props.data}
margin={{
top: 2,
bottom: 0,
right: 2,
left: 3,
}}
// margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="areaGradient" gradientTransform="rotate(0)">
<stop offset="-12%" stopColor={theme.palette.primary.main} />
<stop offset="93%" stopColor="rgba(217, 217, 217, 0)" />
</linearGradient>
<linearGradient id="areaGradient2" gradientTransform="rotate(0.12)">
<stop offset="-57.98%" stopColor={theme.palette.secondary.light} />
<stop offset="99.89%" stopColor="rgba(217, 217, 217, 0)" />
</linearGradient>
<linearGradient id="areaGradient3" gradientTransform="rotate(0.12)">
<stop offset="-57.98%" stopColor={theme.palette.success.light} />
<stop offset="99.89%" stopColor="rgba(217, 217, 217, 0)" />
</linearGradient>
</defs>
<Tooltip
content={<CustomTooltip backgroundColor={props.stroke} tooltipDateFormatter={tooltipDateFormatter} />}
cursor={false}
/>
<Area type="monotone" dataKey="value" stroke={props.stroke} fillOpacity={1} fill={`url(#${gradient})`} />
</AreaChart>
</ResponsiveContainer>
);
}
AreaGraph.propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
time: PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
}),
).isRequired,
stroke: PropTypes.string.isRequired,
gradient: PropTypes.string,
tooltipDateFormatter: PropTypes.func,
};
export default AreaGraph;

View File

@@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
module.exports = {"FormControl":"DialogCalendarRange_FormControl__RVYa_","Calendar":"DialogCalendarRange_Calendar__Y_0_O"};

View File

@@ -0,0 +1,134 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Stack,
StaticDateRangePicker,
TextField,
} from '@/mui/components';
import styles from './DialogCalendarRange.module.scss';
const defineInterval = (diff) => {
if (diff > 14) {
return {
interval: 'WEEKS',
videoChartDataKey: 'date',
};
}
if (diff > 1) {
return {
interval: 'DAYS',
videoChartDataKey: 'date',
};
}
return {
interval: 'HOURS',
videoChartDataKey: 'hours',
};
};
function DialogCalendarRange(props) {
const [dateStart, setDateStart] = useState(dayjs(new Date()).add(-30, 'days'));
const [dateEnd, setDateEnd] = useState(dayjs(new Date()));
const handleDateSubmit = () => {
// do not change the format 'YYYY-MM-DD' to another or it will corrupt backend
const toFormat = (date) => (date ? dayjs(date).format('YYYY-MM-DD') : '');
const diff = dayjs(dateEnd).diff(dateStart, 'days');
const changesStringFrom = dayjs(dateStart).subtract(diff, 'day');
const { interval, videoChartDataKey } = defineInterval(Math.abs(diff));
props.onDateSubmit({
dateStart: toFormat(dateStart),
dateEnd: toFormat(dateEnd),
comparison: {
dateStart: toFormat(changesStringFrom),
dateEnd: toFormat(dateStart),
},
interval,
videoChartDataKey,
});
};
return (
<Dialog
open={props.open}
onClose={props.onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth="xl"
>
<DialogTitle>Custom range</DialogTitle>
<DialogContent>
<Stack direction="row" justifyContent="center" spacing={13}>
<TextField
className={styles.FormControl}
data-id="from"
value={dayjs(dateStart).format('MMM DD, YYYY')}
variant="outlined"
size="small"
label="From"
textAlign="center"
inputProps={{
readOnly: true,
}}
/>
<TextField
className={styles.FormControl}
data-id="from"
value={dayjs(dateEnd).format('MMM DD, YYYY')}
variant="outlined"
textAlign="center"
size="small"
label="To"
inputProps={{
readOnly: true,
}}
/>
</Stack>
<div className={styles.Calendar}>
<StaticDateRangePicker
value={[dayjs(dateStart), dayjs(dateEnd)]}
calendars={2}
slotProps={{
toolbar: {
hidden: true,
},
actionBar: {
actions: [],
},
}}
minDate={dayjs(new Date()).subtract(props.minDate || 90, 'day')}
maxDate={dayjs(new Date())}
onChange={([dateStart$, dateEnd$]) => {
setDateStart(dateStart$);
setDateEnd(dateEnd$);
}}
/>
</div>
</DialogContent>
<DialogActions>
<Button size="small" variant="contained" color="primary" onClick={handleDateSubmit}>
Apply
</Button>
</DialogActions>
</Dialog>
);
}
DialogCalendarRange.propTypes = {
open: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
onDateSubmit: PropTypes.func.isRequired,
minDate: PropTypes.number,
};
export default DialogCalendarRange;

View File

@@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
module.exports = {"Wrapper":"GlobalStateEmpty_Wrapper__S8zu7","ImageWrapper":"GlobalStateEmpty_ImageWrapper__e5pMt","Image":"GlobalStateEmpty_Image__RfO6c","Description":"GlobalStateEmpty_Description__zYOSa","CtaWrapper":"GlobalStateEmpty_CtaWrapper__t0nTZ","Button":"GlobalStateEmpty_Button__ltHZe"};

View File

@@ -0,0 +1 @@
export default {"src":"/_next/static/media/img.c6068819.png","height":496,"width":794,"blurDataURL":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAMAAABPT11nAAAARVBMVEVU1OkseI41i55Ju9BP0OYXSmhDoK5MaXFNxdlR0uRm//9Pvc4A//9Z4fY5mrJ///8bUnEmZXxa5/86l6c9or1GuNFGt87Jv1WKAAAAFHRSTlP9/fv2UZb8AKNgBZQCrZYC/JKYlO9xIIkAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAxSURBVHicBcEHAsAgCACxU0Ggy9Hq/5/aBLvEPQ7DTgBvSK2UvIOZ05f6upF3qOoTPxniAUkMOkARAAAAAElFTkSuQmCC","blurWidth":8,"blurHeight":5};

View File

@@ -0,0 +1,47 @@
import React from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { EDITORIAL_PAGE } from '@/modules/@common/router/constants';
import RoundItemContainer from '../RoundItemContainer';
import { Button, Grid, Typography } from '@/mui/components';
import img from './img/img.png';
import styles from './GlobalStateEmpty.module.scss';
function StateEmpty() {
const router = useRouter();
return (
<RoundItemContainer
container
justifyContent="center"
alignItems="center"
direction="column"
className={styles.Wrapper}
>
<Grid item className={styles.ImageWrapper}>
<Image src={img} alt="test" className={styles.Image} />
</Grid>
<Grid item className={styles.Description}>
<Typography variant="h6" color="text.secondary" fontWeight={400} component="div">
Use cutting-edge analytics to identify new trends and measure indicators to make decisions confidently. <br />{' '}
Start uploading, viewing, sharing, commenting and searching videos!
</Typography>
</Grid>
<Grid item alignItems="center" className={styles.CtaWrapper}>
<Button
className={styles.Button}
variant="contained"
size="small"
onClick={() => router.push(EDITORIAL_PAGE.path)}
>
<span>Let&apos;s Get Started</span>
</Button>
</Grid>
</RoundItemContainer>
);
}
export default StateEmpty;

View File

@@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
module.exports = {"Wrapper":"GlobalStateEmpty_Wrapper__8V0Cd"};

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { WIDGET_DISPLAY_STATE_ERROR } from '@/modules/analytics/common/constants';
import RoundItemContainer from '../RoundItemContainer';
import styles from './GlobalStateEmpty.module.scss';
function GlobalStateError() {
return <RoundItemContainer displayState={WIDGET_DISPLAY_STATE_ERROR} container className={styles.Wrapper} />;
}
export default GlobalStateError;

View File

@@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
module.exports = {"Header":"Header_Header__2RXVu"};

View File

@@ -0,0 +1,58 @@
import React from 'react';
import PropTypes from 'prop-types';
import { PictureAsPdfRounded } from '@mui/icons-material';
import { PDF_EXPORT_HIDE_CONTENT } from '@/modules/analytics/common/constants';
import { CircularProgress, Grid, IconButton, Typography } from '@/mui/components';
import { CustomCsvFilled } from '@/mui/components/CustomIcon';
import styles from './Header.module.scss';
function Header(props) {
return (
<Grid className={styles.Header} container justifyContent="space-between" alignItems="center">
<Grid item>
<Typography variant="h5" color="text.primary" fontWeight={400}>
{props.children}
</Typography>
</Grid>
<div className={styles.Buttons} data-pdf-export={PDF_EXPORT_HIDE_CONTENT}>
<IconButton
disabled={props.disabledExport || props.isExportPdfLoading}
onClick={props.onExportToPdf}
size="small"
>
{props.isExportPdfLoading ? (
<CircularProgress color="info" size={14} />
) : (
<PictureAsPdfRounded fontSize="large" htmlColor="info" />
)}
</IconButton>
<IconButton
disabled={props.disabledExport || props.isExportCsvLoading}
onClick={props.onExportToCsv}
size="small"
>
{props.isExportCsvLoading ? (
<CircularProgress color="info" size={14} />
) : (
<CustomCsvFilled fontSize="large" color="info" />
)}
</IconButton>
</div>
</Grid>
);
}
Header.propTypes = {
children: PropTypes.node.isRequired,
onExportToPdf: PropTypes.func.isRequired,
onExportToCsv: PropTypes.func.isRequired,
isExportCsvLoading: PropTypes.bool.isRequired,
isExportPdfLoading: PropTypes.bool.isRequired,
disabledExport: PropTypes.bool.isRequired,
};
export default Header;

View File

@@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
module.exports = {"Layout":"Layout_Layout__gtqQi","Content":"Layout_Content__eroPg"};

View File

@@ -0,0 +1,22 @@
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'clsx';
import { Grid } from '@/mui/components';
import styles from './Layout.module.scss';
const Layout = forwardRef(({ contentClassName = null, ...props }, ref) => (
<Grid container className={styles.Layout}>
<Grid className={classNames(styles.Content, contentClassName)} ref={ref}>
{props.children}
</Grid>
</Grid>
));
Layout.propTypes = {
contentClassName: PropTypes.string,
children: PropTypes.node.isRequired,
};
export default Layout;

View File

@@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
module.exports = {"Wrapper":"RoundItemContainer_Wrapper__3xXcA","ResetPadding":"RoundItemContainer_ResetPadding__ZtEhe"};

View File

@@ -0,0 +1,74 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'clsx';
import {
WIDGET_DISPLAY_STATE_DATA,
WIDGET_DISPLAY_STATE_ERROR,
WIDGET_DISPLAY_STATE_NO_DATA,
} from '@/modules/analytics/common/constants';
import Stub from '@/modules/analytics/common/components/Stub';
import { Grid } from '@/mui/components';
import styles from './RoundItemContainer.module.scss';
function RoundItemContainer({
className = '',
displayState = WIDGET_DISPLAY_STATE_DATA,
type = 'vertical',
size = 'medium',
...props
}) {
const getDisplayItem = {
[WIDGET_DISPLAY_STATE_DATA]: props.children,
[WIDGET_DISPLAY_STATE_NO_DATA]: (
<Stub
imageType={WIDGET_DISPLAY_STATE_NO_DATA}
type={type}
size={size}
firstTitle="No Data..."
firstSubTitle={
size === 'large'
? 'No Data matching selected filters. Try adjusting your filters.'
: 'No Data matching selected filters.'
}
secondSubTitle={size === 'large' ? null : 'Try adjusting your filters.'}
/>
),
[WIDGET_DISPLAY_STATE_ERROR]: (
<Stub
imageType={WIDGET_DISPLAY_STATE_ERROR}
type={type}
size={size}
firstTitle={size === 'medium' || size === 'large' ? 'Error has occured...' : 'Error'}
secondTitle={size === 'medium' || size === 'large' ? null : 'has occured...'}
firstSubTitle={size === 'large' ? 'Something went wrong. Try adjusting your filters.' : 'Something went wrong.'}
secondSubTitle={size === 'large' ? null : 'Try adjusting your filters.'}
/>
),
};
const shouldResetPadding = [WIDGET_DISPLAY_STATE_NO_DATA, WIDGET_DISPLAY_STATE_ERROR].includes(displayState);
return (
<Grid
{...props}
className={classNames(styles.Wrapper, className, {
[styles.ResetPadding]: shouldResetPadding,
})}
>
{getDisplayItem[displayState]}
</Grid>
);
}
RoundItemContainer.propTypes = {
children: PropTypes.node.isRequired,
className: PropTypes.string,
displayState: PropTypes.oneOf([WIDGET_DISPLAY_STATE_DATA, WIDGET_DISPLAY_STATE_NO_DATA, WIDGET_DISPLAY_STATE_ERROR]),
type: PropTypes.oneOf(['horizontal', 'vertical']),
size: PropTypes.oneOf(['small', 'medium', 'large']),
};
export default RoundItemContainer;

View File

@@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
module.exports = {"Wrapper":"Stub_Wrapper__t2zzO","Wrapper_vertical":"Stub_Wrapper_vertical__QC6bX","Wrapper_horizontal":"Stub_Wrapper_horizontal__TJUoi","ImageWrapper":"Stub_ImageWrapper__T8R_J","ImageWrapper_medium":"Stub_ImageWrapper_medium__SORuu","ImageWrapper_large":"Stub_ImageWrapper_large__HbY9h","Image":"Stub_Image__et7s7","TextWrapper":"Stub_TextWrapper__mHeve","TextWrapper_vertical":"Stub_TextWrapper_vertical__9MKBq","Title":"Stub_Title__bu82q","Title_vertical":"Stub_Title_vertical__RkNBR","Title_medium":"Stub_Title_medium__yHPB1","Title_large":"Stub_Title_large__d9tCM","Subtitle":"Stub_Subtitle__dm_cw","Subtitle_horizontal":"Stub_Subtitle_horizontal__2Jt5h","Subtitle_medium":"Stub_Subtitle_medium__xtfo1","Subtitle_large":"Stub_Subtitle_large__klY9u"};

View File

@@ -0,0 +1 @@
export default {"src":"/_next/static/media/imgError.8e71741c.png","height":169,"width":416,"blurDataURL":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAADCAMAAACZFr56AAAAElBMVEVMaXGnp9+Zpcy3v9qBktV/f6M1we1ZAAAABnRSTlMACyE7PQ6klpETAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAHUlEQVR4nGNgYGBiZYACJhZGBgYGRkYmZmZmRkYAAVwAHnlLK7MAAAAASUVORK5CYII=","blurWidth":8,"blurHeight":3};

View File

@@ -0,0 +1 @@
export default {"src":"/_next/static/media/imgNoData.f0cfbdf7.png","height":186,"width":417,"blurDataURL":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAECAMAAACEE47CAAAAGFBMVEVqarV8bMWAksrd5O7h4+vv7PPKyN7q7PN0o8d6AAAACHRSTlMJARxMg7J/8iXq8DQAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAiSURBVHicY2BkZGRgABGMjEwgGsRjYmNhYgAxmNlZGRgYAAK+AC8YPyUwAAAAAElFTkSuQmCC","blurWidth":8,"blurHeight":4};

View File

@@ -0,0 +1,132 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'clsx';
import Image from 'next/image';
import {
WIDGET_DISPLAY_STATE_DATA,
WIDGET_DISPLAY_STATE_ERROR,
WIDGET_DISPLAY_STATE_NO_DATA,
} from '@/modules/analytics/common/constants';
import { Grid } from '@/mui/components';
import imgError from './img/imgError.png';
import imgNoData from './img/imgNoData.png';
import styles from './Stub.module.scss';
function Stub({ secondTitle = null, secondSubTitle = null, ...props }) {
const getDisplayImg = {
[WIDGET_DISPLAY_STATE_NO_DATA]: imgNoData,
[WIDGET_DISPLAY_STATE_ERROR]: imgError,
};
return (
<Grid
container
className={classNames(styles.Wrapper, {
[styles.Wrapper_vertical]: props.type === 'vertical',
[styles.Wrapper_horizontal]: props.type === 'horizontal',
})}
>
<Grid
item
className={classNames(styles.ImageWrapper, {
[styles.ImageWrapper_small]: props.size === 'small',
[styles.ImageWrapper_medium]: props.size === 'medium',
[styles.ImageWrapper_large]: props.size === 'large',
})}
>
<Image
src={getDisplayImg[props.imageType]}
alt="stub"
className={styles.Image}
width={500}
height={500}
unoptimized
loading="eager"
/>
</Grid>
<Grid
item
className={classNames(styles.TextWrapper, {
[styles.TextWrapper_vertical]: props.type === 'vertical',
[styles.TextWrapper_horizontal]: props.type === 'horizontal',
})}
>
<Grid
item
className={classNames(styles.Title, {
[styles.Title_vertical]: props.type === 'vertical',
[styles.Title_horizontal]: props.type === 'horizontal',
[styles.Title_small]: props.size === 'small',
[styles.Title_medium]: props.size === 'medium',
[styles.Title_large]: props.size === 'large',
})}
>
{props.firstTitle}
</Grid>
{secondTitle && (
<Grid
item
className={classNames(styles.Title, {
[styles.Title_vertical]: props.type === 'vertical',
[styles.Title_horizontal]: props.type === 'horizontal',
[styles.Title_small]: props.size === 'small',
[styles.Title_medium]: props.size === 'medium',
[styles.Title_large]: props.size === 'large',
})}
>
{secondTitle}
</Grid>
)}
{props.firstSubTitle && (
<Grid
item
className={classNames(styles.Subtitle, {
[styles.Subtitle_vertical]: props.type === 'vertical',
[styles.Subtitle_horizontal]: props.type === 'horizontal',
[styles.Subtitle_small]: props.size === 'small',
[styles.Subtitle_medium]: props.size === 'medium',
[styles.Subtitle_large]: props.size === 'large',
})}
>
{props.firstSubTitle}
</Grid>
)}
{secondSubTitle && (
<Grid
item
className={classNames(styles.Subtitle, {
[styles.Subtitle_vertical]: props.type === 'vertical',
[styles.Subtitle_horizontal]: props.type === 'horizontal',
[styles.Subtitle_small]: props.size === 'small',
[styles.Subtitle_medium]: props.size === 'medium',
[styles.Subtitle_large]: props.size === 'large',
})}
>
{secondSubTitle}
</Grid>
)}
</Grid>
</Grid>
);
}
Stub.propTypes = {
type: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,
size: PropTypes.oneOf(['small', 'medium', 'large']).isRequired,
imageType: PropTypes.oneOf([WIDGET_DISPLAY_STATE_DATA, WIDGET_DISPLAY_STATE_NO_DATA, WIDGET_DISPLAY_STATE_ERROR])
.isRequired,
firstTitle: PropTypes.string.isRequired,
secondTitle: PropTypes.string,
firstSubTitle: PropTypes.string.isRequired,
secondSubTitle: PropTypes.string,
};
export default Stub;

View File

@@ -0,0 +1,10 @@
import AreaGraph from './AreaGraph';
import DialogCalendarRange from './DialogCalendarRange';
// info data states
import GlobalStateEmpty from './GlobalStateEmpty';
import GlobalStateError from './GlobalStateError';
import Header from './Header';
import Layout from './Layout';
import RoundItemContainer from './RoundItemContainer';
export { Layout, Header, RoundItemContainer, AreaGraph, DialogCalendarRange, GlobalStateEmpty, GlobalStateError };

View File

@@ -0,0 +1,52 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@/mui/components';
// Sorry for many !important
// This component used in parallel with PCN modules.
// PCN uses global styles for MUI and no way to override
function ConfirmDialog({ cancelButtonText = null, onCancel = null, isOpen = false, ...props }) {
useEffect(() => () => props.onClose?.(), []);
return (
<Dialog
open={isOpen}
onClose={props.onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle onClose={props.onClose} id="alert-dialog-title">
{props.title}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">{props.body}</DialogContentText>
</DialogContent>
<DialogActions>
{cancelButtonText && (
<Button color="primary" size="small" variant="outlined" onClick={onCancel || props.onClose}>
{cancelButtonText}
</Button>
)}
<Button size="small" color="primary" onClick={props.onConfirm}>
{props.confirmButtonText}
</Button>
</DialogActions>
</Dialog>
);
}
ConfirmDialog.propTypes = {
title: PropTypes.string.isRequired,
body: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]).isRequired,
isOpen: PropTypes.bool,
confirmButtonText: PropTypes.string.isRequired,
cancelButtonText: PropTypes.string,
onConfirm: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
onCancel: PropTypes.func,
};
export default ConfirmDialog;

View File

@@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
module.exports = {"Wrapper":"CustomReports_Wrapper__qdynA","Title":"CustomReports_Title__blde6","Controls":"CustomReports_Controls__x_2lE","Header":"CustomReports_Header__T_UZs","Filters":"CustomReports_Filters__jHFIu","Search":"CustomReports_Search__m8eEC","TableWrap":"CustomReports_TableWrap__WhDNM","RowButtons":"CustomReports_RowButtons__0KQEH","Row":"CustomReports_Row__ppZe6","Cell":"CustomReports_Cell__kEUmi","VisibilityHidden":"CustomReports_VisibilityHidden__nrIvc"};

View File

@@ -0,0 +1,279 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import dayjs from 'dayjs';
import timezonePlugin from 'dayjs/plugin/timezone';
import utcPlugin from 'dayjs/plugin/utc';
import { useRouter } from 'next/router';
import { AccessTime, Add, ContentCopy, DeleteRounded, Download, SearchRounded } from '@mui/icons-material';
import { NAME_MAX_LENGTH, TABLE_HEADERS } from '../constants';
import { SORT_ASC, SORT_DESC } from '@/modules/@common/constants/sort';
import * as selectors from '../redux/selectors';
import * as actions from '../redux/slices';
import { getUserEmailSelector, getUserTimezoneSelector } from '@/modules/@common/user/redux/selectors';
import {
Box,
Button,
IconButton,
InputAdornment,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TablePagination,
TableRow,
TableScroll,
TableSortLabel,
TextField,
Tooltip,
Typography,
} from '@/mui/components';
import styles from './CustomReports.module.scss';
dayjs.extend(utcPlugin);
dayjs.extend(timezonePlugin);
function CustomReports() {
const dispatch = useDispatch();
const userTimezone = useSelector(getUserTimezoneSelector);
const userEmail = useSelector(getUserEmailSelector);
const searchText = useSelector(selectors.searchTextSelector);
const page = useSelector(selectors.pageSelector);
const pageSize = useSelector(selectors.pageSizeSelector);
const sortBy = useSelector(selectors.sortBySelector);
const sortOrder = useSelector(selectors.sortOrderSelector);
const records = useSelector(selectors.recordsSelector);
const recordsTotal = useSelector(selectors.recordsTotalSelector);
const isDownloading = useSelector(selectors.isDownloadingSelector);
const router = useRouter();
const fetchData = (payload) => {
dispatch(actions.getCustomReportsAction(payload));
};
useEffect(() => {
fetchData();
}, []);
const handleRowClick = (id) => {
router.push(`/custom-reports-new/${id}`);
};
const onSearch = (value) => {
dispatch(
actions.setFieldAction({
searchText: value,
page: 0,
}),
);
fetchData(value);
};
const handleSorting = (id) => {
const isAsc = sortBy === id && sortOrder === 'ASC';
dispatch(
actions.setFieldAction({
sortOrder: isAsc ? 'DESC' : 'ASC',
sortBy: id,
page: 0,
}),
);
fetchData();
};
return (
<div className={styles.Wrapper}>
<Stack direction="column" fullWidth justifyContent="center" className={styles.Title}>
<Typography component="div" variant="h6" noWrap>
Custom Reports
</Typography>
</Stack>
<Stack
spacing={1}
fullWidth
direction="row"
alignItems="center"
justifyContent="space-between"
className={styles.Controls}
>
<TextField
className={styles.Search}
placeholder="Search by report name or owner"
value={searchText}
onChange={(event) => onSearch(event.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => onSearch(searchText)}>
<SearchRounded />
</IconButton>
</InputAdornment>
),
}}
variant="outlined"
size="small"
/>
<Stack spacing={2} justifyContent="flex-end" alignItems="center" direction="row">
<Button
data-id="add-new-report"
size="small"
onClick={() => {
router.push('/custom-reports-new/new');
}}
startIcon={<Add />}
>
Report
</Button>
</Stack>
</Stack>
<div className={styles.TableWrap}>
<TableContainer>
<TableScroll>
<Table aria-label="table">
<TableHead>
<TableRow>
{TABLE_HEADERS.map((header, index) => (
<TableCell key={`${header.id}-${index + 1}`} className={styles.Cell}>
{header.isSortable && (
<TableSortLabel
active={sortBy === header.id}
direction={sortBy === header.id ? sortOrder?.toLowerCase() : SORT_ASC}
onClick={() => {
handleSorting(header.id);
}}
>
{header.label}
{sortBy === header.id ? (
<Box component="span" className={styles.VisibilityHidden}>
{sortOrder === SORT_DESC ? 'sorted descending' : 'sorted ascending'}
</Box>
) : null}
</TableSortLabel>
)}
{!header.isSortable && header.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{records?.map(({ id, name, createdAt, updatedAt, createdBy, lastDelivery, nextDelivery }) => (
<TableRow
className={styles.Row}
key={id}
onClick={() => {
handleRowClick(id);
}}
>
<TableCell className={styles.Cell}>
{name?.length > NAME_MAX_LENGTH ? (
<Tooltip title={name} placement="top">
{`${name.slice(0, NAME_MAX_LENGTH)}...`}
</Tooltip>
) : (
name
)}
</TableCell>
<TableCell className={styles.Cell}>{createdBy}</TableCell>
<TableCell className={styles.Cell}>
{dayjs(createdAt).tz(userTimezone).format('MMM DD, YYYY, hh:mm A')}
</TableCell>
<TableCell className={styles.Cell}>
{updatedAt ? dayjs(updatedAt).tz(userTimezone).format('MMM DD, YYYY, hh:mm A') : ''}
</TableCell>
<TableCell>
<AccessTime htmlColor={nextDelivery ? 'success.main' : 'error.main'} />
</TableCell>
<TableCell className={styles.Cell}>
{lastDelivery ? dayjs(lastDelivery).tz(userTimezone).format('MMM DD, YYYY, hh:mm A') : ''}
</TableCell>
<TableCell className={styles.Cell}>
{nextDelivery ? dayjs(nextDelivery).tz(userTimezone).format('MMM DD, YYYY, hh:mm A') : ''}
</TableCell>
<TableCell>
<div className={styles.RowButtons}>
{userEmail === createdBy && (
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
dispatch(
actions.runDownloadReportAction({
id,
name,
requestFullInfo: true,
}),
);
}}
disabled={isDownloading}
>
<Download />
</IconButton>
)}
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
router.push(`/custom-reports-new/${id}?duplicate=true`);
}}
>
<ContentCopy />
</IconButton>
{userEmail === createdBy && (
<IconButton
size="small"
color="error"
onClick={(e) => {
e.stopPropagation();
dispatch(actions.deleteCustomReportAction({ id }));
}}
>
<DeleteRounded />
</IconButton>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableScroll>
<TablePagination
component="div"
rowsPerPageOptions={[10, 25, 50]}
count={recordsTotal}
rowsPerPage={pageSize}
page={page}
onPageChange={(event, newPage) => {
dispatch(actions.setFieldAction({ page: newPage }));
fetchData();
}}
onRowsPerPageChange={(event) => {
dispatch(
actions.setFieldAction({
pageSize: +event.target.value,
page: 0,
}),
);
fetchData();
}}
/>
</TableContainer>
</div>
</div>
);
}
export default CustomReports;

View File

@@ -0,0 +1,3 @@
import AnalyticsAdPerformanceDashboard from './components';
export default AnalyticsAdPerformanceDashboard;

View File

@@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
module.exports = {"Failure":"Failure_Failure__OknnO","FailureHeader":"Failure_FailureHeader__hI_wi","FailureText":"Failure_FailureText__DGySc"};

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { Grid } from '@/mui/components';
import styles from './Failure.module.scss';
function Failure() {
return (
<Grid container justifyContent="center" alignItems="center" direction="column" className={styles.Failure}>
<Grid item>
<h2 className={styles.FailureHeader}>Analytics is under maintenance.</h2>
</Grid>
<Grid item className={styles.FailureText}>
<span>Please contact your account manager for further information.</span>
</Grid>
</Grid>
);
}
export default Failure;

View File

@@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
module.exports = {"Wrapper":"General_Wrapper__i_2Ph","Iframe":"General_Iframe__5xFcc"};

View File

@@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
module.exports = {"SelectAccountWrapper":"InfoNeedSelectAccount_SelectAccountWrapper__Rqb_z","Logo":"InfoNeedSelectAccount_Logo__gfgnv","InfoText":"InfoNeedSelectAccount_InfoText__50EX6"};

View File

@@ -0,0 +1,21 @@
import React from 'react';
import Image from 'next/image';
import { Grid } from '@/mui/components';
import logo from '@/assets/img/logo.png';
import styles from './InfoNeedSelectAccount.module.scss';
function InfoNeedSelectAccount() {
return (
<Grid container alignContent="space-around" justifyContent="center">
<Grid item className={styles.SelectAccountWrapper}>
<Image src={logo} alt="logo" className={styles.Logo} width={200} height={50} />
<div className={styles.InfoText}>Select an Account</div>
</Grid>
</Grid>
);
}
export default InfoNeedSelectAccount;

View File

@@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
module.exports = {"Loader":"Loader_Loader__LNQ0B","ProgressRoot":"Loader_ProgressRoot__7ug0y","LoaderText":"Loader_LoaderText__PXXSd"};

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { CircularProgress, Grid } from '@/mui/components';
import styles from './Loader.module.scss';
function Loader() {
return (
<Grid container justifyContent="center" alignItems="center" className={styles.Loader}>
<Grid item>
<CircularProgress
classes={{
root: styles.ProgressRoot,
}}
/>
</Grid>
<Grid item className={styles.LoaderText}>
<span>Loading Dashboard...</span>
</Grid>
</Grid>
);
}
export default Loader;

Some files were not shown because too many files have changed in this diff Show More