@dytsou/shorten-url 2.2.6

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,728 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>URL Shortener</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 600px;
23
+ margin: 50px auto;
24
+ background: white;
25
+ border-radius: 20px;
26
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
27
+ padding: 40px;
28
+ }
29
+
30
+ h1 {
31
+ text-align: center;
32
+ color: #333;
33
+ margin-bottom: 30px;
34
+ font-size: 2.5em;
35
+ background: linear-gradient(45deg, #667eea, #764ba2);
36
+ -webkit-background-clip: text;
37
+ -webkit-text-fill-color: transparent;
38
+ background-clip: text;
39
+ }
40
+
41
+ .form-group {
42
+ margin-bottom: 20px;
43
+ }
44
+
45
+ label {
46
+ display: block;
47
+ margin-bottom: 8px;
48
+ color: #555;
49
+ font-weight: 500;
50
+ font-size: 14px;
51
+ }
52
+
53
+ input[type="url"],
54
+ input[type="text"] {
55
+ width: 100%;
56
+ padding: 15px;
57
+ border: 2px solid #e1e5e9;
58
+ border-radius: 10px;
59
+ font-size: 16px;
60
+ transition: border-color 0.3s ease;
61
+ }
62
+
63
+ input[type="url"]:focus,
64
+ input[type="text"]:focus {
65
+ outline: none;
66
+ border-color: #667eea;
67
+ }
68
+
69
+ .custom-slug-group {
70
+ position: relative;
71
+ }
72
+
73
+ .url-prefix {
74
+ display: block;
75
+ padding: 6px 10px;
76
+ background: #f8f9fa;
77
+ border: 2px solid #e1e5e9;
78
+ border-bottom: none;
79
+ border-radius: 10px 10px 0 0;
80
+ color: #666;
81
+ font-size: 12px;
82
+ font-weight: 400;
83
+ margin-bottom: 0;
84
+ word-break: break-all;
85
+ }
86
+
87
+ .custom-slug-input {
88
+ border-radius: 0 0 10px 10px !important;
89
+ border-top: none !important;
90
+ padding: 15px !important;
91
+ }
92
+
93
+ .custom-slug-input:focus + .url-prefix,
94
+ .custom-slug-input:focus ~ .url-prefix,
95
+ .custom-slug-group:focus-within .url-prefix {
96
+ border-color: #667eea;
97
+ background: #f0f3ff;
98
+ }
99
+
100
+ button {
101
+ width: 100%;
102
+ padding: 15px;
103
+ background: linear-gradient(45deg, #667eeac3, #764ba2ba);
104
+ color: white;
105
+ border: none;
106
+ border-radius: 10px;
107
+ font-size: 16px;
108
+ font-weight: 600;
109
+ cursor: pointer;
110
+ transition:
111
+ transform 0.2s ease,
112
+ box-shadow 0.2s ease;
113
+ }
114
+
115
+ button:hover {
116
+ transform: translateY(-2px);
117
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
118
+ }
119
+
120
+ button:active {
121
+ transform: translateY(0);
122
+ }
123
+
124
+ .button-group {
125
+ display: flex;
126
+ flex-direction: column;
127
+ gap: 15px;
128
+ }
129
+
130
+ .button-group button {
131
+ width: 100%;
132
+ }
133
+
134
+ .result {
135
+ margin-top: 30px;
136
+ padding: 20px;
137
+ background: #f8f9fa;
138
+ border-radius: 10px;
139
+ border-left: 4px solid #667eea;
140
+ display: none;
141
+ }
142
+
143
+ .result.show {
144
+ display: block;
145
+ }
146
+
147
+ .result h3 {
148
+ color: #333;
149
+ margin-bottom: 10px;
150
+ }
151
+
152
+ .shortened-url {
153
+ display: flex;
154
+ align-items: stretch;
155
+ gap: 10px;
156
+ margin-top: 10px;
157
+ flex-wrap: wrap;
158
+ }
159
+
160
+ .shortened-url input {
161
+ flex: 1;
162
+ min-width: 200px;
163
+ padding: 10px;
164
+ border: 1px solid #ddd;
165
+ border-radius: 5px;
166
+ background: white;
167
+ font-size: 14px;
168
+ }
169
+
170
+ .copy-btn {
171
+ padding: 10px 20px;
172
+ background: #28a745;
173
+ color: white;
174
+ border: none;
175
+ border-radius: 5px;
176
+ cursor: pointer;
177
+ font-size: 14px;
178
+ white-space: nowrap;
179
+ flex-shrink: 0;
180
+ transition: all 0.3s ease;
181
+ min-width: 80px;
182
+ }
183
+
184
+ .copy-btn:hover {
185
+ background: #218838;
186
+ transform: translateY(-1px);
187
+ }
188
+
189
+ .copy-btn:active {
190
+ transform: translateY(0);
191
+ }
192
+
193
+ .goto-btn {
194
+ padding: 15px;
195
+ background: linear-gradient(45deg, #6a64e4, #5039e4d7);
196
+ color: white;
197
+ border: none;
198
+ border-radius: 10px;
199
+ font-size: 16px;
200
+ font-weight: 600;
201
+ cursor: pointer;
202
+ transition:
203
+ transform 0.2s ease,
204
+ box-shadow 0.2s ease;
205
+ width: 100%;
206
+ }
207
+
208
+ .goto-btn:hover {
209
+ transform: translateY(-2px);
210
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
211
+ }
212
+
213
+ .goto-btn:active {
214
+ transform: translateY(0);
215
+ }
216
+
217
+ .error {
218
+ color: #dc3545;
219
+ background: #f8d7da;
220
+ border: 1px solid #f5c6cb;
221
+ padding: 10px;
222
+ border-radius: 5px;
223
+ margin-top: 10px;
224
+ display: none;
225
+ }
226
+
227
+ .error.show {
228
+ display: block;
229
+ }
230
+
231
+ .loading {
232
+ display: none;
233
+ text-align: center;
234
+ color: #666;
235
+ }
236
+
237
+ .loading.show {
238
+ display: block;
239
+ }
240
+
241
+ .help-text {
242
+ font-size: 12px;
243
+ color: #666;
244
+ margin-top: 5px;
245
+ }
246
+
247
+ @media (max-width: 768px) {
248
+ .container {
249
+ margin: 20px auto;
250
+ padding: 20px;
251
+ }
252
+
253
+ h1 {
254
+ font-size: 2em;
255
+ }
256
+
257
+ .url-prefix {
258
+ font-size: 12px;
259
+ padding: 6px 12px;
260
+ }
261
+
262
+ .shortened-url {
263
+ flex-direction: column;
264
+ gap: 15px;
265
+ }
266
+
267
+ .shortened-url input {
268
+ min-width: unset;
269
+ }
270
+
271
+ .copy-btn,
272
+ .goto-btn {
273
+ min-width: unset;
274
+ width: 100%;
275
+ }
276
+ }
277
+
278
+ @media (max-width: 480px) {
279
+ .container {
280
+ margin: 10px auto;
281
+ padding: 15px;
282
+ }
283
+
284
+ .url-prefix {
285
+ font-size: 11px;
286
+ padding: 5px 10px;
287
+ }
288
+
289
+ input[type="url"],
290
+ input[type="text"] {
291
+ padding: 12px;
292
+ font-size: 14px;
293
+ }
294
+
295
+ button {
296
+ padding: 12px;
297
+ font-size: 14px;
298
+ }
299
+ }
300
+ </style>
301
+ </head>
302
+
303
+ <body>
304
+ <div class="container">
305
+ <h1>URL Shortener</h1>
306
+
307
+ <form id="urlForm">
308
+ <div class="form-group">
309
+ <label for="longUrl">Enter your long URL:</label>
310
+ <input
311
+ type="url"
312
+ id="longUrl"
313
+ name="longUrl"
314
+ placeholder="https://example.com/very-long-url"
315
+ required
316
+ />
317
+ </div>
318
+
319
+ <div class="form-group">
320
+ <label for="customSlug">Custom short URL (optional):</label>
321
+ <div class="custom-slug-group">
322
+ <span class="url-prefix">yourdomain.com/</span>
323
+ <input
324
+ type="text"
325
+ id="customSlug"
326
+ name="customSlug"
327
+ class="custom-slug-input"
328
+ placeholder="my-custom-link"
329
+ pattern="[a-zA-Z0-9\-_]+"
330
+ maxlength="50"
331
+ />
332
+ </div>
333
+ <div class="help-text">
334
+ Leave empty for auto-generated short URL. Only letters, numbers, hyphens, and
335
+ underscores allowed.
336
+ </div>
337
+ </div>
338
+
339
+ <div class="button-group">
340
+ <button type="submit">Shorten URL</button>
341
+ <button type="button" class="goto-btn" onclick="shortenAndVisit()">
342
+ Shorten URL and Visit
343
+ </button>
344
+ </div>
345
+ </form>
346
+
347
+ <div class="loading" id="loading">
348
+ <p>Generating short URL...</p>
349
+ </div>
350
+
351
+ <div class="error" id="error"></div>
352
+
353
+ <div class="result" id="result">
354
+ <h3>Your shortened URL:</h3>
355
+ <div class="shortened-url">
356
+ <input type="text" id="shortUrlOutput" readonly />
357
+ <button class="copy-btn" onclick="copyToClipboard()">Copy</button>
358
+ </div>
359
+ </div>
360
+ </div>
361
+
362
+ <footer class="footer">
363
+ <nav>
364
+ <a href="https://github.com/dytsou/shorten-url" target="_blank" rel="noopener">GitHub</a>
365
+ <a href="api/">API</a>
366
+ </nav>
367
+ <div class="footer-copy">
368
+ © 2026 dytsou. Licensed under
369
+ <a
370
+ href="https://github.com/dytsou/shorten-url/blob/main/LICENSE"
371
+ target="_blank"
372
+ rel="noopener"
373
+ >MIT License</a
374
+ >.
375
+ </div>
376
+ </footer>
377
+
378
+ <style>
379
+ .footer {
380
+ margin-top: 40px;
381
+ padding: 30px 0 10px 0;
382
+ background: transparent;
383
+ color: #fff;
384
+ text-align: center;
385
+ border-radius: 0 0 20px 20px;
386
+ box-shadow: 0 -4px 24px rgba(102, 126, 234, 0.08);
387
+ }
388
+
389
+ .footer nav {
390
+ display: flex;
391
+ flex-wrap: wrap;
392
+ justify-content: center;
393
+ gap: 24px;
394
+ margin-bottom: 10px;
395
+ }
396
+
397
+ .footer nav a {
398
+ color: #fff;
399
+ text-decoration: none;
400
+ font-weight: 500;
401
+ opacity: 0.85;
402
+ transition:
403
+ opacity 0.2s,
404
+ text-decoration 0.2s;
405
+ }
406
+
407
+ .footer nav a:hover {
408
+ opacity: 1;
409
+ text-decoration: underline;
410
+ }
411
+
412
+ .footer-copy {
413
+ font-size: 0.95em;
414
+ opacity: 0.7;
415
+ }
416
+
417
+ .footer-copy a {
418
+ color: inherit;
419
+ text-decoration: none;
420
+ opacity: 1;
421
+ }
422
+
423
+ .footer-copy a:hover {
424
+ text-decoration: underline;
425
+ }
426
+
427
+ @media (max-width: 600px) {
428
+ .footer nav {
429
+ gap: 14px;
430
+ font-size: 0.98em;
431
+ }
432
+
433
+ .footer {
434
+ padding: 20px 0 8px 0;
435
+ }
436
+ }
437
+ </style>
438
+
439
+ <script>
440
+ // Get the current domain for the URL prefix
441
+ const currentDomain = window.location.host;
442
+ const urlPrefix = document.querySelector(".url-prefix");
443
+ const customSlugInput = document.getElementById("customSlug");
444
+
445
+ urlPrefix.textContent = currentDomain + "/";
446
+
447
+ // Helper to parse worker responses (JSON or text) and surface meaningful errors
448
+ async function parseWorkerResponse(response) {
449
+ const contentType = response.headers.get("content-type") || "";
450
+ let body = null;
451
+
452
+ if (contentType.includes("application/json")) {
453
+ try {
454
+ body = await response.json();
455
+ } catch (e) {
456
+ console.error("Failed to parse JSON response from worker:", e);
457
+ body = null;
458
+ }
459
+ } else {
460
+ try {
461
+ body = await response.text();
462
+ } catch (e) {
463
+ console.error("Failed to read text response from worker:", e);
464
+ body = null;
465
+ }
466
+ }
467
+
468
+ return body;
469
+ }
470
+
471
+ function getErrorMessageFromBody(body, status, defaultMsg) {
472
+ const prefix = typeof status === "number" ? `[HTTP ${status}] ` : "";
473
+ const baseDefault = prefix + defaultMsg;
474
+
475
+ if (body == null) return baseDefault;
476
+
477
+ if (typeof body === "string") {
478
+ const trimmed = body.trim();
479
+ return trimmed ? prefix + trimmed : baseDefault;
480
+ }
481
+
482
+ if (typeof body === "object") {
483
+ if (typeof body.message === "string" && body.message.trim() !== "") {
484
+ return prefix + body.message;
485
+ }
486
+ if (typeof body.key === "string" && body.key.trim() !== "") {
487
+ return prefix + body.key;
488
+ }
489
+ try {
490
+ return prefix + JSON.stringify(body);
491
+ } catch (e) {
492
+ console.error("Failed to stringify error body:", e);
493
+ return baseDefault;
494
+ }
495
+ }
496
+
497
+ return baseDefault;
498
+ }
499
+
500
+ document.getElementById("urlForm").addEventListener("submit", async function (e) {
501
+ e.preventDefault();
502
+
503
+ const longUrl = document.getElementById("longUrl").value;
504
+ const customSlug = document.getElementById("customSlug").value;
505
+
506
+ // Show loading, hide previous results
507
+ document.getElementById("loading").classList.add("show");
508
+ document.getElementById("result").classList.remove("show");
509
+ document.getElementById("error").classList.remove("show");
510
+
511
+ try {
512
+ // Prepare the request payload
513
+ const payload = {
514
+ url: longUrl,
515
+ };
516
+
517
+ // Add custom slug if provided
518
+ if (customSlug && customSlug.trim() !== "") {
519
+ payload.custom_slug = customSlug.trim();
520
+ }
521
+
522
+ // Make API call to your Cloudflare Worker
523
+ const response = await fetch(window.location.origin + "/shorten", {
524
+ method: "POST",
525
+ headers: {
526
+ "Content-Type": "application/json",
527
+ },
528
+ body: JSON.stringify(payload),
529
+ });
530
+
531
+ const result = await parseWorkerResponse(response);
532
+
533
+ // Hide loading
534
+ document.getElementById("loading").classList.remove("show");
535
+
536
+ if (response.ok && result && typeof result.short_url === "string") {
537
+ // Show success result using short_url from API
538
+ const shortUrl = result.short_url;
539
+ document.getElementById("shortUrlOutput").value = shortUrl;
540
+ document.getElementById("result").classList.add("show");
541
+ const linkWrapper = document.getElementById("shortUrlLinkWrapper");
542
+ const linkEl = document.getElementById("shortUrlLink");
543
+ if (linkWrapper && linkEl) {
544
+ linkEl.href = shortUrl;
545
+ linkEl.textContent = shortUrl;
546
+ linkWrapper.style.display = "block";
547
+ }
548
+
549
+ // Reset form
550
+ document.getElementById("urlForm").reset();
551
+ } else {
552
+ // Show error using worker-provided message when available
553
+ const errorMsg = getErrorMessageFromBody(
554
+ result,
555
+ response.status,
556
+ "Unable to shorten URL. Please check your input and try again."
557
+ );
558
+ document.getElementById("error").textContent = errorMsg;
559
+ document.getElementById("error").classList.add("show");
560
+ }
561
+ } catch (error) {
562
+ // Hide loading
563
+ document.getElementById("loading").classList.remove("show");
564
+
565
+ // Show error
566
+ document.getElementById("error").textContent = "Network error. Please try again.";
567
+ document.getElementById("error").classList.add("show");
568
+ }
569
+ });
570
+
571
+ // Modern copy to clipboard function with fallbacks
572
+ async function copyToClipboard() {
573
+ const shortUrlInput = document.getElementById("shortUrlOutput");
574
+ const copyBtn = document.querySelector(".copy-btn");
575
+ const originalText = copyBtn.textContent;
576
+ const originalBg = copyBtn.style.backgroundColor || "#28a745";
577
+
578
+ if (!shortUrlInput.value) {
579
+ showCopyFeedback(copyBtn, "No URL!", "#dc3545", originalText, originalBg);
580
+ return;
581
+ }
582
+
583
+ try {
584
+ // Try modern Clipboard API first
585
+ if (navigator.clipboard && window.isSecureContext) {
586
+ await navigator.clipboard.writeText(shortUrlInput.value);
587
+ showCopyFeedback(copyBtn, "Copied!", "#28a745", originalText, originalBg);
588
+ } else {
589
+ // Fallback: ask user to copy manually
590
+ shortUrlInput.select();
591
+ shortUrlInput.focus();
592
+ showCopyFeedback(copyBtn, "Select & Copy", "#ffc107", originalText, originalBg, 3000);
593
+ }
594
+ } catch (err) {
595
+ console.error("Failed to copy: ", err);
596
+
597
+ // Final fallback - show the URL and ask user to copy manually
598
+ shortUrlInput.select();
599
+ shortUrlInput.focus();
600
+ showCopyFeedback(copyBtn, "Select & Copy", "#ffc107", originalText, originalBg, 3000);
601
+ }
602
+ }
603
+
604
+ // Helper function for copy feedback
605
+ function showCopyFeedback(button, text, bgColor, originalText, originalBg, duration = 2000) {
606
+ button.textContent = text;
607
+ button.style.backgroundColor = bgColor;
608
+ button.style.transform = "scale(0.95)";
609
+
610
+ setTimeout(() => {
611
+ button.textContent = originalText;
612
+ button.style.backgroundColor = originalBg;
613
+ button.style.transform = "scale(1)";
614
+ }, duration);
615
+ }
616
+
617
+ // Shorten URL and Visit function
618
+ async function shortenAndVisit() {
619
+ const longUrl = document.getElementById("longUrl").value;
620
+ const customSlug = document.getElementById("customSlug").value;
621
+ const gotoBtn = document.querySelector(".goto-btn");
622
+ const originalText = gotoBtn.textContent;
623
+ const originalBg = gotoBtn.style.backgroundColor || "#007bff";
624
+
625
+ // Validate URL input
626
+ if (!longUrl) {
627
+ showGotoFeedback(gotoBtn, "Enter URL first!", "#dc3545", originalText, originalBg);
628
+ return;
629
+ }
630
+
631
+ // Show processing state
632
+ showGotoFeedback(gotoBtn, "Shortening...", "#ffc107", originalText, originalBg, 10000);
633
+
634
+ try {
635
+ // Prepare the request payload
636
+ const payload = {
637
+ url: longUrl,
638
+ };
639
+
640
+ // Add custom slug if provided
641
+ if (customSlug && customSlug.trim() !== "") {
642
+ payload.custom_slug = customSlug.trim();
643
+ }
644
+
645
+ // Make API call to shorten URL
646
+ const response = await fetch(window.location.origin + "/shorten", {
647
+ method: "POST",
648
+ headers: {
649
+ "Content-Type": "application/json",
650
+ },
651
+ body: JSON.stringify(payload),
652
+ });
653
+
654
+ const result = await parseWorkerResponse(response);
655
+
656
+ if (response.ok && result && typeof result.short_url === "string") {
657
+ // URL shortened successfully, now open it
658
+ const shortUrl = result.short_url;
659
+
660
+ // Open the shortened URL in a new tab
661
+ window.open(shortUrl, "_blank", "noopener,noreferrer");
662
+
663
+ // Show success feedback
664
+ showGotoFeedback(gotoBtn, "Opened!", "#28a745", originalText, originalBg);
665
+
666
+ // Reset form
667
+ document.getElementById("urlForm").reset();
668
+
669
+ // Also show the shortened URL in the result area
670
+ document.getElementById("shortUrlOutput").value = shortUrl;
671
+ document.getElementById("result").classList.add("show");
672
+
673
+ // Optionally update a clickable link if the elements exist in the DOM.
674
+ const linkWrapper = document.getElementById("shortUrlLinkWrapper");
675
+ const linkEl = document.getElementById("shortUrlLink");
676
+ if (linkWrapper && linkEl) {
677
+ linkEl.href = shortUrl;
678
+ linkEl.textContent = shortUrl;
679
+ linkWrapper.style.display = "block";
680
+ }
681
+
682
+ document.getElementById("error").classList.remove("show");
683
+ } else {
684
+ // Show error using worker-provided message when available
685
+ const errorMsg = getErrorMessageFromBody(
686
+ result,
687
+ response.status,
688
+ "Unable to shorten URL. Please verify the URL and try again."
689
+ );
690
+ showGotoFeedback(gotoBtn, "Error!", "#dc3545", originalText, originalBg);
691
+ document.getElementById("error").textContent = errorMsg;
692
+ document.getElementById("error").classList.add("show");
693
+ }
694
+ } catch (error) {
695
+ console.error("Network error:", error);
696
+ showGotoFeedback(gotoBtn, "Network Error!", "#dc3545", originalText, originalBg);
697
+ document.getElementById("error").textContent = "Network error. Please try again.";
698
+ document.getElementById("error").classList.add("show");
699
+ }
700
+ }
701
+
702
+ // Helper function for goto feedback
703
+ function showGotoFeedback(button, text, bgColor, originalText, originalBg, duration = 2000) {
704
+ button.textContent = text;
705
+ button.style.backgroundColor = bgColor;
706
+ button.style.transform = "scale(0.95)";
707
+
708
+ setTimeout(() => {
709
+ button.textContent = originalText;
710
+ button.style.backgroundColor = originalBg;
711
+ button.style.transform = "scale(1)";
712
+ }, duration);
713
+ }
714
+
715
+ // Custom slug validation
716
+ document.getElementById("customSlug").addEventListener("input", function (e) {
717
+ const value = e.target.value;
718
+ const validPattern = /^[a-zA-Z0-9\-_]*$/;
719
+
720
+ if (!validPattern.test(value)) {
721
+ e.target.setCustomValidity("Only letters, numbers, hyphens, and underscores are allowed");
722
+ } else {
723
+ e.target.setCustomValidity("");
724
+ }
725
+ });
726
+ </script>
727
+ </body>
728
+ </html>