Score artwork comments with local signatures plus Together AI, and optionally require Turnstile before posting.
182 lines
5.2 KiB
React
182 lines
5.2 KiB
React
import React, { useEffect, useRef } from 'react'
|
|
|
|
const providerAdapters = {
|
|
turnstile: {
|
|
globalName: 'turnstile',
|
|
render(api, container, { siteKey, theme, onToken }) {
|
|
return api.render(container, {
|
|
sitekey: siteKey,
|
|
theme,
|
|
appearance: 'always',
|
|
size: 'normal',
|
|
callback: (token) => onToken?.(token || ''),
|
|
'expired-callback': () => onToken?.(''),
|
|
'error-callback': () => onToken?.(''),
|
|
})
|
|
},
|
|
cleanup(api, widgetId, container, onToken) {
|
|
if (widgetId !== null && api?.remove) {
|
|
api.remove(widgetId)
|
|
}
|
|
if (container) {
|
|
container.innerHTML = ''
|
|
}
|
|
onToken?.('')
|
|
},
|
|
},
|
|
recaptcha: {
|
|
globalName: 'grecaptcha',
|
|
render(api, container, { siteKey, theme, onToken }) {
|
|
return api.render(container, {
|
|
sitekey: siteKey,
|
|
theme,
|
|
callback: (token) => onToken?.(token || ''),
|
|
'expired-callback': () => onToken?.(''),
|
|
'error-callback': () => onToken?.(''),
|
|
})
|
|
},
|
|
cleanup(api, widgetId, container, onToken) {
|
|
if (widgetId !== null && api?.reset) {
|
|
api.reset(widgetId)
|
|
}
|
|
if (container) {
|
|
container.innerHTML = ''
|
|
}
|
|
onToken?.('')
|
|
},
|
|
},
|
|
hcaptcha: {
|
|
globalName: 'hcaptcha',
|
|
render(api, container, { siteKey, theme, onToken }) {
|
|
return api.render(container, {
|
|
sitekey: siteKey,
|
|
theme,
|
|
callback: (token) => onToken?.(token || ''),
|
|
'expired-callback': () => onToken?.(''),
|
|
'error-callback': () => onToken?.(''),
|
|
})
|
|
},
|
|
cleanup(api, widgetId, container, onToken) {
|
|
if (widgetId !== null && api?.remove) {
|
|
api.remove(widgetId)
|
|
}
|
|
if (container) {
|
|
container.innerHTML = ''
|
|
}
|
|
onToken?.('')
|
|
},
|
|
},
|
|
}
|
|
|
|
function loadCaptchaScript(src) {
|
|
if (!src) {
|
|
return Promise.resolve()
|
|
}
|
|
|
|
if (!window.__skinbaseCaptchaScripts) {
|
|
window.__skinbaseCaptchaScripts = {}
|
|
}
|
|
|
|
if (!window.__skinbaseCaptchaScripts[src]) {
|
|
window.__skinbaseCaptchaScripts[src] = new Promise((resolve, reject) => {
|
|
const existing = document.querySelector(`script[src="${src}"]`)
|
|
|
|
if (existing) {
|
|
// The registration page may have loaded the same Turnstile script
|
|
// already. In that case its load event has fired before this promise
|
|
// was created, so waiting only for another load event would never
|
|
// resolve and the widget would remain an empty container.
|
|
if (window.turnstile || window.grecaptcha || window.hcaptcha) {
|
|
resolve()
|
|
return
|
|
}
|
|
|
|
if (existing.dataset.loaded === 'true') {
|
|
resolve()
|
|
return
|
|
}
|
|
|
|
existing.addEventListener('load', () => resolve(), { once: true })
|
|
existing.addEventListener('error', () => reject(new Error(`Failed to load captcha script: ${src}`)), { once: true })
|
|
return
|
|
}
|
|
|
|
const script = document.createElement('script')
|
|
script.src = src
|
|
script.async = true
|
|
script.defer = true
|
|
script.addEventListener('load', () => {
|
|
script.dataset.loaded = 'true'
|
|
resolve()
|
|
}, { once: true })
|
|
script.addEventListener('error', () => reject(new Error(`Failed to load captcha script: ${src}`)), { once: true })
|
|
document.head.appendChild(script)
|
|
})
|
|
}
|
|
|
|
return window.__skinbaseCaptchaScripts[src]
|
|
}
|
|
|
|
export default function TurnstileField({ provider = 'turnstile', siteKey, scriptUrl = '', onToken, theme = 'dark', className = '' }) {
|
|
const containerRef = useRef(null)
|
|
const widgetIdRef = useRef(null)
|
|
const onTokenRef = useRef(onToken)
|
|
|
|
useEffect(() => {
|
|
onTokenRef.current = onToken
|
|
}, [onToken])
|
|
|
|
useEffect(() => {
|
|
const adapter = providerAdapters[provider] || providerAdapters.turnstile
|
|
|
|
if (!siteKey || !containerRef.current) {
|
|
return undefined
|
|
}
|
|
|
|
let cancelled = false
|
|
let intervalId = null
|
|
|
|
const mountWidget = () => {
|
|
const api = window[adapter.globalName]
|
|
|
|
if (cancelled || !api?.render || widgetIdRef.current !== null) {
|
|
return
|
|
}
|
|
|
|
widgetIdRef.current = adapter.render(api, containerRef.current, {
|
|
siteKey,
|
|
theme,
|
|
onToken: (token) => onTokenRef.current?.(token),
|
|
})
|
|
}
|
|
|
|
loadCaptchaScript(scriptUrl).catch(() => onTokenRef.current?.('')).finally(() => {
|
|
const api = window[adapter.globalName]
|
|
// Do not call turnstile.ready() here. Cloudflare rejects ready() when
|
|
// api.js was loaded with async/defer (as it is on the registration
|
|
// page). Polling until render() exists works for both an already-loaded
|
|
// shared script and a script that is still loading.
|
|
mountWidget()
|
|
|
|
if (widgetIdRef.current === null) {
|
|
intervalId = window.setInterval(mountWidget, 250)
|
|
}
|
|
})
|
|
|
|
return () => {
|
|
cancelled = true
|
|
if (intervalId) {
|
|
window.clearInterval(intervalId)
|
|
}
|
|
adapter.cleanup(window[adapter.globalName], widgetIdRef.current, containerRef.current, (token) => onTokenRef.current?.(token))
|
|
widgetIdRef.current = null
|
|
}
|
|
}, [provider, scriptUrl, siteKey, theme])
|
|
|
|
if (!siteKey) {
|
|
return null
|
|
}
|
|
|
|
return <div ref={containerRef} className={className} />
|
|
}
|