madebydanny.uk written in html, css, and a lot of JavaScript I don't understand madebydanny.uk
html css javascript
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

added cloudflare turnstile to /cdn

+721 -7
.DS_Store

This is a binary file and will not be displayed.

+36
cdn.html
··· 15 15 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 16 16 <link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;1,400&family=DM+Sans:wght@400;500&display=swap" rel="stylesheet"> 17 17 <script src="https://kit.fontawesome.com/0ca27f8db1.js" crossorigin="anonymous"></script> 18 + <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> 18 19 19 20 <style> 20 21 /* ── Design tokens (match main site) ── */ ··· 346 347 .upload-btn:hover { opacity: 0.85; transform: translateY(-1px); } 347 348 .upload-btn:active { transform: scale(0.99); } 348 349 .upload-btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none; } 350 + 351 + /* ── Turnstile ── */ 352 + .turnstile-wrap { 353 + display: block; 354 + margin-top: 1rem; 355 + margin-bottom: 1rem; 356 + } 357 + 358 + .cf-turnstile { 359 + display: flex; 360 + justify-content: center; 361 + } 362 + 363 + /* Cloudflare Turnstile dark mode styling */ 364 + .cf-turnstile iframe { 365 + filter: brightness(0.9); 366 + } 349 367 350 368 .status-msg { 351 369 text-align: center; ··· 714 732 <div class="progress-label" id="progress-label">0%</div> 715 733 </div> 716 734 735 + <div class="turnstile-wrap" id="turnstile-wrap"> 736 + <div class="cf-turnstile" id="cf-turnstile" data-sitekey="0x4AAAAAADElA2DcjN_ENLMX" data-theme="dark"></div> 737 + </div> 738 + 717 739 <button class="upload-btn" id="upload-btn"> 718 740 <i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN 719 741 </button> ··· 1007 1029 const statusEl = document.getElementById('status'); 1008 1030 const resultBox = document.getElementById('result-box'); 1009 1031 const resultUrl = document.getElementById('result-url'); 1032 + const turnstileWrap = document.getElementById('turnstile-wrap'); 1010 1033 1011 1034 function setStatus(msg, color) { 1012 1035 statusEl.textContent = msg; ··· 1026 1049 fileInfo.classList.remove('show'); 1027 1050 progressWrap.classList.remove('show'); 1028 1051 setProgress(0); 1052 + // Reset Turnstile 1053 + if (window.turnstile) { 1054 + window.turnstile.reset(); 1055 + } 1029 1056 }, delay); 1030 1057 } 1031 1058 1032 1059 uploadBtn.addEventListener('click', () => { 1033 1060 if (!fileInput.files[0]) { 1034 1061 setStatus('Please select a file first.', 'var(--red)'); 1062 + return; 1063 + } 1064 + 1065 + // Get Turnstile token 1066 + const token = window.turnstile?.getResponse?.(); 1067 + 1068 + if (!token) { 1069 + setStatus('Please complete the verification.', 'var(--red)'); 1035 1070 return; 1036 1071 } 1037 1072 ··· 1080 1115 1081 1116 xhr.open('POST', API); 1082 1117 xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream'); 1118 + xhr.setRequestHeader('X-Turnstile-Token', token); 1083 1119 xhr.send(file); 1084 1120 }); 1085 1121
+674
imrs.html
··· 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>IMRS — Image Resizing Service · madebydanny.uk</title> 7 + <meta name="description" content="A free image resizing and AVIF conversion service built on Cloudflare Workers. Pass any image URL and get it back optimised."> 8 + <meta name="author" content="Daniel Morrisey"> 9 + <meta name="robots" content="index, follow"> 10 + 11 + <!-- Open Graph --> 12 + <meta property="og:title" content="IMRS — Image Resizing Service · madebydanny.uk"> 13 + <meta property="og:description" content="A free image resizing and AVIF conversion service built on Cloudflare Workers."> 14 + <meta property="og:type" content="website"> 15 + <meta property="og:url" content="https://imrs.madebydanny.uk/"> 16 + 17 + <link rel="canonical" href="https://imrs.madebydanny.uk/"> 18 + <link rel="icon" type="image/png" href="https://cdn.blueat.net/img/avatar/plain/did:plc:l37td5yhxl2irrzrgvei4qay/bafkreidfielr2nk5xr4v2odm5bth5yjk5y536ex5qpxsheq2ocl2qobwt4"> 19 + 20 + <link rel="preconnect" href="https://fonts.googleapis.com"> 21 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 22 + <link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;1,400&family=DM+Sans:wght@400;500&display=swap" rel="stylesheet"> 23 + <script src="https://kit.fontawesome.com/0ca27f8db1.js" crossorigin="anonymous"></script> 24 + 25 + <style> 26 + :root { 27 + --bg: #0e0d0c; 28 + --bg-raised: #171512; 29 + --bg-card: #1a1815; 30 + --border: #2d2926; 31 + --border-hover:#4a4238; 32 + --text: #e8e0d8; 33 + --text-muted: #8a7f74; 34 + --text-dim: #584f47; 35 + --accent: #c9a96e; 36 + --accent-dim: rgba(201, 169, 110, 0.12); 37 + --green: #4caf7d; 38 + --red: #e06c6c; 39 + --font-serif: 'Lora', Georgia, serif; 40 + --font-sans: 'DM Sans', system-ui, sans-serif; 41 + --font-mono: 'Monaco', 'Courier New', monospace; 42 + --radius: 10px; 43 + --transition: 0.2s ease; 44 + --max-w: 680px; 45 + } 46 + 47 + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 48 + html { scroll-behavior: smooth; } 49 + 50 + body { 51 + font-family: var(--font-sans); 52 + font-size: 16px; 53 + line-height: 1.7; 54 + color: var(--text); 55 + background-color: var(--bg); 56 + -webkit-font-smoothing: antialiased; 57 + } 58 + 59 + h1, h2, h3 { font-family: var(--font-serif); font-weight: 400; line-height: 1.2; } 60 + 61 + a { color: var(--accent); text-decoration: none; transition: opacity var(--transition); } 62 + a:hover { opacity: 0.75; } 63 + 64 + code { 65 + font-family: var(--font-mono); 66 + font-size: 0.875em; 67 + background: var(--bg-raised); 68 + padding: 0.15em 0.45em; 69 + border-radius: 4px; 70 + color: var(--accent); 71 + word-break: break-all; 72 + } 73 + 74 + /* ── Header ── */ 75 + 76 + .site-header { 77 + position: sticky; 78 + top: 0; 79 + z-index: 100; 80 + background: rgba(14, 13, 12, 0.92); 81 + backdrop-filter: blur(12px); 82 + border-bottom: 1px solid var(--border); 83 + } 84 + 85 + .nav-container { 86 + max-width: var(--max-w); 87 + margin: 0 auto; 88 + padding: 0.875rem 1.5rem; 89 + display: flex; 90 + justify-content: space-between; 91 + align-items: center; 92 + } 93 + 94 + .nav-logo { 95 + font-family: var(--font-serif); 96 + font-style: italic; 97 + font-size: 1.1rem; 98 + color: var(--text-muted); 99 + } 100 + .nav-logo:hover { color: var(--text); opacity: 1; } 101 + 102 + .nav-title { 103 + font-family: var(--font-serif); 104 + font-size: 1rem; 105 + color: var(--text); 106 + } 107 + 108 + /* ── Main ── */ 109 + 110 + .main-content { 111 + max-width: var(--max-w); 112 + margin: 0 auto; 113 + padding: 0 1.5rem; 114 + } 115 + 116 + /* ── Hero ── */ 117 + 118 + .hero { padding: 4rem 0 3rem; } 119 + 120 + .hero-eyebrow { 121 + font-size: 0.875rem; 122 + color: var(--text-muted); 123 + margin-bottom: 0.25rem; 124 + letter-spacing: 0.04em; 125 + } 126 + 127 + .hero-title { 128 + font-size: clamp(2.5rem, 6vw, 3.5rem); 129 + color: var(--text); 130 + margin-bottom: 1.25rem; 131 + letter-spacing: -0.02em; 132 + } 133 + 134 + .hero-bio { 135 + font-size: 1.0625rem; 136 + color: var(--text-muted); 137 + line-height: 1.8; 138 + max-width: 560px; 139 + margin-bottom: 1.5rem; 140 + } 141 + 142 + .hero-bio a { 143 + color: var(--accent); 144 + border-bottom: 1px solid var(--accent-dim); 145 + transition: border-color var(--transition), opacity var(--transition); 146 + } 147 + .hero-bio a:hover { opacity: 1; border-bottom-color: var(--accent); } 148 + 149 + .hero-badges { 150 + display: flex; 151 + flex-wrap: wrap; 152 + gap: 0.5rem; 153 + } 154 + 155 + .badge { 156 + display: inline-flex; 157 + align-items: center; 158 + gap: 0.4rem; 159 + font-size: 0.8125rem; 160 + color: var(--text-muted); 161 + background: var(--bg-raised); 162 + border: 1px solid var(--border); 163 + padding: 0.25rem 0.875rem; 164 + border-radius: 999px; 165 + letter-spacing: 0.01em; 166 + } 167 + 168 + .badge i { font-size: 0.75rem; color: var(--accent); } 169 + 170 + /* ── Sections ── */ 171 + 172 + .content-section { 173 + padding: 2.5rem 0; 174 + border-top: 1px solid var(--border); 175 + } 176 + 177 + .section-label { 178 + font-family: var(--font-sans); 179 + font-size: 0.75rem; 180 + font-weight: 500; 181 + text-transform: uppercase; 182 + letter-spacing: 0.1em; 183 + color: var(--text-dim); 184 + margin-bottom: 1.25rem; 185 + } 186 + 187 + /* ── Try it card ── */ 188 + 189 + .try-card { 190 + background: var(--bg-card); 191 + border: 1px solid var(--border); 192 + border-radius: var(--radius); 193 + overflow: hidden; 194 + } 195 + 196 + .try-card-body { padding: 1.25rem; } 197 + 198 + .url-row { 199 + display: flex; 200 + gap: 0.5rem; 201 + } 202 + 203 + .url-input { 204 + flex: 1; 205 + background: var(--bg-raised); 206 + border: 1px solid var(--border); 207 + border-radius: 999px; 208 + padding: 0.6rem 1rem; 209 + font-family: var(--font-mono); 210 + font-size: 0.8125rem; 211 + color: var(--text); 212 + outline: none; 213 + transition: border-color var(--transition); 214 + } 215 + .url-input::placeholder { color: var(--text-dim); } 216 + .url-input:focus { border-color: var(--accent); } 217 + 218 + .try-btn { 219 + background: var(--accent); 220 + color: var(--bg); 221 + border: none; 222 + border-radius: 999px; 223 + padding: 0.6rem 1.25rem; 224 + font-family: var(--font-sans); 225 + font-size: 0.875rem; 226 + font-weight: 500; 227 + cursor: pointer; 228 + white-space: nowrap; 229 + transition: opacity var(--transition); 230 + flex-shrink: 0; 231 + } 232 + .try-btn:hover { opacity: 0.85; } 233 + .try-btn:disabled { opacity: 0.4; cursor: not-allowed; } 234 + 235 + .try-result { 236 + display: none; 237 + margin-top: 1rem; 238 + } 239 + .try-result.show { display: block; } 240 + 241 + .result-img-wrap { 242 + border-radius: 8px; 243 + overflow: hidden; 244 + border: 1px solid var(--border); 245 + margin-bottom: 0.875rem; 246 + background: var(--bg-raised); 247 + min-height: 120px; 248 + display: flex; 249 + align-items: center; 250 + justify-content: center; 251 + } 252 + 253 + .result-img-wrap img { 254 + width: 100%; 255 + height: auto; 256 + max-height: 360px; 257 + object-fit: contain; 258 + display: block; 259 + } 260 + 261 + .result-url-row { 262 + display: flex; 263 + align-items: center; 264 + gap: 0.5rem; 265 + } 266 + 267 + .result-url-box { 268 + flex: 1; 269 + font-family: var(--font-mono); 270 + font-size: 0.775rem; 271 + padding: 0.5rem 0.875rem; 272 + background: var(--bg-raised); 273 + border: 1px solid var(--border); 274 + border-radius: 8px; 275 + color: var(--text-muted); 276 + word-break: break-all; 277 + line-height: 1.5; 278 + } 279 + 280 + .copy-btn { 281 + background: var(--bg-raised); 282 + border: 1px solid var(--border); 283 + border-radius: 999px; 284 + padding: 0.5rem 1rem; 285 + font-family: var(--font-sans); 286 + font-size: 0.8125rem; 287 + color: var(--text-muted); 288 + cursor: pointer; 289 + white-space: nowrap; 290 + transition: border-color var(--transition), color var(--transition); 291 + flex-shrink: 0; 292 + } 293 + .copy-btn:hover { border-color: var(--accent); color: var(--accent); } 294 + .copy-btn.copied { background: var(--green); border-color: var(--green); color: var(--bg); } 295 + 296 + .try-error { 297 + display: none; 298 + font-size: 0.875rem; 299 + color: var(--red); 300 + margin-top: 0.75rem; 301 + } 302 + .try-error.show { display: block; } 303 + 304 + .try-card-meta { 305 + font-size: 0.8125rem; 306 + color: var(--text-dim); 307 + padding: 0.75rem 1.25rem; 308 + border-top: 1px solid var(--border); 309 + } 310 + 311 + /* ── Usage examples ── */ 312 + 313 + .usage-list { 314 + display: flex; 315 + flex-direction: column; 316 + gap: 1.25rem; 317 + } 318 + 319 + .usage-item { } 320 + 321 + .usage-item h3 { 322 + font-family: var(--font-sans); 323 + font-size: 0.875rem; 324 + font-weight: 500; 325 + color: var(--text); 326 + margin-bottom: 0.375rem; 327 + } 328 + 329 + .usage-item p { 330 + font-size: 0.875rem; 331 + color: var(--text-muted); 332 + margin-bottom: 0.5rem; 333 + line-height: 1.6; 334 + } 335 + 336 + .code-block { 337 + background: var(--bg-card); 338 + border: 1px solid var(--border); 339 + border-radius: var(--radius); 340 + padding: 0.875rem 1rem; 341 + font-family: var(--font-mono); 342 + font-size: 0.8rem; 343 + color: var(--text-muted); 344 + overflow-x: auto; 345 + line-height: 1.7; 346 + white-space: pre; 347 + } 348 + 349 + .code-block .hl { color: var(--accent); } 350 + 351 + /* ── How it works ── */ 352 + 353 + .steps { 354 + display: flex; 355 + flex-direction: column; 356 + } 357 + 358 + .step { 359 + display: flex; 360 + gap: 1rem; 361 + padding: 1rem 0; 362 + border-bottom: 1px solid var(--border); 363 + } 364 + .step:last-child { border-bottom: none; } 365 + 366 + .step-num { 367 + flex-shrink: 0; 368 + width: 1.75rem; 369 + height: 1.75rem; 370 + border-radius: 50%; 371 + background: var(--bg-card); 372 + border: 1px solid var(--border); 373 + display: flex; 374 + align-items: center; 375 + justify-content: center; 376 + font-size: 0.75rem; 377 + font-weight: 500; 378 + color: var(--accent); 379 + margin-top: 0.1rem; 380 + } 381 + 382 + .step-body h3 { 383 + font-family: var(--font-serif); 384 + font-size: 0.9375rem; 385 + font-weight: 400; 386 + color: var(--text); 387 + margin-bottom: 0.2rem; 388 + } 389 + .step-body p { 390 + font-size: 0.875rem; 391 + color: var(--text-muted); 392 + line-height: 1.7; 393 + } 394 + 395 + /* ── Response info grid ── */ 396 + 397 + .info-grid { 398 + display: grid; 399 + grid-template-columns: repeat(2, 1fr); 400 + gap: 0.75rem; 401 + } 402 + 403 + .info-card { 404 + background: var(--bg-card); 405 + border: 1px solid var(--border); 406 + border-radius: var(--radius); 407 + padding: 1.125rem; 408 + transition: border-color var(--transition); 409 + } 410 + .info-card:hover { border-color: var(--border-hover); } 411 + 412 + .info-card i { 413 + font-size: 1rem; 414 + color: var(--accent); 415 + opacity: 0.8; 416 + margin-bottom: 0.5rem; 417 + display: block; 418 + } 419 + 420 + .info-card h3 { 421 + font-family: var(--font-sans); 422 + font-size: 0.875rem; 423 + font-weight: 500; 424 + color: var(--text); 425 + margin-bottom: 0.25rem; 426 + } 427 + 428 + .info-card p { 429 + font-size: 0.8125rem; 430 + color: var(--text-muted); 431 + line-height: 1.6; 432 + } 433 + 434 + /* ── Footer ── */ 435 + 436 + .site-footer { 437 + max-width: var(--max-w); 438 + margin: 0 auto; 439 + padding: 2.5rem 1.5rem 3rem; 440 + border-top: 1px solid var(--border); 441 + font-size: 0.8125rem; 442 + color: var(--text-dim); 443 + line-height: 1.8; 444 + } 445 + 446 + .site-footer a { color: var(--text-muted); } 447 + .site-footer a:hover { color: var(--text); opacity: 1; } 448 + .footer-tor { margin-top: 0.25rem; } 449 + 450 + /* ── Responsive ── */ 451 + 452 + @media (max-width: 600px) { 453 + .hero { padding: 2.5rem 0 2rem; } 454 + .info-grid { grid-template-columns: 1fr; } 455 + .url-row { flex-direction: column; } 456 + .try-btn { border-radius: var(--radius); } 457 + } 458 + </style> 459 + </head> 460 + <body> 461 + 462 + <header class="site-header"> 463 + <nav class="nav-container"> 464 + <a href="https://madebydanny.uk" class="nav-logo">← danny</a> 465 + <span class="nav-title">IMRS</span> 466 + </nav> 467 + </header> 468 + 469 + <main class="main-content"> 470 + 471 + <!-- Hero --> 472 + <section class="hero"> 473 + <p class="hero-eyebrow">madebydanny.uk</p> 474 + <h1 class="hero-title">IMRS</h1> 475 + <p class="hero-bio"> 476 + Image media reduction system, easily transform images built on <a href="https://workers.cloudflare.com" target="_blank">Cloudflare Workers</a>. 477 + Pass any public image URL and get it back converted to AVIF — smaller, faster, cached at the edge. 478 + </p> 479 + <div class="hero-badges"> 480 + <span class="badge"><i class="fa-brands fa-cloudflare"></i> Cloudflare Workers</span> 481 + <span class="badge"><i class="fa-solid fa-bolt"></i> Edge cached</span> 482 + <span class="badge"><i class="fa-solid fa-image"></i> AVIF output</span> 483 + </div> 484 + </section> 485 + 486 + <!-- Try it --> 487 + <section class="content-section"> 488 + <h2 class="section-label">try it</h2> 489 + <div class="try-card"> 490 + <div class="try-card-body"> 491 + <div class="url-row"> 492 + <input 493 + class="url-input" 494 + id="img-url-input" 495 + type="url" 496 + placeholder="https://example.com/image.jpg" 497 + autocomplete="off" 498 + spellcheck="false" 499 + > 500 + <button class="try-btn" id="try-btn" onclick="tryUrl()"> 501 + <i class="fa-solid fa-arrow-right"></i> Resize 502 + </button> 503 + </div> 504 + <p class="try-error" id="try-error"></p> 505 + <div class="try-result" id="try-result"> 506 + <div class="result-img-wrap"> 507 + <img id="result-img" src="" alt="Resized image"> 508 + </div> 509 + <div class="result-url-row"> 510 + <div class="result-url-box" id="result-url"></div> 511 + <button class="copy-btn" id="copy-btn" onclick="copyResult()"> 512 + <i class="fa-solid fa-copy"></i> Copy 513 + </button> 514 + </div> 515 + </div> 516 + </div> 517 + <div class="try-card-meta">Processed images are cached for 1 year at Cloudflare's edge</div> 518 + </div> 519 + </section> 520 + 521 + <!-- Usage --> 522 + <section class="content-section"> 523 + <h2 class="section-label">usage</h2> 524 + <div class="usage-list"> 525 + <div class="usage-item"> 526 + <h3>Query parameter</h3> 527 + <p>Pass the image URL as a <code>?url=</code> parameter.</p> 528 + <div class="code-block"><span class="hl">https://imrs.madebydanny.uk</span>?url=https://example.com/photo.jpg</div> 529 + </div> 530 + <div class="usage-item"> 531 + <h3>Path passthrough</h3> 532 + <p>Append the full URL directly after the origin.</p> 533 + <div class="code-block"><span class="hl">https://imrs.madebydanny.uk/</span>https://example.com/photo.jpg</div> 534 + </div> 535 + <div class="usage-item"> 536 + <h3>In an HTML image tag</h3> 537 + <p>Wrap any <code>src</code> to serve AVIF automatically to supporting browsers.</p> 538 + <div class="code-block">&lt;img src="<span class="hl">https://imrs.madebydanny.uk/?url=</span>https://example.com/photo.jpg"&gt;</div> 539 + </div> 540 + </div> 541 + </section> 542 + 543 + <!-- How it works --> 544 + <section class="content-section"> 545 + <h2 class="section-label">how it works</h2> 546 + <div class="steps"> 547 + <div class="step"> 548 + <div class="step-num">1</div> 549 + <div class="step-body"> 550 + <h3>You pass an image URL</h3> 551 + <p>Either via <code>?url=</code> or as a path suffix. The URL must be a publicly accessible image.</p> 552 + </div> 553 + </div> 554 + <div class="step"> 555 + <div class="step-num">2</div> 556 + <div class="step-body"> 557 + <h3>The Worker fetches and transforms it</h3> 558 + <p>Cloudflare's image processing pipeline converts the image to AVIF at 85% quality, scaled down to fit its original dimensions.</p> 559 + </div> 560 + </div> 561 + <div class="step"> 562 + <div class="step-num">3</div> 563 + <div class="step-body"> 564 + <h3>You get a cached AVIF back</h3> 565 + <p>The response is served with <code>Cache-Control: immutable, max-age=31536000</code> — one year at the edge. Subsequent requests for the same URL are instant.</p> 566 + </div> 567 + </div> 568 + </div> 569 + </section> 570 + 571 + <!-- Response headers --> 572 + <section class="content-section"> 573 + <h2 class="section-label">response</h2> 574 + <div class="info-grid"> 575 + <div class="info-card"> 576 + <i class="fa-solid fa-file-image"></i> 577 + <h3>Content-Type</h3> 578 + <p><code>image/avif</code> — regardless of source format.</p> 579 + </div> 580 + <div class="info-card"> 581 + <i class="fa-solid fa-clock"></i> 582 + <h3>Cache-Control</h3> 583 + <p>Public, immutable, 1-year TTL. Served from edge on repeat requests.</p> 584 + </div> 585 + <div class="info-card"> 586 + <i class="fa-solid fa-globe"></i> 587 + <h3>CORS</h3> 588 + <p><code>Access-Control-Allow-Origin: *</code> — safe to use from any origin.</p> 589 + </div> 590 + <div class="info-card"> 591 + <i class="fa-solid fa-compress"></i> 592 + <h3>Quality</h3> 593 + <p>85% AVIF quality. Typically 50–70% smaller than an equivalent JPEG.</p> 594 + </div> 595 + </div> 596 + </section> 597 + 598 + </main> 599 + 600 + <footer class="site-footer"> 601 + <p>© 2024–26 Daniel Morrisey · <a href="https://madebydanny.uk" target="_blank">madebydanny.uk</a> · built on <a href="https://workers.cloudflare.com" target="_blank">Cloudflare Workers</a></p> 602 + <p class="footer-tor"><a href="http://irgwdhat74pqcpkk7ynrphvohnnt574yvwmhredrfusemgu6wj2ik5id.onion/" target="_blank" rel="noopener noreferrer">Open in Tor</a></p> 603 + </footer> 604 + 605 + <script> 606 + const BASE = 'https://imrs.madebydanny.uk'; 607 + 608 + function tryUrl() { 609 + const input = document.getElementById('img-url-input'); 610 + const btn = document.getElementById('try-btn'); 611 + const result = document.getElementById('try-result'); 612 + const errEl = document.getElementById('try-error'); 613 + const imgEl = document.getElementById('result-img'); 614 + const urlBox = document.getElementById('result-url'); 615 + 616 + const raw = input.value.trim(); 617 + 618 + errEl.textContent = ''; 619 + errEl.classList.remove('show'); 620 + result.classList.remove('show'); 621 + 622 + if (!raw) { 623 + errEl.textContent = 'Please enter an image URL.'; 624 + errEl.classList.add('show'); 625 + return; 626 + } 627 + 628 + if (!raw.startsWith('http')) { 629 + errEl.textContent = 'URL must start with http:// or https://'; 630 + errEl.classList.add('show'); 631 + return; 632 + } 633 + 634 + btn.disabled = true; 635 + btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>'; 636 + 637 + const proxied = `${BASE}/?url=${encodeURIComponent(raw)}`; 638 + urlBox.textContent = proxied; 639 + 640 + imgEl.onload = () => { 641 + result.classList.add('show'); 642 + btn.disabled = false; 643 + btn.innerHTML = '<i class="fa-solid fa-arrow-right"></i> Resize'; 644 + }; 645 + 646 + imgEl.onerror = () => { 647 + errEl.textContent = 'Could not load image. Check the URL is publicly accessible.'; 648 + errEl.classList.add('show'); 649 + btn.disabled = false; 650 + btn.innerHTML = '<i class="fa-solid fa-arrow-right"></i> Resize'; 651 + }; 652 + 653 + imgEl.src = proxied; 654 + } 655 + 656 + document.getElementById('img-url-input').addEventListener('keydown', e => { 657 + if (e.key === 'Enter') tryUrl(); 658 + }); 659 + 660 + function copyResult() { 661 + const url = document.getElementById('result-url').textContent; 662 + const btn = document.getElementById('copy-btn'); 663 + navigator.clipboard.writeText(url); 664 + btn.classList.add('copied'); 665 + btn.innerHTML = '<i class="fa-solid fa-check"></i> Copied'; 666 + setTimeout(() => { 667 + btn.classList.remove('copied'); 668 + btn.innerHTML = '<i class="fa-solid fa-copy"></i> Copy'; 669 + }, 2000); 670 + } 671 + </script> 672 + 673 + </body> 674 + </html>
+4
index.html
··· 188 188 <span class="grid-link-title">Tweets 2 Bsky</span> 189 189 <span class="grid-link-desc">Mirror X accounts to Bluesky</span> 190 190 </a> 191 + <a href="/imrs.html" class="grid-link"> 192 + <span class="grid-link-title">Tweets 2 Bsky</span> 193 + <span class="grid-link-desc">Mirror X accounts to Bluesky</span> 194 + </a> 191 195 </div> 192 196 </section> 193 197
+7 -7
js/photos.js
··· 2 2 3 3 const PHOTOS = [ 4 4 { 5 - src: 'https://cloudflareisawesome.madebydanny.uk/photos-upload-1/20240622_235041500_iOS.jpg', 5 + src: 'https://imrs.madebydanny.uk/https://cloudflareisawesome.madebydanny.uk/photos-upload-1/20240622_235041500_iOS.jpg', 6 6 title: 'DSC_3811.jpg', 7 7 date: '2025-10-14' 8 8 }, 9 9 { 10 - src: 'https://cloudflareisawesome.madebydanny.uk/photos-upload-1/DSC_3773.jpg', 10 + src: 'https://imrs.madebydanny.uk/https://cloudflareisawesome.madebydanny.uk/photos-upload-1/DSC_3773.jpg', 11 11 title: 'DSC_3773.jpg', 12 12 date: '2025-10-14' 13 13 }, 14 14 { 15 - src: 'https://cloudflareisawesome.madebydanny.uk/photos-upload-1/DSC_3766.jpg', 15 + src: 'https://imrs.madebydanny.uk/https://cloudflareisawesome.madebydanny.uk/photos-upload-1/DSC_3766.jpg', 16 16 title: 'DSC_3766.jpg', 17 17 date: '2025-10-14' 18 18 }, 19 19 { 20 - src: 'https://cloudflareisawesome.madebydanny.uk/photos-upload-1/DSC_3765.jpg', 20 + src: 'https://imrs.madebydanny.uk/https://cloudflareisawesome.madebydanny.uk/photos-upload-1/DSC_3765.jpg', 21 21 title: 'DSC_3765.jpg', 22 22 date: '2025-10-14' 23 23 }, 24 24 { 25 - src: 'https://cloudflareisawesome.madebydanny.uk/photos-upload-1/DSC_3704.jpg', 25 + src: 'https://imrs.madebydanny.uk/https://cloudflareisawesome.madebydanny.uk/photos-upload-1/DSC_3704.jpg', 26 26 title: 'DSC_3704.jpg', 27 27 date: '2025-10-14' 28 28 }, 29 29 { 30 - src: 'https://cloudflareisawesome.madebydanny.uk/photos-upload-1/DSC_3692.jpg', 30 + src: 'https://imrs.madebydanny.uk/https://cloudflareisawesome.madebydanny.uk/photos-upload-1/DSC_3692.jpg', 31 31 title: 'DSC_3692.jpg', 32 32 date: '2025-10-14' 33 33 }, 34 34 { 35 - src: 'https://cloudflareisawesome.madebydanny.uk/photos-upload-1/20240622_234015800_iOS.jpg', 35 + src: 'https://imrs.madebydanny.uk/https://cloudflareisawesome.madebydanny.uk/photos-upload-1/20240622_234015800_iOS.jpg', 36 36 title: 'DSC_3683.jpg', 37 37 date: '2025-10-14' 38 38 }