Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import Script from "next/script";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||
|
||||
interface TurnstileRenderOptions {
|
||||
sitekey: string;
|
||||
action?: string;
|
||||
callback: (token: string) => void;
|
||||
"error-callback"?: () => void;
|
||||
"expired-callback"?: () => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
turnstile?: {
|
||||
render: (container: HTMLElement, options: TurnstileRenderOptions) => string;
|
||||
reset: (widgetId?: string) => void;
|
||||
remove: (widgetId?: string) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface TurnstileWidgetHandle {
|
||||
/** Tokens are single-use - call this after any failed submission before
|
||||
* retrying, or the retry is rejected as timeout-or-duplicate. */
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
interface TurnstileWidgetProps {
|
||||
/** Sourced live from useSystemStatus()'s turnstile_site_key - not a
|
||||
* build-time env var, so an admin-updated key (see the Settings page's
|
||||
* Server secrets box) takes effect without rebuilding the frontend. */
|
||||
siteKey: string | null | undefined;
|
||||
onToken: (token: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TurnstileWidget = forwardRef<TurnstileWidgetHandle, TurnstileWidgetProps>(
|
||||
function TurnstileWidget({ siteKey, onToken, className }, ref) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const widgetIdRef = useRef<string | null>(null);
|
||||
const onTokenRef = useRef(onToken);
|
||||
const [scriptLoaded, setScriptLoaded] = useState(
|
||||
() => typeof window !== "undefined" && !!window.turnstile,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onTokenRef.current = onToken;
|
||||
}, [onToken]);
|
||||
|
||||
// next/script's onLoad only reliably fires for the mount that actually
|
||||
// injects the tag - a later page that mounts this same widget after an
|
||||
// earlier page already loaded the Cloudflare script (same src, so Next
|
||||
// skips re-injecting it) can otherwise wait on an onLoad that never
|
||||
// comes. Poll briefly as a fallback for that case.
|
||||
useEffect(() => {
|
||||
if (scriptLoaded) return;
|
||||
const interval = setInterval(() => {
|
||||
if (window.turnstile) {
|
||||
setScriptLoaded(true);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
return () => clearInterval(interval);
|
||||
}, [scriptLoaded]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
reset: () => {
|
||||
if (window.turnstile && widgetIdRef.current) {
|
||||
window.turnstile.reset(widgetIdRef.current);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (!scriptLoaded || !containerRef.current || !window.turnstile || !siteKey) return;
|
||||
// Explicit render, not implicit auto-render, into our own ref'd div.
|
||||
const widgetId = window.turnstile.render(containerRef.current, {
|
||||
sitekey: siteKey,
|
||||
action: "turnstile-spin-v2",
|
||||
callback: (token) => onTokenRef.current(token),
|
||||
});
|
||||
widgetIdRef.current = widgetId;
|
||||
return () => {
|
||||
window.turnstile?.remove(widgetId);
|
||||
widgetIdRef.current = null;
|
||||
};
|
||||
}, [scriptLoaded, siteKey]);
|
||||
|
||||
if (!siteKey) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"
|
||||
strategy="afterInteractive"
|
||||
onLoad={() => setScriptLoaded(true)}
|
||||
/>
|
||||
<div ref={containerRef} className={className} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user