@prosopo/client-bundle-example 2.5.1 → 2.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,454 @@
1
+ // Copyright 2021-2025 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ // Vite plugin to inject navigation into HTML files
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import {
18
+ type IndexHtmlTransformContext,
19
+ type Plugin,
20
+ normalizePath,
21
+ } from "vite";
22
+
23
+ interface CaptchaTypeDetails {
24
+ path: string;
25
+ exists: boolean;
26
+ }
27
+
28
+ interface CaptchaTypes {
29
+ [key: string]: {
30
+ [key: string]: CaptchaTypeDetails;
31
+ };
32
+ }
33
+
34
+ export default function navigationInjector(): Plugin {
35
+ // Define all pages, including existing and planned
36
+ const pageDefinitions = {
37
+ standard: {
38
+ image: {
39
+ implicit: { path: "index.html", exists: true },
40
+ explicit: { path: "image-explicit.html", exists: false },
41
+ },
42
+ pow: {
43
+ implicit: { path: "pow-implicit.html", exists: false },
44
+ explicit: { path: "pow-explicit.html", exists: false },
45
+ },
46
+ frictionless: {
47
+ implicit: {
48
+ path: "frictionless-implicit.html",
49
+ exists: false,
50
+ },
51
+ explicit: { path: "frictionless-explicit.html", exists: true },
52
+ },
53
+ },
54
+ invisible: {
55
+ image: {
56
+ implicit: { path: "invisible-image-implicit.html", exists: true },
57
+ explicit: { path: "invisible-image-explicit.html", exists: true },
58
+ },
59
+ pow: {
60
+ implicit: { path: "invisible-pow-implicit.html", exists: true },
61
+ explicit: { path: "invisible-pow-explicit.html", exists: true },
62
+ },
63
+ frictionless: {
64
+ implicit: {
65
+ path: "invisible-frictionless-implicit.html",
66
+ exists: true,
67
+ },
68
+ explicit: {
69
+ path: "invisible-frictionless-explicit.html",
70
+ exists: true,
71
+ },
72
+ },
73
+ },
74
+ };
75
+
76
+ // Style for the navigation bar
77
+ const navStyle = `
78
+ <style>
79
+ .nav-topbar {
80
+ background-color: #2196F3;
81
+ padding: 0;
82
+ margin-bottom: 20px;
83
+ transition: all 0.3s ease;
84
+ overflow: hidden;
85
+ max-height: 500px; /* Initial state - visible */
86
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
87
+ }
88
+ .nav-topbar.collapsed {
89
+ max-height: 50px; /* Collapsed state - only show toggle button and title */
90
+ }
91
+ .nav-container {
92
+ max-width: 1200px;
93
+ margin: 0 auto;
94
+ padding: 0 15px;
95
+ position: relative;
96
+ }
97
+ .nav-header {
98
+ display: flex;
99
+ justify-content: space-between;
100
+ align-items: center;
101
+ padding: 10px 0;
102
+ position: relative;
103
+ z-index: 20;
104
+ }
105
+ .nav-title {
106
+ color: white;
107
+ font-size: 18px;
108
+ font-weight: bold;
109
+ margin: 0;
110
+ display: flex;
111
+ align-items: center;
112
+ }
113
+ .nav-title-text {
114
+ margin-right: 10px;
115
+ }
116
+ .nav-title-hint {
117
+ font-size: 14px;
118
+ font-weight: normal;
119
+ opacity: 0;
120
+ transition: opacity 0.3s ease;
121
+ }
122
+ .collapsed .nav-title-hint {
123
+ opacity: 0.8;
124
+ }
125
+ .nav-toggle {
126
+ background-color: rgba(0,0,0,0.1);
127
+ color: white;
128
+ border: none;
129
+ padding: 5px 10px;
130
+ cursor: pointer;
131
+ border-radius: 4px;
132
+ z-index: 10;
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ }
137
+ .nav-toggle:hover {
138
+ background-color: rgba(0,0,0,0.2);
139
+ }
140
+ .nav-toggle-icon {
141
+ width: 24px;
142
+ height: 24px;
143
+ display: inline-block;
144
+ position: relative;
145
+ }
146
+ .nav-toggle-icon svg {
147
+ position: absolute;
148
+ top: 0;
149
+ left: 0;
150
+ transition: opacity 0.3s ease;
151
+ }
152
+ .nav-toggle-icon .icon-collapse {
153
+ opacity: 1;
154
+ }
155
+ .nav-toggle-icon .icon-expand {
156
+ opacity: 0;
157
+ }
158
+ .collapsed .nav-toggle-icon .icon-collapse {
159
+ opacity: 0;
160
+ }
161
+ .collapsed .nav-toggle-icon .icon-expand {
162
+ opacity: 1;
163
+ }
164
+ .nav-content {
165
+ padding: 0 0 15px 0;
166
+ transition: opacity 0.3s ease;
167
+ }
168
+ .collapsed .nav-content {
169
+ opacity: 0;
170
+ }
171
+ .nav-row {
172
+ display: flex;
173
+ flex-wrap: wrap;
174
+ margin-bottom: 15px;
175
+ }
176
+ .nav-group {
177
+ flex: 1;
178
+ min-width: 250px;
179
+ margin-bottom: 10px;
180
+ margin-right: 20px;
181
+ }
182
+ .nav-group-title {
183
+ color: rgba(255,255,255,0.8);
184
+ font-size: 0.9em;
185
+ margin: 0 0 5px 0;
186
+ text-transform: uppercase;
187
+ font-weight: 500;
188
+ }
189
+ .nav-group-links {
190
+ list-style: none;
191
+ margin: 0;
192
+ padding: 0;
193
+ }
194
+ .nav-group-links li {
195
+ margin: 5px 0;
196
+ }
197
+ .nav-group-links a {
198
+ color: white;
199
+ text-decoration: none;
200
+ font-weight: bold;
201
+ padding: 5px 0;
202
+ display: inline-block;
203
+ }
204
+ .nav-group-links a:hover {
205
+ text-decoration: underline;
206
+ }
207
+ .nav-group-links .active {
208
+ border-bottom: 2px solid white;
209
+ }
210
+ .nav-group-links .disabled {
211
+ color: rgba(255,255,255,0.5);
212
+ cursor: not-allowed;
213
+ }
214
+
215
+ @media (max-width: 768px) {
216
+ .nav-row {
217
+ flex-direction: column;
218
+ }
219
+ .nav-title-hint {
220
+ display: none;
221
+ }
222
+ }
223
+ </style>
224
+ `;
225
+
226
+ // JavaScript for toggling navigation
227
+ const navScript = `
228
+ <script>
229
+ document.addEventListener('DOMContentLoaded', function() {
230
+ const toggleBtn = document.getElementById('nav-toggle');
231
+ const navBar = document.getElementById('nav-topbar');
232
+ const navHeader = document.getElementById('nav-header');
233
+
234
+ // Check if navigation was previously collapsed
235
+ const navCollapsed = localStorage.getItem('navCollapsed') === 'true';
236
+ if (navCollapsed) {
237
+ navBar.classList.add('collapsed');
238
+ }
239
+
240
+ // Function to expand the navigation
241
+ function expandNav() {
242
+ if (navBar.classList.contains('collapsed')) {
243
+ navBar.classList.remove('collapsed');
244
+ localStorage.setItem('navCollapsed', 'false');
245
+ }
246
+ }
247
+
248
+ // Function to collapse the navigation
249
+ function collapseNav() {
250
+ if (!navBar.classList.contains('collapsed')) {
251
+ navBar.classList.add('collapsed');
252
+ localStorage.setItem('navCollapsed', 'true');
253
+ }
254
+ }
255
+
256
+ // Function to toggle the navigation
257
+ function toggleNav() {
258
+ navBar.classList.toggle('collapsed');
259
+ localStorage.setItem('navCollapsed', navBar.classList.contains('collapsed'));
260
+ }
261
+
262
+ // Toggle on button click
263
+ toggleBtn.addEventListener('click', function(e) {
264
+ e.stopPropagation(); // Prevent header click from firing
265
+ toggleNav();
266
+ });
267
+
268
+ // Expand on toggle button hover when collapsed
269
+ toggleBtn.addEventListener('mouseenter', function() {
270
+ if (navBar.classList.contains('collapsed')) {
271
+ expandNav();
272
+ }
273
+ });
274
+
275
+ // Toggle on header click
276
+ navHeader.addEventListener('click', function(e) {
277
+ // Don't toggle if a link within the header was clicked
278
+ if (e.target.tagName === 'A') {
279
+ return;
280
+ }
281
+ toggleNav();
282
+ });
283
+
284
+ // Make the header appear clickable with cursor style
285
+ navHeader.style.cursor = 'pointer';
286
+ });
287
+ </script>
288
+ `;
289
+
290
+ return {
291
+ name: "navigation-injector",
292
+ transformIndexHtml: {
293
+ enforce: "pre",
294
+ transform(html: string, ctx: IndexHtmlTransformContext): string {
295
+ // Get the relative path from the root
296
+ const relativePath = ctx.filename
297
+ ? normalizePath(path.relative(process.cwd(), ctx.filename))
298
+ : "";
299
+
300
+ // Extract the page path relative to src or dist directory
301
+ let pagePath = "";
302
+ const srcDir = "src/";
303
+ const distDir = "dist/";
304
+
305
+ if (relativePath.includes(srcDir)) {
306
+ pagePath = relativePath.substring(
307
+ relativePath.indexOf(srcDir) + srcDir.length,
308
+ );
309
+ } else if (relativePath.includes(distDir)) {
310
+ pagePath = relativePath.substring(
311
+ relativePath.indexOf(distDir) + distDir.length,
312
+ );
313
+ }
314
+
315
+ console.log(`Processing file: ${relativePath}, pagePath: ${pagePath}`);
316
+
317
+ // Build navigation structure
318
+ const standardNav = buildCaptchaNavGroup(
319
+ "Standard Captchas",
320
+ pageDefinitions.standard,
321
+ pagePath,
322
+ );
323
+ const invisibleNav = buildCaptchaNavGroup(
324
+ "Invisible Captchas",
325
+ pageDefinitions.invisible,
326
+ pagePath,
327
+ );
328
+
329
+ const navHtml = `
330
+ <div id="nav-topbar" class="nav-topbar">
331
+ <div class="nav-container">
332
+ <div id="nav-header" class="nav-header">
333
+ <h1 class="nav-title">
334
+ <span class="nav-title-text">Procaptcha Demo Playground</span>
335
+ <span class="nav-title-hint">(Click anywhere to see more examples)</span>
336
+ </h1>
337
+ <button id="nav-toggle" class="nav-toggle" aria-label="Toggle navigation">
338
+ <span class="nav-toggle-icon">
339
+ <svg class="icon-collapse" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
340
+ <polyline points="18 15 12 9 6 15"></polyline>
341
+ </svg>
342
+ <svg class="icon-expand" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
343
+ <polyline points="6 9 12 15 18 9"></polyline>
344
+ </svg>
345
+ </span>
346
+ </button>
347
+ </div>
348
+ <div class="nav-content">
349
+ <div class="nav-row">
350
+ ${standardNav}
351
+ ${invisibleNav}
352
+ </div>
353
+ </div>
354
+ </div>
355
+ </div>
356
+ `;
357
+
358
+ // Replace environment variables
359
+ const bundleUrl =
360
+ process.env.VITE_BUNDLE_URL ||
361
+ "https://staging-js.prosopo.io/js/procaptcha.bundle.js";
362
+
363
+ console.log(`Using bundle URL: ${bundleUrl}`);
364
+ html = html.replace(/%VITE_BUNDLE_URL%/g, bundleUrl);
365
+
366
+ // Replace other environment variables
367
+ if (process.env.PROSOPO_SITE_KEY) {
368
+ html = html.replace(
369
+ /%PROSOPO_SITE_KEY%/g,
370
+ process.env.PROSOPO_SITE_KEY,
371
+ );
372
+ }
373
+ if (process.env.PROSOPO_SERVER_URL) {
374
+ html = html.replace(
375
+ /%PROSOPO_SERVER_URL%/g,
376
+ process.env.PROSOPO_SERVER_URL,
377
+ );
378
+ }
379
+
380
+ // Inject navigation after <body> tag and script before </body>
381
+ if (html.includes("<body>") && html.includes("</body>")) {
382
+ return html
383
+ .replace("</head>", `${navStyle}</head>`)
384
+ .replace("<body>", `<body>\n${navHtml}`)
385
+ .replace("</body>", `${navScript}\n</body>`);
386
+ }
387
+
388
+ return html;
389
+ },
390
+ },
391
+ };
392
+
393
+ // Helper function to build a navigation group for each captcha type
394
+ function buildCaptchaNavGroup(
395
+ title: string,
396
+ captchaTypes: CaptchaTypes,
397
+ currentPath: string,
398
+ ): string {
399
+ const navGroups: string[] = [];
400
+
401
+ for (const [typeName, captchaType] of Object.entries(captchaTypes)) {
402
+ const typeTitle = typeName.charAt(0).toUpperCase() + typeName.slice(1);
403
+ const links: string[] = [];
404
+
405
+ for (const [implName, implDetails] of Object.entries(captchaType)) {
406
+ const pagePath = implDetails.path;
407
+ const pageExists = implDetails.exists;
408
+
409
+ // Calculate relative path
410
+ let href: string;
411
+ if (currentPath.includes("/")) {
412
+ // We're in a subdirectory
413
+ const depth = (currentPath.match(/\//g) || []).length;
414
+ href = "../".repeat(depth) + pagePath;
415
+ } else {
416
+ // We're in the src root
417
+ href = pagePath;
418
+ }
419
+
420
+ // Mark current page as active and check if the file exists
421
+ const isActive = currentPath === pagePath;
422
+ const implTitle = implName.charAt(0).toUpperCase() + implName.slice(1);
423
+
424
+ let linkHtml: string;
425
+ if (pageExists) {
426
+ const activeClass = isActive ? ' class="active"' : "";
427
+ linkHtml = `<a href="${href}"${activeClass}>${implTitle}</a>`;
428
+ } else {
429
+ linkHtml = `<span class="disabled" title="Coming Soon">${implTitle}</span>`;
430
+ }
431
+
432
+ links.push(`<li>${linkHtml}</li>`);
433
+ }
434
+
435
+ navGroups.push(`
436
+ <div class="nav-group">
437
+ <div class="nav-group-title">${typeTitle}</div>
438
+ <ul class="nav-group-links">
439
+ ${links.join("\n ")}
440
+ </ul>
441
+ </div>
442
+ `);
443
+ }
444
+
445
+ return `
446
+ <div class="nav-section">
447
+ <h3 style="color: white; margin: 0 0 10px 0;">${title}</h3>
448
+ <div class="nav-row">
449
+ ${navGroups.join("\n ")}
450
+ </div>
451
+ </div>
452
+ `;
453
+ }
454
+ }