@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.
- package/LICENSE +22 -0
- package/README.md +359 -0
- package/config/config.example.js +118 -0
- package/config/config.js +101 -0
- package/docs/api/index.html +34 -0
- package/docs/api/openapi.yaml +150 -0
- package/docs/index.html +728 -0
- package/package.json +52 -0
- package/src/worker.example.js +376 -0
package/docs/index.html
ADDED
|
@@ -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>
|