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

removed old CDN file

-1046
-1046
cdn.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>MBD CDN - madebydanny.uk</title> 7 - <script src="https://kit.fontawesome.com/0ca27f8db1.js" crossorigin="anonymous"></script> 8 - <link rel="icon" href="https://public-cdn.madebydanny.uk/user-content/2026-01-30/33913bec-bc2f-4e6c-a474-2ef8f8c00197"> 9 - <meta name="description" content="The MBD CDN is a network of servers located around the world powered by the Cloudflare global network."> 10 - <meta property="og:title" content="MBD CDN - madebydanny.uk"> 11 - <meta property="og:description" content="The MBD CDN is a network of servers located around the world powered by the Cloudflare global network."> 12 - <meta property="og:image" content="https://imrs.madebydanny.uk?url=https://public-cdn.madebydanny.uk/user-content/2026-01-30/cb09a559-ae35-4617-971c-9230521f7a9c.png"> 13 - <meta property="og:type" content="website"> 14 - 15 - <style> 16 - :root { 17 - --bg: #121212; 18 - --card-bg: #4a2b32; 19 - --post-bg: #1e1e1e; 20 - --stat-bg: #2a1a21; 21 - --text: #ffffff; 22 - --subtext: #dcbaba; 23 - --link: #ffcccc; 24 - --border: rgba(255,255,255,0.1); 25 - --green: #34c759; 26 - --red: #ff6b6b; 27 - } 28 - 29 - *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 30 - 31 - body { 32 - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 33 - background: var(--bg); 34 - color: var(--text); 35 - min-height: 100vh; 36 - line-height: 1.5; 37 - } 38 - 39 - a { color: var(--link); text-decoration: none; } 40 - a:hover { text-decoration: underline; color: #ffd9d9; } 41 - 42 - /* ── HEADER ─────────────────────────────── */ 43 - .site-header { 44 - text-align: center; 45 - padding: 44px 20px 10px; 46 - max-width: 900px; 47 - margin: 0 auto; 48 - } 49 - 50 - .site-header h1 { 51 - font-size: 2.2rem; 52 - font-weight: 700; 53 - letter-spacing: -1px; 54 - margin-bottom: 10px; 55 - } 56 - 57 - .site-header p { 58 - color: var(--subtext); 59 - font-size: 0.95rem; 60 - max-width: 560px; 61 - margin: 0 auto; 62 - } 63 - 64 - /* ── STATS ──────────────────────────────── */ 65 - .stats-wrap { 66 - max-width: 900px; 67 - margin: 30px auto 0; 68 - padding: 0 20px; 69 - } 70 - 71 - .stats-grid { 72 - display: grid; 73 - grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); 74 - gap: 14px; 75 - } 76 - 77 - .stat-card { 78 - background: linear-gradient(135deg, var(--stat-bg) 0%, var(--card-bg) 100%); 79 - border: 1px solid var(--border); 80 - border-radius: 12px; 81 - padding: 20px; 82 - text-align: center; 83 - position: relative; 84 - overflow: hidden; 85 - transition: transform 0.2s, box-shadow 0.2s; 86 - } 87 - 88 - .stat-card::before { 89 - content: ''; 90 - position: absolute; 91 - top: 0; left: 0; right: 0; 92 - height: 3px; 93 - background: linear-gradient(90deg, var(--link), rgba(255,204,204,0.25)); 94 - opacity: 0; 95 - transition: opacity 0.3s; 96 - } 97 - 98 - .stat-card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0,0,0,0.4); } 99 - .stat-card:hover::before { opacity: 1; } 100 - 101 - .stat-icon { font-size: 1.8rem; margin-bottom: 8px; opacity: 0.75; } 102 - 103 - .stat-value { 104 - font-size: 2rem; 105 - font-weight: 700; 106 - letter-spacing: -1px; 107 - margin: 4px 0; 108 - } 109 - 110 - .stat-value.loading { opacity: 0.35; animation: blink 1.4s ease-in-out infinite; } 111 - @keyframes blink { 0%,100%{opacity:.35} 50%{opacity:.7} } 112 - 113 - .stat-label { 114 - font-size: 0.8rem; 115 - color: var(--subtext); 116 - text-transform: uppercase; 117 - letter-spacing: 0.5px; 118 - font-weight: 600; 119 - } 120 - 121 - /* ── TABS ───────────────────────────────── */ 122 - .tabs-wrap { 123 - max-width: 900px; 124 - margin: 36px auto 0; 125 - padding: 0 20px; 126 - } 127 - 128 - .tab-bar { 129 - display: flex; 130 - gap: 4px; 131 - border-bottom: 1px solid var(--border); 132 - overflow-x: auto; 133 - scrollbar-width: none; 134 - } 135 - 136 - .tab-bar::-webkit-scrollbar { display: none; } 137 - 138 - .tab-btn { 139 - font-family: inherit; 140 - font-size: 0.9rem; 141 - font-weight: 600; 142 - color: var(--subtext); 143 - background: none; 144 - border: none; 145 - border-bottom: 2px solid transparent; 146 - padding: 10px 18px; 147 - cursor: pointer; 148 - white-space: nowrap; 149 - transition: color 0.2s, border-color 0.2s; 150 - margin-bottom: -1px; 151 - } 152 - 153 - .tab-btn:hover { color: var(--text); } 154 - .tab-btn.active { color: var(--link); border-bottom-color: var(--link); } 155 - 156 - .tab-content { padding: 32px 0 80px; } 157 - 158 - .tab-pane { display: none; } 159 - .tab-pane.active { display: block; } 160 - 161 - /* ── CARD ───────────────────────────────── */ 162 - .card { 163 - background: var(--card-bg); 164 - border: 1px solid var(--border); 165 - border-radius: 16px; 166 - padding: 32px; 167 - box-shadow: 0 6px 12px rgba(0,0,0,0.35); 168 - } 169 - 170 - .card + .card { margin-top: 20px; } 171 - 172 - .card h2 { 173 - font-size: 1.25rem; 174 - font-weight: 700; 175 - letter-spacing: -0.4px; 176 - margin-bottom: 8px; 177 - } 178 - 179 - .card .desc { 180 - color: var(--subtext); 181 - font-size: 0.9rem; 182 - margin-bottom: 20px; 183 - } 184 - 185 - hr.divider { 186 - border: none; 187 - border-top: 1px solid var(--border); 188 - margin: 28px 0; 189 - } 190 - 191 - /* ── UPLOAD ─────────────────────────────── */ 192 - .drop-zone { 193 - display: block; 194 - border: 2px dashed var(--border); 195 - border-radius: 12px; 196 - padding: 50px 20px; 197 - text-align: center; 198 - cursor: pointer; 199 - background: rgba(0,0,0,0.2); 200 - color: var(--subtext); 201 - transition: all 0.25s; 202 - } 203 - 204 - .drop-zone:hover { 205 - border-color: var(--link); 206 - background: rgba(255,255,255,0.02); 207 - transform: scale(1.005); 208 - } 209 - 210 - .drop-zone.drag-over { 211 - border-color: var(--green); 212 - background: rgba(52,199,89,0.05); 213 - transform: scale(1.01); 214 - } 215 - 216 - .drop-zone i { font-size: 2.4rem; display: block; margin-bottom: 12px; opacity: 0.65; } 217 - .drop-zone input { display: none; } 218 - 219 - #file-name { font-size: 0.95rem; font-weight: 500; display: block; margin-top: 4px; } 220 - 221 - .file-info { 222 - display: none; 223 - margin-top: 14px; 224 - padding: 12px 16px; 225 - background: var(--post-bg); 226 - border: 1px solid var(--border); 227 - border-radius: 8px; 228 - font-size: 0.82rem; 229 - color: var(--subtext); 230 - } 231 - 232 - .file-info.show { display: block; } 233 - 234 - .progress-wrap { display: none; margin-top: 14px; } 235 - .progress-wrap.show { display: block; } 236 - 237 - .progress-track { 238 - height: 7px; 239 - background: var(--post-bg); 240 - border-radius: 999px; 241 - overflow: hidden; 242 - border: 1px solid var(--border); 243 - } 244 - 245 - .progress-fill { 246 - height: 100%; 247 - width: 0%; 248 - background: linear-gradient(90deg, var(--link), var(--green)); 249 - border-radius: 999px; 250 - transition: width 0.2s ease; 251 - } 252 - 253 - .progress-label { 254 - text-align: right; 255 - font-size: 0.75rem; 256 - color: var(--subtext); 257 - margin-top: 5px; 258 - } 259 - 260 - .upload-btn { 261 - display: block; 262 - width: 100%; 263 - margin-top: 18px; 264 - padding: 14px 24px; 265 - background: linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%); 266 - color: #000; 267 - border: none; 268 - border-radius: 999px; 269 - font-family: inherit; 270 - font-size: 1rem; 271 - font-weight: 600; 272 - cursor: pointer; 273 - box-shadow: 0 2px 8px rgba(255,255,255,0.08); 274 - transition: all 0.2s; 275 - } 276 - 277 - .upload-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 14px rgba(255,255,255,0.18); } 278 - .upload-btn:active { transform: scale(0.985); } 279 - .upload-btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; box-shadow: none; } 280 - 281 - .status-msg { 282 - text-align: center; 283 - margin-top: 14px; 284 - font-size: 0.9rem; 285 - font-weight: 500; 286 - min-height: 1.3em; 287 - color: var(--link); 288 - } 289 - 290 - .result-box { 291 - display: none; 292 - margin-top: 18px; 293 - padding: 18px; 294 - background: var(--post-bg); 295 - border: 1px solid var(--green); 296 - border-radius: 12px; 297 - animation: slideIn 0.3s ease; 298 - } 299 - 300 - .result-box.show { display: block; } 301 - 302 - @keyframes slideIn { 303 - from { opacity: 0; transform: translateY(-8px); } 304 - to { opacity: 1; transform: translateY(0); } 305 - } 306 - 307 - .result-label { 308 - font-size: 0.72rem; 309 - font-weight: 700; 310 - text-transform: uppercase; 311 - letter-spacing: 0.6px; 312 - color: var(--subtext); 313 - margin-bottom: 8px; 314 - } 315 - 316 - .result-url { 317 - font-family: 'Monaco', 'Courier New', monospace; 318 - font-size: 0.85rem; 319 - padding: 10px 12px; 320 - background: rgba(0,0,0,0.3); 321 - border: 1px solid var(--border); 322 - border-radius: 6px; 323 - word-break: break-all; 324 - color: var(--text); 325 - margin-bottom: 12px; 326 - } 327 - 328 - .copy-btn { 329 - display: inline-flex; 330 - align-items: center; 331 - gap: 7px; 332 - padding: 9px 16px; 333 - background: rgba(255,255,255,0.05); 334 - border: 1px solid var(--border); 335 - border-radius: 8px; 336 - color: var(--link); 337 - font-family: inherit; 338 - font-size: 0.85rem; 339 - font-weight: 600; 340 - cursor: pointer; 341 - transition: all 0.15s; 342 - margin-right: 8px; 343 - } 344 - 345 - .copy-btn:hover { background: rgba(255,255,255,0.09); } 346 - .copy-btn.copied { background: var(--green); color: #000; border-color: var(--green); } 347 - 348 - .open-btn { 349 - display: inline-flex; 350 - align-items: center; 351 - gap: 7px; 352 - padding: 9px 16px; 353 - background: rgba(255,255,255,0.05); 354 - border: 1px solid var(--border); 355 - border-radius: 8px; 356 - color: var(--link); 357 - font-size: 0.85rem; 358 - font-weight: 600; 359 - transition: all 0.15s; 360 - } 361 - 362 - .open-btn:hover { background: rgba(255,255,255,0.09); text-decoration: none; } 363 - 364 - /* ── ABOUT ──────────────────────────────── */ 365 - .about-text { 366 - color: var(--subtext); 367 - font-size: 0.92rem; 368 - line-height: 1.7; 369 - } 370 - 371 - .about-text p + p { margin-top: 12px; } 372 - 373 - .social-row { 374 - display: flex; 375 - flex-wrap: wrap; 376 - gap: 10px; 377 - justify-content: center; 378 - } 379 - 380 - .social-btn { 381 - display: inline-flex; 382 - align-items: center; 383 - gap: 8px; 384 - padding: 9px 16px; 385 - background: rgba(255,255,255,0.04); 386 - border: 1px solid var(--border); 387 - border-radius: 999px; 388 - color: var(--text); 389 - font-size: 0.88rem; 390 - font-weight: 600; 391 - transition: all 0.2s; 392 - } 393 - 394 - .social-btn:hover { 395 - background: rgba(255,255,255,0.08); 396 - transform: translateY(-2px); 397 - box-shadow: 0 4px 12px rgba(0,0,0,0.3); 398 - text-decoration: none; 399 - } 400 - 401 - /* ── HOW IT WORKS ───────────────────────── */ 402 - .steps { 403 - display: flex; 404 - flex-direction: column; 405 - gap: 18px; 406 - margin-bottom: 28px; 407 - } 408 - 409 - .step { 410 - display: flex; 411 - align-items: flex-start; 412 - gap: 16px; 413 - } 414 - 415 - .step-num { 416 - flex-shrink: 0; 417 - width: 34px; height: 34px; 418 - border-radius: 50%; 419 - background: linear-gradient(135deg, var(--stat-bg), var(--card-bg)); 420 - border: 1px solid var(--border); 421 - display: flex; 422 - align-items: center; 423 - justify-content: center; 424 - font-size: 0.78rem; 425 - font-weight: 700; 426 - color: var(--link); 427 - } 428 - 429 - .step-body h3 { font-size: 0.95rem; font-weight: 700; margin-bottom: 3px; } 430 - .step-body p { color: var(--subtext); font-size: 0.85rem; line-height: 1.55; } 431 - 432 - .how-grid { 433 - display: grid; 434 - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 435 - gap: 14px; 436 - } 437 - 438 - .how-card { 439 - background: var(--post-bg); 440 - border: 1px solid var(--border); 441 - border-radius: 10px; 442 - padding: 18px; 443 - } 444 - 445 - .how-card i { font-size: 1.5rem; color: var(--link); margin-bottom: 10px; display: block; } 446 - .how-card h3 { font-size: 0.9rem; font-weight: 700; margin-bottom: 5px; } 447 - .how-card p { font-size: 0.8rem; color: var(--subtext); line-height: 1.55; } 448 - 449 - /* ── LIMITS ─────────────────────────────── */ 450 - .limits-grid { 451 - display: grid; 452 - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 453 - gap: 14px; 454 - margin-bottom: 22px; 455 - } 456 - 457 - .limit-card { 458 - background: var(--post-bg); 459 - border: 1px solid var(--border); 460 - border-radius: 12px; 461 - padding: 22px 18px; 462 - text-align: center; 463 - position: relative; 464 - overflow: hidden; 465 - } 466 - 467 - .limit-card::before { 468 - content: ''; 469 - position: absolute; 470 - top: 0; left: 0; right: 0; 471 - height: 3px; 472 - background: linear-gradient(90deg, var(--link), rgba(255,204,204,0.2)); 473 - } 474 - 475 - .limit-card i { font-size: 1.8rem; color: var(--link); margin-bottom: 12px; display: block; opacity: 0.8; } 476 - .limit-value { font-size: 1.7rem; font-weight: 700; letter-spacing: -0.5px; margin-bottom: 4px; } 477 - .limit-label { font-size: 0.82rem; color: var(--subtext); font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; } 478 - .limit-note { margin-top: 5px; font-size: 0.72rem; color: rgba(220,186,186,0.5); } 479 - 480 - /* ── USAGE BARS ─────────────────────────── */ 481 - .usage-section { margin-top: 28px; } 482 - 483 - .usage-section h2 { font-size: 1rem; font-weight: 700; margin-bottom: 16px; } 484 - 485 - .usage-item { margin-bottom: 16px; } 486 - 487 - .usage-row { 488 - display: flex; 489 - justify-content: space-between; 490 - font-size: 0.82rem; 491 - color: var(--subtext); 492 - margin-bottom: 6px; 493 - } 494 - 495 - .usage-row span:first-child { font-weight: 600; color: var(--text); } 496 - 497 - .usage-track { 498 - height: 8px; 499 - background: var(--post-bg); 500 - border-radius: 999px; 501 - overflow: hidden; 502 - border: 1px solid var(--border); 503 - } 504 - 505 - .usage-fill { 506 - height: 100%; 507 - border-radius: 999px; 508 - background: linear-gradient(90deg, var(--link), var(--green)); 509 - transition: width 0.6s cubic-bezier(.4,0,.2,1); 510 - } 511 - 512 - .usage-fill.warn { background: linear-gradient(90deg, #ffaa00, #ff6b00); } 513 - .usage-fill.danger { background: linear-gradient(90deg, var(--red), #cc0000); } 514 - 515 - .usage-loading { font-size: 0.8rem; color: var(--subtext); opacity: 0.5; } 516 - 517 - .limits-note { 518 - padding: 14px 18px; 519 - background: rgba(255,107,107,0.06); 520 - border: 1px solid rgba(255,107,107,0.18); 521 - border-radius: 10px; 522 - font-size: 0.83rem; 523 - color: var(--subtext); 524 - line-height: 1.6; 525 - } 526 - 527 - .limits-note i { color: var(--red); margin-right: 4px; } 528 - 529 - /* ── FOOTER ─────────────────────────────── */ 530 - .site-footer { 531 - text-align: center; 532 - padding: 0 20px 32px; 533 - font-size: 0.82rem; 534 - color: var(--subtext); 535 - } 536 - 537 - /* ── RESPONSIVE ─────────────────────────── */ 538 - @media (max-width: 600px) { 539 - .stats-grid { grid-template-columns: repeat(2, 1fr); } 540 - .site-header h1 { font-size: 1.8rem; } 541 - .stat-value { font-size: 1.5rem; } 542 - .how-grid, .limits-grid { grid-template-columns: 1fr; } 543 - .card { padding: 22px 18px; } 544 - } 545 - </style> 546 - </head> 547 - <body> 548 - 549 - <header class="site-header"> 550 - <h1><i class="fa-solid fa-database" style="color:#fff"></i> MBD CDN</h1> 551 - <p>A network of servers located around the world powered by the Cloudflare global network — delivering media at extremely fast speeds with 100% uptime.</p> 552 - </header> 553 - 554 - <!-- STATS --> 555 - <div class="stats-wrap"> 556 - <div class="stats-grid"> 557 - <div class="stat-card"> 558 - <div class="stat-icon"><i class="fa-regular fa-image"></i></div> 559 - <div class="stat-value loading" id="stat-images">--</div> 560 - <div class="stat-label">Images</div> 561 - </div> 562 - <div class="stat-card"> 563 - <div class="stat-icon"><i class="fa-solid fa-video"></i></div> 564 - <div class="stat-value loading" id="stat-videos">--</div> 565 - <div class="stat-label">Videos</div> 566 - </div> 567 - <div class="stat-card"> 568 - <div class="stat-icon"><i class="fa-solid fa-photo-film"></i></div> 569 - <div class="stat-value loading" id="stat-gifs">--</div> 570 - <div class="stat-label">GIFs</div> 571 - </div> 572 - <div class="stat-card"> 573 - <div class="stat-icon"><i class="fa-solid fa-database"></i></div> 574 - <div class="stat-value loading" id="stat-storage">--</div> 575 - <div class="stat-label">Storage Used</div> 576 - </div> 577 - </div> 578 - </div> 579 - 580 - <!-- TABS --> 581 - <div class="tabs-wrap"> 582 - <div class="tab-bar"> 583 - <button class="tab-btn active" onclick="switchTab('upload', this)"> 584 - <i class="fa-solid fa-cloud-arrow-up"></i> Upload 585 - </button> 586 - <button class="tab-btn" onclick="switchTab('about', this)"> 587 - <i class="fa-solid fa-circle-info"></i> About 588 - </button> 589 - <button class="tab-btn" onclick="switchTab('how', this)"> 590 - <i class="fa-solid fa-gears"></i> How it Works 591 - </button> 592 - <button class="tab-btn" onclick="switchTab('limits', this)"> 593 - <i class="fa-solid fa-gauge-high"></i> Limits 594 - </button> 595 - </div> 596 - 597 - <div class="tab-content"> 598 - 599 - <!-- UPLOAD --> 600 - <div class="tab-pane active" id="tab-upload"> 601 - <div class="card"> 602 - <h2>Upload a File</h2> 603 - <p class="desc">Images, GIFs, and videos accepted. Files are served globally via Cloudflare immediately after upload.</p> 604 - 605 - <label class="drop-zone" id="drop-zone" for="file-input"> 606 - <i class="fa-solid fa-cloud-arrow-up" id="drop-icon"></i> 607 - <span id="file-name">Click to select or drag a file here</span> 608 - <input type="file" id="file-input" accept="image/*,video/*"> 609 - </label> 610 - 611 - <div class="file-info" id="file-info"> 612 - <div><b>File:</b> <span id="detail-name"></span></div> 613 - <div style="margin-top:4px"><b>Size:</b> <span id="detail-size"></span> &nbsp;·&nbsp; <b>Type:</b> <span id="detail-type"></span></div> 614 - </div> 615 - 616 - <div class="progress-wrap" id="progress-wrap"> 617 - <div class="progress-track"> 618 - <div class="progress-fill" id="progress-fill"></div> 619 - </div> 620 - <div class="progress-label" id="progress-label">0%</div> 621 - </div> 622 - 623 - <button class="upload-btn" id="upload-btn"> 624 - <i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN 625 - </button> 626 - 627 - <div class="status-msg" id="status"></div> 628 - 629 - <div class="result-box" id="result-box"> 630 - <div class="result-label">✅ Public URL</div> 631 - <div class="result-url" id="result-url"></div> 632 - <button class="copy-btn" id="copy-btn" onclick="copyUrl()"> 633 - <i class="fa-solid fa-copy"></i> Copy URL 634 - </button> 635 - <a class="open-btn" id="open-link" href="#" target="_blank" rel="noopener"> 636 - <i class="fa-solid fa-arrow-up-right-from-square"></i> Open 637 - </a> 638 - </div> 639 - </div> 640 - </div> 641 - 642 - <!-- ABOUT --> 643 - <div class="tab-pane" id="tab-about"> 644 - <div class="card"> 645 - <h2>About MBD CDN</h2> 646 - <div class="about-text"> 647 - <p>The MBD CDN is a content delivery network built by <a href="https://madebydanny.uk" target="_blank">madebydanny.uk</a> to host and serve media files — images, GIFs, and videos — at extremely fast speeds with global availability.</p> 648 - <p>Files uploaded to the CDN are stored in Cloudflare R2 object storage and served from Cloudflare's global edge network, which spans over 310 cities worldwide. This means your media is delivered from a server geographically close to whoever is viewing it — minimising latency and maximising load speed.</p> 649 - <p>The platform is designed to be simple and permanent. Files are stored indefinitely once uploaded and are immediately available via a public URL.</p> 650 - </div> 651 - 652 - <hr class="divider"> 653 - 654 - <h2 style="margin-bottom:16px;">Social Links</h2> 655 - <div id="social-links" class="social-row"></div> 656 - </div> 657 - </div> 658 - 659 - <!-- HOW IT WORKS --> 660 - <div class="tab-pane" id="tab-how"> 661 - <div class="card"> 662 - <h2>How it Works</h2> 663 - <p class="desc">From the moment you click Upload to the moment someone loads your file — here's what happens.</p> 664 - 665 - <div class="steps"> 666 - <div class="step"> 667 - <div class="step-num">1</div> 668 - <div class="step-body"> 669 - <h3>You select a file</h3> 670 - <p>Your file is read locally in the browser and sent directly to the CDN API over HTTPS. It goes straight to the Cloudflare edge — no intermediate servers involved.</p> 671 - </div> 672 - </div> 673 - <div class="step"> 674 - <div class="step-num">2</div> 675 - <div class="step-body"> 676 - <h3>The Worker receives it</h3> 677 - <p>A Cloudflare Worker handles the upload at the edge. It assigns a UUID filename, detects the file type, and streams the body directly into R2 object storage — with no cold starts and near-instant response times.</p> 678 - </div> 679 - </div> 680 - <div class="step"> 681 - <div class="step-num">3</div> 682 - <div class="step-body"> 683 - <h3>R2 stores it permanently</h3> 684 - <p>The file is written to Cloudflare R2 — S3-compatible storage with zero egress fees and 11 nines of durability. Upload metadata is logged to D1 (Cloudflare's edge SQL database) to track stats.</p> 685 - </div> 686 - </div> 687 - <div class="step"> 688 - <div class="step-num">4</div> 689 - <div class="step-body"> 690 - <h3>You get a public URL</h3> 691 - <p>A permanent <code style="font-size:0.8rem;color:var(--link)">public-cdn.madebydanny.uk</code> link is returned instantly. Anyone with it can access the file — served from whichever Cloudflare PoP is closest to them.</p> 692 - </div> 693 - </div> 694 - </div> 695 - 696 - <div class="how-grid"> 697 - <div class="how-card"> 698 - <i class="fa-brands fa-cloudflare"></i> 699 - <h3>310+ Edge Locations</h3> 700 - <p>Files are cached and served globally — sub-50ms for most users regardless of where they are.</p> 701 - </div> 702 - <div class="how-card"> 703 - <i class="fa-solid fa-database"></i> 704 - <h3>R2 Object Storage</h3> 705 - <p>Zero egress fees, no expiry. Files are stored in Cloudflare R2 with enterprise-grade durability.</p> 706 - </div> 707 - <div class="how-card"> 708 - <i class="fa-solid fa-bolt"></i> 709 - <h3>Zero Cold Starts</h3> 710 - <p>Workers run at the edge with no spin-up delay — every upload and file request is handled immediately.</p> 711 - </div> 712 - </div> 713 - </div> 714 - </div> 715 - 716 - <!-- LIMITS --> 717 - <div class="tab-pane" id="tab-limits"> 718 - <div class="card"> 719 - <h2>Usage Limits</h2> 720 - <p class="desc">Fair-use limits are in place to keep the CDN reliable and available for everyone.</p> 721 - 722 - <div class="limits-grid"> 723 - <div class="limit-card"> 724 - <i class="fa-solid fa-file-arrow-up"></i> 725 - <div class="limit-value" id="limit-max-file">100 MB</div> 726 - <div class="limit-label">Max File Size</div> 727 - <div class="limit-note">Per individual upload</div> 728 - </div> 729 - <div class="limit-card"> 730 - <i class="fa-solid fa-hard-drive"></i> 731 - <div class="limit-value" id="limit-max-bytes">—</div> 732 - <div class="limit-label">Daily Storage</div> 733 - <div class="limit-note">Total uploads per day</div> 734 - </div> 735 - <div class="limit-card"> 736 - <i class="fa-solid fa-arrow-up-from-bracket"></i> 737 - <div class="limit-value" id="limit-max-files">—</div> 738 - <div class="limit-label">Uploads Per Day</div> 739 - <div class="limit-note">Resets at midnight UTC</div> 740 - </div> 741 - </div> 742 - 743 - <div class="limits-note"> 744 - <i class="fa-solid fa-circle-exclamation"></i> 745 - All limits reset daily at <strong>midnight UTC</strong>. They are enforced per IP to protect performance for all users. If you need higher limits, consider self-hosting the stack or get in touch via the social links in the About section. 746 - </div> 747 - 748 - <div class="usage-section"> 749 - <h2>Your Usage Today</h2> 750 - <div id="usage-loading" class="usage-loading">Loading your usage…</div> 751 - <div id="usage-bars" style="display:none"> 752 - <div class="usage-item"> 753 - <div class="usage-row"> 754 - <span>Files Uploaded</span> 755 - <span id="usage-files-label">0 / 30</span> 756 - </div> 757 - <div class="usage-track"> 758 - <div class="usage-fill" id="usage-files-fill" style="width:0%"></div> 759 - </div> 760 - </div> 761 - <div class="usage-item" style="margin-top:14px"> 762 - <div class="usage-row"> 763 - <span>Storage Used</span> 764 - <span id="usage-bytes-label">0 B / 1 GB</span> 765 - </div> 766 - <div class="usage-track"> 767 - <div class="usage-fill" id="usage-bytes-fill" style="width:0%"></div> 768 - </div> 769 - </div> 770 - </div> 771 - </div> 772 - 773 - <hr class="divider"> 774 - 775 - <h2 style="font-size:1rem; margin-bottom:10px;">Accepted File Types</h2> 776 - <p style="color:var(--subtext); font-size:0.85rem; line-height:1.7;"> 777 - <strong style="color:var(--text)">Images:</strong> JPEG, PNG, WebP, AVIF, SVG &nbsp;&nbsp; 778 - <strong style="color:var(--text)">Animated:</strong> GIF &nbsp;&nbsp; 779 - <strong style="color:var(--text)">Video:</strong> MP4, WebM, MOV 780 - </p> 781 - </div> 782 - </div> 783 - 784 - </div> 785 - </div> 786 - 787 - <footer class="site-footer"> 788 - &copy; <script>document.write(new Date().getFullYear())</script> <a href="https://madebydanny.uk" target="_blank">madebydanny.uk</a> 789 - </footer> 790 - 791 - <script> 792 - const API = 'https://cdn.madebydanny.uk'; 793 - 794 - function formatBytes(b) { 795 - if (!b) return '0 B'; 796 - const k = 1024, s = ['B','KB','MB','GB','TB']; 797 - const i = Math.floor(Math.log(b) / Math.log(k)); 798 - return (b / Math.pow(k, i)).toFixed(1).replace(/\.0$/,'') + ' ' + s[i]; 799 - } 800 - 801 - function fmt(n) { return Number(n).toLocaleString(); } 802 - 803 - // ── TABS ───────────────────────────────────────────── 804 - function switchTab(name, btn) { 805 - document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); 806 - document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); 807 - document.getElementById('tab-' + name).classList.add('active'); 808 - if (btn) btn.classList.add('active'); 809 - if (name === 'limits') loadLimits(); 810 - } 811 - 812 - // ── STATS ───────────────────────────────────────────── 813 - async function loadStats() { 814 - try { 815 - const r = await fetch(`${API}/stats`); 816 - const d = await r.json(); 817 - if (d.success) { 818 - document.getElementById('stat-images').textContent = fmt(d.stats.images); 819 - document.getElementById('stat-videos').textContent = fmt(d.stats.videos); 820 - document.getElementById('stat-gifs').textContent = fmt(d.stats.gifs); 821 - document.getElementById('stat-storage').textContent = formatBytes(d.stats.totalSize); 822 - document.querySelectorAll('.stat-value').forEach(v => v.classList.remove('loading')); 823 - } 824 - } catch { 825 - document.querySelectorAll('.stat-value').forEach(v => { 826 - v.textContent = '—'; 827 - v.classList.remove('loading'); 828 - }); 829 - } 830 - } 831 - 832 - // ── LIMITS (live from API) ──────────────────────────── 833 - async function loadLimits() { 834 - try { 835 - const r = await fetch(`${API}/limits`); 836 - const d = await r.json(); 837 - if (!d.success) throw new Error(); 838 - 839 - const { file_count, total_size, max_files, max_bytes, max_file } = d.limits; 840 - 841 - document.getElementById('limit-max-file').textContent = formatBytes(max_file); 842 - document.getElementById('limit-max-bytes').textContent = formatBytes(max_bytes); 843 - document.getElementById('limit-max-files').textContent = max_files; 844 - 845 - const filePct = Math.min((file_count / max_files) * 100, 100); 846 - const bytesPct = Math.min((total_size / max_bytes) * 100, 100); 847 - 848 - const filesFill = document.getElementById('usage-files-fill'); 849 - const bytesFill = document.getElementById('usage-bytes-fill'); 850 - 851 - filesFill.style.width = filePct + '%'; 852 - bytesFill.style.width = bytesPct + '%'; 853 - 854 - function fillClass(pct) { 855 - if (pct >= 90) return 'danger'; 856 - if (pct >= 70) return 'warn'; 857 - return ''; 858 - } 859 - 860 - filesFill.className = 'usage-fill ' + fillClass(filePct); 861 - bytesFill.className = 'usage-fill ' + fillClass(bytesPct); 862 - 863 - document.getElementById('usage-files-label').textContent = 864 - `${fmt(file_count)} / ${fmt(max_files)}`; 865 - document.getElementById('usage-bytes-label').textContent = 866 - `${formatBytes(total_size)} / ${formatBytes(max_bytes)}`; 867 - 868 - document.getElementById('usage-loading').style.display = 'none'; 869 - document.getElementById('usage-bars').style.display = 'block'; 870 - 871 - } catch { 872 - document.getElementById('usage-loading').textContent = 'Could not load usage data.'; 873 - } 874 - } 875 - 876 - // ── FILE SELECT ─────────────────────────────────────── 877 - const fileInput = document.getElementById('file-input'); 878 - const dropZone = document.getElementById('drop-zone'); 879 - const fileInfo = document.getElementById('file-info'); 880 - 881 - function showFile(file) { 882 - const type = file.type || ''; 883 - const icon = document.getElementById('drop-icon'); 884 - 885 - // Update drop zone icon to match file type 886 - if (type.startsWith('video/')) { 887 - icon.className = 'fa-solid fa-film'; 888 - } else if (type === 'image/gif') { 889 - icon.className = 'fa-solid fa-photo-film'; 890 - } else if (type.startsWith('image/')) { 891 - icon.className = 'fa-regular fa-image'; 892 - } else { 893 - icon.className = 'fa-solid fa-cloud-arrow-up'; 894 - } 895 - 896 - document.getElementById('file-name').textContent = file.name; 897 - document.getElementById('detail-name').textContent = file.name; 898 - document.getElementById('detail-size').textContent = formatBytes(file.size); 899 - document.getElementById('detail-type').textContent = type || 'Unknown'; 900 - fileInfo.classList.add('show'); 901 - } 902 - 903 - fileInput.addEventListener('change', () => { 904 - if (fileInput.files[0]) showFile(fileInput.files[0]); 905 - }); 906 - 907 - dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); }); 908 - dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); }); 909 - dropZone.addEventListener('drop', e => { 910 - e.preventDefault(); 911 - dropZone.classList.remove('drag-over'); 912 - const f = e.dataTransfer.files[0]; 913 - if (f) { fileInput.files = e.dataTransfer.files; showFile(f); } 914 - }); 915 - 916 - // ── UPLOAD (XHR for real progress) ─────────────────── 917 - const uploadBtn = document.getElementById('upload-btn'); 918 - const progressWrap = document.getElementById('progress-wrap'); 919 - const progressFill = document.getElementById('progress-fill'); 920 - const progressLabel= document.getElementById('progress-label'); 921 - const statusEl = document.getElementById('status'); 922 - const resultBox = document.getElementById('result-box'); 923 - const resultUrl = document.getElementById('result-url'); 924 - 925 - function setStatus(msg, color) { 926 - statusEl.textContent = msg; 927 - statusEl.style.color = color || 'var(--link)'; 928 - } 929 - 930 - function setProgress(pct) { 931 - progressFill.style.width = pct + '%'; 932 - progressLabel.textContent = Math.round(pct) + '%'; 933 - } 934 - 935 - function resetUploadUI(delay = 0) { 936 - setTimeout(() => { 937 - fileInput.value = ''; 938 - document.getElementById('drop-icon').className = 'fa-solid fa-cloud-arrow-up'; 939 - document.getElementById('file-name').textContent = 'Click to select or drag a file here'; 940 - fileInfo.classList.remove('show'); 941 - progressWrap.classList.remove('show'); 942 - setProgress(0); 943 - }, delay); 944 - } 945 - 946 - uploadBtn.addEventListener('click', () => { 947 - if (!fileInput.files[0]) { 948 - setStatus('⚠️ Please select a file first.', 'var(--red)'); 949 - return; 950 - } 951 - 952 - const file = fileInput.files[0]; 953 - uploadBtn.disabled = true; 954 - uploadBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Uploading…'; 955 - setStatus(''); 956 - resultBox.classList.remove('show'); 957 - progressWrap.classList.add('show'); 958 - setProgress(0); 959 - 960 - const xhr = new XMLHttpRequest(); 961 - 962 - xhr.upload.addEventListener('progress', e => { 963 - if (e.lengthComputable) setProgress((e.loaded / e.total) * 100); 964 - }); 965 - 966 - xhr.addEventListener('load', () => { 967 - setProgress(100); 968 - try { 969 - const data = JSON.parse(xhr.responseText); 970 - if (data.success) { 971 - resultUrl.textContent = data.url; 972 - document.getElementById('open-link').href = data.url; 973 - resultBox.classList.add('show'); 974 - setStatus(''); 975 - setTimeout(() => loadStats(), 600); 976 - resetUploadUI(3000); 977 - } else { 978 - throw new Error(data.error || 'Upload failed'); 979 - } 980 - } catch (err) { 981 - progressWrap.classList.remove('show'); 982 - setStatus('❌ ' + err.message, 'var(--red)'); 983 - } 984 - 985 - uploadBtn.disabled = false; 986 - uploadBtn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN'; 987 - }); 988 - 989 - xhr.addEventListener('error', () => { 990 - progressWrap.classList.remove('show'); 991 - setStatus('❌ Network error. Please try again.', 'var(--red)'); 992 - uploadBtn.disabled = false; 993 - uploadBtn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN'; 994 - }); 995 - 996 - xhr.addEventListener('abort', () => { 997 - progressWrap.classList.remove('show'); 998 - setStatus('Upload cancelled.', 'var(--subtext)'); 999 - uploadBtn.disabled = false; 1000 - uploadBtn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN'; 1001 - }); 1002 - 1003 - xhr.open('POST', API); 1004 - xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream'); 1005 - xhr.send(file); 1006 - }); 1007 - 1008 - // ── COPY ───────────────────────────────────────────── 1009 - function copyUrl() { 1010 - navigator.clipboard.writeText(resultUrl.textContent); 1011 - const btn = document.getElementById('copy-btn'); 1012 - btn.classList.add('copied'); 1013 - btn.innerHTML = '<i class="fa-solid fa-check"></i> Copied!'; 1014 - setTimeout(() => { 1015 - btn.classList.remove('copied'); 1016 - btn.innerHTML = '<i class="fa-solid fa-copy"></i> Copy URL'; 1017 - }, 2000); 1018 - } 1019 - 1020 - // ── SOCIAL LINKS FALLBACK ───────────────────────────── 1021 - window.addEventListener('load', () => { 1022 - const el = document.getElementById('social-links'); 1023 - if (!el.children.length) { 1024 - el.innerHTML = ` 1025 - <a class="social-btn" href="https://aturi.to/did:plc:l37td5yhxl2irrzrgvei4qay" target="_blank" rel="noopener"> 1026 - <i class="fa-brands fa-bluesky"></i> Bluesky 1027 - </a>`; 1028 - } 1029 - }); 1030 - 1031 - // ── INIT ───────────────────────────────────────────── 1032 - loadStats(); 1033 - </script> 1034 - 1035 - <!-- social-links.js is optional; silence 404s by loading it async --> 1036 - <script> 1037 - (function() { 1038 - const s = document.createElement('script'); 1039 - s.src = '/js/social-links.js'; 1040 - s.onerror = () => {}; 1041 - document.body.appendChild(s); 1042 - })(); 1043 - </script> 1044 - 1045 - </body> 1046 - </html>