Add reviews and support tutorial management
This commit is contained in:
@@ -598,6 +598,58 @@ html[data-theme] .sticky-toolbar {
|
||||
box-shadow: 0 10px 24px rgba(52, 34, 18, 0.08);
|
||||
}
|
||||
|
||||
html[data-theme] .html-preview-textarea {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html[data-theme] .html-preview-textarea__header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
html[data-theme] .html-preview-textarea__tabs {
|
||||
background: rgba(18, 18, 18, 0.06);
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
html[data-theme] .html-preview-textarea__tab {
|
||||
border-radius: 999px;
|
||||
color: rgba(18, 18, 18, 0.62);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
html[data-theme] .html-preview-textarea__tab[aria-selected='true'] {
|
||||
background: #111;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
html[data-theme] .html-preview-textarea__description {
|
||||
color: rgba(18, 18, 18, 0.55);
|
||||
font-size: 12px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
html[data-theme] .html-preview-textarea textarea {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
html[data-theme] .html-preview-textarea__preview {
|
||||
background: #fff;
|
||||
border: 1px solid rgba(18, 18, 18, 0.12);
|
||||
border-radius: 8px;
|
||||
min-height: 420px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html[data-theme] .array-field,
|
||||
html[data-theme] .blocks-field,
|
||||
html[data-theme] .collapsible-field,
|
||||
|
||||
@@ -26,6 +26,15 @@ const CACHE_PATHS = [
|
||||
'/zh/support/warranty',
|
||||
] as const
|
||||
|
||||
const CACHE_ROUTE_PATTERNS = [
|
||||
'/en/downloads/[slug]',
|
||||
'/zh/downloads/[slug]',
|
||||
'/en/news/[slug]',
|
||||
'/zh/news/[slug]',
|
||||
'/en/products/[slug]',
|
||||
'/zh/products/[slug]',
|
||||
] as const
|
||||
|
||||
function normalizeReturnPath(value: FormDataEntryValue | null) {
|
||||
if (typeof value !== 'string') return '/admin'
|
||||
if (!value.startsWith('/admin')) return '/admin'
|
||||
@@ -39,5 +48,9 @@ export async function clearFrontendCache(formData: FormData) {
|
||||
revalidatePath(path)
|
||||
}
|
||||
|
||||
for (const path of CACHE_ROUTE_PATTERNS) {
|
||||
revalidatePath(path, 'page')
|
||||
}
|
||||
|
||||
redirect(`${returnTo}${returnTo.includes('?') ? '&' : '?'}cache=cleared`)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const allowedHosts = new Set(['www.eversolo.com', 'eversolo.com'])
|
||||
const proxyPath = '/api/eversolo-paste-url'
|
||||
const pasteFetchCollectDelay = 300
|
||||
const pasteFetchIdleReleaseDelay = 250
|
||||
const pasteFetchRetryDelays = [0, 300, 800]
|
||||
const pasteFetchConcurrency = 4
|
||||
const pasteFetchPatchVersion = 4
|
||||
|
||||
type PatchedWindow = Window & {
|
||||
__eversoloPasteURLFetchPatchVersion?: number
|
||||
}
|
||||
|
||||
type QueuedPasteFetch = {
|
||||
init?: RequestInit
|
||||
proxiedURL: string
|
||||
reject: (reason?: unknown) => void
|
||||
resolve: (response: Response) => void
|
||||
result?: PromiseSettledResult<Response>
|
||||
source?: QueuedPasteFetch
|
||||
}
|
||||
|
||||
let activePasteFetches = 0
|
||||
let pasteFetchCollectTimer: number | undefined
|
||||
let pasteFetchReleaseTimer: number | undefined
|
||||
const pasteFetchPendingQueue: QueuedPasteFetch[] = []
|
||||
let pasteFetchSession: QueuedPasteFetch[] = []
|
||||
|
||||
function getFetchURL(input: RequestInfo | URL) {
|
||||
if (typeof input === 'string') return input
|
||||
if (input instanceof URL) return input.href
|
||||
|
||||
return input.url
|
||||
}
|
||||
|
||||
function getFetchMethod(input: RequestInfo | URL, init?: RequestInit) {
|
||||
if (init?.method) return init.method
|
||||
if (typeof input !== 'string' && !(input instanceof URL)) return input.method
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getProxiedPasteURL(input: RequestInfo | URL, init?: RequestInit) {
|
||||
const method = getFetchMethod(input, init)
|
||||
|
||||
if (method && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(getFetchURL(input))
|
||||
|
||||
if (url.protocol !== 'https:' || !allowedHosts.has(url.hostname)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return `${proxyPath}?src=${encodeURIComponent(url.href)}`
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function wait(delay: number) {
|
||||
return new Promise((resolve) => {
|
||||
window.setTimeout(resolve, delay)
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchWithRetry(
|
||||
originalFetch: typeof fetch,
|
||||
proxiedURL: string,
|
||||
init?: RequestInit,
|
||||
) {
|
||||
let lastError: unknown
|
||||
|
||||
for (const [index, delay] of pasteFetchRetryDelays.entries()) {
|
||||
if (delay > 0) {
|
||||
await wait(delay)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await originalFetch(proxiedURL, {
|
||||
...init,
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (response.ok || index === pasteFetchRetryDelays.length - 1) {
|
||||
return response
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError
|
||||
}
|
||||
|
||||
function releasePasteFetchSession() {
|
||||
if (activePasteFetches > 0 || pasteFetchPendingQueue.length > 0) return
|
||||
|
||||
const session = pasteFetchSession
|
||||
|
||||
pasteFetchSession = []
|
||||
pasteFetchReleaseTimer = undefined
|
||||
|
||||
session.forEach((item) => {
|
||||
const result = item.result ?? item.source?.result
|
||||
|
||||
if (!result) return
|
||||
|
||||
if (result.status === 'fulfilled') {
|
||||
item.resolve(result.value.clone())
|
||||
} else {
|
||||
item.reject(result.reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function schedulePasteFetchRelease() {
|
||||
if (activePasteFetches > 0 || pasteFetchPendingQueue.length > 0) return
|
||||
|
||||
if (pasteFetchReleaseTimer) {
|
||||
window.clearTimeout(pasteFetchReleaseTimer)
|
||||
}
|
||||
|
||||
pasteFetchReleaseTimer = window.setTimeout(() => {
|
||||
releasePasteFetchSession()
|
||||
}, pasteFetchIdleReleaseDelay)
|
||||
}
|
||||
|
||||
function processPasteFetchQueue(originalFetch: typeof fetch) {
|
||||
pasteFetchCollectTimer = undefined
|
||||
|
||||
while (activePasteFetches < pasteFetchConcurrency && pasteFetchPendingQueue.length > 0) {
|
||||
const item = pasteFetchPendingQueue.shift()
|
||||
|
||||
if (!item) continue
|
||||
|
||||
activePasteFetches += 1
|
||||
fetchWithRetry(originalFetch, item.proxiedURL, item.init)
|
||||
.then((response) => {
|
||||
item.result = {
|
||||
status: 'fulfilled',
|
||||
value: response,
|
||||
}
|
||||
})
|
||||
.catch((reason: unknown) => {
|
||||
item.result = {
|
||||
reason,
|
||||
status: 'rejected',
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
activePasteFetches -= 1
|
||||
processPasteFetchQueue(originalFetch)
|
||||
schedulePasteFetchRelease()
|
||||
})
|
||||
}
|
||||
|
||||
schedulePasteFetchRelease()
|
||||
}
|
||||
|
||||
function enqueuePasteFetch(originalFetch: typeof fetch, proxiedURL: string, init?: RequestInit) {
|
||||
return new Promise<Response>((resolve, reject) => {
|
||||
if (pasteFetchReleaseTimer) {
|
||||
window.clearTimeout(pasteFetchReleaseTimer)
|
||||
pasteFetchReleaseTimer = undefined
|
||||
}
|
||||
|
||||
const source = pasteFetchSession.find((pendingItem) => {
|
||||
return !pendingItem.source && pendingItem.proxiedURL === proxiedURL
|
||||
})
|
||||
const item: QueuedPasteFetch = {
|
||||
init,
|
||||
proxiedURL,
|
||||
reject,
|
||||
resolve,
|
||||
source,
|
||||
}
|
||||
|
||||
pasteFetchSession.push(item)
|
||||
|
||||
if (source) {
|
||||
schedulePasteFetchRelease()
|
||||
return
|
||||
}
|
||||
|
||||
pasteFetchPendingQueue.push(item)
|
||||
|
||||
if (pasteFetchCollectTimer) {
|
||||
window.clearTimeout(pasteFetchCollectTimer)
|
||||
}
|
||||
|
||||
pasteFetchCollectTimer = window.setTimeout(() => {
|
||||
processPasteFetchQueue(originalFetch)
|
||||
}, pasteFetchCollectDelay)
|
||||
})
|
||||
}
|
||||
|
||||
export function EversoloPasteURLFetchProvider({ children }: { children?: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
const patchedWindow = window as PatchedWindow
|
||||
|
||||
if (patchedWindow.__eversoloPasteURLFetchPatchVersion === pasteFetchPatchVersion) return
|
||||
|
||||
const originalFetch = window.fetch.bind(window)
|
||||
|
||||
patchedWindow.__eversoloPasteURLFetchPatchVersion = pasteFetchPatchVersion
|
||||
window.fetch = (input, init) => {
|
||||
const proxiedPasteURL = getProxiedPasteURL(input, init)
|
||||
|
||||
if (proxiedPasteURL) {
|
||||
return enqueuePasteFetch(originalFetch, proxiedPasteURL, init)
|
||||
}
|
||||
|
||||
return originalFetch(input, init)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return children
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { CompactArrayRowLabel as CompactArrayRowLabel_2254b697cb2592993885eb4171
|
||||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { TreeViewFeatureClient as TreeViewFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
@@ -23,6 +24,8 @@ import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HtmlPreviewTextareaField as HtmlPreviewTextareaField_753f6c383200cc3519583a44e0773d61 } from '@/payload/admin/HtmlPreviewTextareaField'
|
||||
import { EversoloPasteURLFetchProvider as EversoloPasteURLFetchProvider_a0494b42366ba3cb335226070f16d4b1 } from '@/app/(payload)/admin/_components/eversolo-paste-url-fetch-provider'
|
||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||
|
||||
/** @type import('payload').ImportMap */
|
||||
@@ -31,6 +34,7 @@ export const importMap = {
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/client#TreeViewFeatureClient": TreeViewFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
@@ -52,5 +56,7 @@ export const importMap = {
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@/payload/admin/HtmlPreviewTextareaField#HtmlPreviewTextareaField": HtmlPreviewTextareaField_753f6c383200cc3519583a44e0773d61,
|
||||
"@/app/(payload)/admin/_components/eversolo-paste-url-fetch-provider#EversoloPasteURLFetchProvider": EversoloPasteURLFetchProvider_a0494b42366ba3cb335226070f16d4b1,
|
||||
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user