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.

at main 1157 lines 39 kB view raw
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 <meta name="description" content="A simple, fast, and free CDN powered by Cloudflare. Upload images, GIFs, videos and documents."> 8 <meta property="og:title" content="MBD CDN — madebydanny.uk"> 9 <meta property="og:description" content="A simple easy to use CDN, free for life"> 10 <meta property="og:image" content="https://cdn.madebydanny.uk/user-content/2026-04-29/93bde54d-bc21-43b9-8e4e-d9d324e9607d.png"> 11 <meta property="og:type" content="website"> 12 <link rel="icon" href="https://public-cdn.madebydanny.uk/user-content/2026-01-30/33913bec-bc2f-4e6c-a474-2ef8f8c00197"> 13 14 <link rel="preconnect" href="https://fonts.googleapis.com"> 15 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 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 <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> 19 20 <style> 21 /* ── Design tokens (match main site) ── */ 22 :root { 23 --bg: #0e0d0c; 24 --bg-raised: #171512; 25 --bg-card: #1a1815; 26 --border: #2d2926; 27 --border-hover:#4a4238; 28 29 --text: #e8e0d8; 30 --text-muted: #8a7f74; 31 --text-dim: #584f47; 32 33 --accent: #c9a96e; 34 --accent-dim: rgba(201,169,110,0.12); 35 --accent-glow: rgba(201,169,110,0.08); 36 37 --green: #4caf7d; 38 --red: #e06c6c; 39 40 --font-serif: 'Lora', Georgia, serif; 41 --font-sans: 'DM Sans', system-ui, sans-serif; 42 --font-mono: 'Monaco', 'Courier New', monospace; 43 44 --radius: 10px; 45 --max-w: 720px; 46 --transition: 0.2s ease; 47 } 48 49 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 50 html { scroll-behavior: smooth; } 51 52 body { 53 font-family: var(--font-sans); 54 font-size: 16px; 55 line-height: 1.7; 56 color: var(--text); 57 background: var(--bg); 58 -webkit-font-smoothing: antialiased; 59 } 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.85em; 67 background: var(--bg-raised); 68 padding: 0.15em 0.4em; 69 border-radius: 4px; 70 color: var(--accent); 71 } 72 73 /* ── Header ── */ 74 .site-header { 75 position: sticky; 76 top: 0; 77 z-index: 100; 78 background: rgba(14,13,12,0.92); 79 backdrop-filter: blur(12px); 80 border-bottom: 1px solid var(--border); 81 } 82 83 .nav-container { 84 max-width: var(--max-w); 85 margin: 0 auto; 86 padding: 0.875rem 1.5rem; 87 display: flex; 88 justify-content: space-between; 89 align-items: center; 90 } 91 92 .nav-logo { 93 font-family: var(--font-serif); 94 font-style: italic; 95 font-size: 1.1rem; 96 color: var(--text-muted); 97 } 98 .nav-logo:hover { color: var(--text); opacity: 1; } 99 100 .nav-title { 101 font-family: var(--font-serif); 102 font-size: 1.125rem; 103 color: var(--text); 104 } 105 106 /* ── Main ── */ 107 .main-content { 108 max-width: var(--max-w); 109 margin: 0 auto; 110 padding: 0 1.5rem; 111 } 112 113 /* ── Page hero ── */ 114 .page-hero { 115 padding: 3rem 0 2rem; 116 border-bottom: 1px solid var(--border); 117 } 118 119 .page-hero-eyebrow { 120 font-size: 0.75rem; 121 text-transform: uppercase; 122 letter-spacing: 0.1em; 123 color: var(--text-dim); 124 margin-bottom: 0.5rem; 125 } 126 127 .page-hero h1 { 128 font-family: var(--font-serif); 129 font-size: clamp(2rem, 5vw, 2.75rem); 130 font-weight: 400; 131 color: var(--text); 132 letter-spacing: -0.02em; 133 margin-bottom: 0.625rem; 134 } 135 136 .page-hero p { 137 font-size: 0.9375rem; 138 color: var(--text-muted); 139 max-width: 520px; 140 } 141 142 /* ── Stats ── */ 143 .stats-section { 144 padding: 1.75rem 0; 145 border-bottom: 1px solid var(--border); 146 } 147 148 .section-label { 149 font-family: var(--font-sans); 150 font-size: 0.75rem; 151 font-weight: 500; 152 text-transform: uppercase; 153 letter-spacing: 0.1em; 154 color: var(--text-dim); 155 margin-bottom: 1rem; 156 } 157 158 .stats-grid { 159 display: grid; 160 grid-template-columns: repeat(4, 1fr); 161 gap: 0.75rem; 162 } 163 164 .stat-card { 165 background: var(--bg-card); 166 border: 1px solid var(--border); 167 border-radius: var(--radius); 168 padding: 1rem; 169 text-align: center; 170 transition: border-color var(--transition); 171 } 172 .stat-card:hover { border-color: var(--border-hover); } 173 174 .stat-icon { 175 font-size: 1.1rem; 176 color: var(--accent); 177 opacity: 0.7; 178 margin-bottom: 0.375rem; 179 } 180 181 .stat-value { 182 font-family: var(--font-serif); 183 font-size: 1.5rem; 184 color: var(--text); 185 letter-spacing: -0.02em; 186 line-height: 1.2; 187 } 188 189 .stat-value.loading { 190 color: var(--text-dim); 191 animation: pulse 1.6s ease-in-out infinite; 192 } 193 194 @keyframes pulse { 0%,100%{opacity:.4} 50%{opacity:.8} } 195 196 .stat-label { 197 font-size: 0.75rem; 198 color: var(--text-dim); 199 text-transform: uppercase; 200 letter-spacing: 0.05em; 201 margin-top: 0.2rem; 202 } 203 204 .storage-row { 205 margin-top: 0.75rem; 206 } 207 208 .storage-card { 209 background: var(--bg-card); 210 border: 1px solid var(--border); 211 border-radius: var(--radius); 212 padding: 1rem 1.25rem; 213 display: flex; 214 align-items: center; 215 gap: 1rem; 216 transition: border-color var(--transition); 217 } 218 .storage-card:hover { border-color: var(--border-hover); } 219 220 .storage-card .stat-icon { margin-bottom: 0; font-size: 1.25rem; flex-shrink: 0; } 221 222 .storage-card .stat-value { font-size: 1.25rem; } 223 224 /* ── Tabs ── */ 225 .tabs-section { 226 padding-top: 1.75rem; 227 } 228 229 .tab-bar { 230 display: flex; 231 gap: 0; 232 border-bottom: 1px solid var(--border); 233 overflow-x: auto; 234 scrollbar-width: none; 235 margin-bottom: 2rem; 236 } 237 .tab-bar::-webkit-scrollbar { display: none; } 238 239 .tab-btn { 240 font-family: var(--font-sans); 241 font-size: 0.875rem; 242 font-weight: 500; 243 color: var(--text-muted); 244 background: none; 245 border: none; 246 border-bottom: 2px solid transparent; 247 padding: 0.625rem 1.125rem; 248 cursor: pointer; 249 white-space: nowrap; 250 margin-bottom: -1px; 251 transition: color var(--transition), border-color var(--transition); 252 } 253 .tab-btn:hover { color: var(--text); } 254 .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); } 255 256 .tab-pane { display: none; padding-bottom: 4rem; } 257 .tab-pane.active { display: block; } 258 259 /* ── Upload tab ── */ 260 .upload-area { 261 margin-bottom: 1.5rem; 262 } 263 264 .drop-zone { 265 display: block; 266 border: 1px dashed var(--border-hover); 267 border-radius: var(--radius); 268 padding: 3rem 1.5rem; 269 text-align: center; 270 cursor: pointer; 271 color: var(--text-muted); 272 transition: border-color var(--transition), background var(--transition); 273 } 274 .drop-zone:hover { 275 border-color: var(--accent); 276 background: var(--accent-glow); 277 } 278 .drop-zone.drag-over { 279 border-color: var(--green); 280 background: rgba(76,175,125,0.05); 281 } 282 .drop-zone i { 283 font-size: 2rem; 284 color: var(--accent); 285 opacity: 0.6; 286 display: block; 287 margin-bottom: 0.75rem; 288 } 289 .drop-zone input { display: none; } 290 291 #file-name { 292 font-size: 0.9rem; 293 display: block; 294 margin-top: 0.25rem; 295 } 296 297 .file-info { 298 display: none; 299 margin-top: 0.875rem; 300 padding: 0.75rem 1rem; 301 background: var(--bg-card); 302 border: 1px solid var(--border); 303 border-radius: var(--radius); 304 font-size: 0.8125rem; 305 color: var(--text-muted); 306 } 307 .file-info.show { display: block; } 308 309 .progress-wrap { display: none; margin-top: 0.875rem; } 310 .progress-wrap.show { display: block; } 311 312 .progress-track { 313 height: 5px; 314 background: var(--bg-raised); 315 border-radius: 999px; 316 overflow: hidden; 317 } 318 .progress-fill { 319 height: 100%; 320 width: 0%; 321 background: linear-gradient(90deg, var(--accent), var(--green)); 322 border-radius: 999px; 323 transition: width 0.2s ease; 324 } 325 .progress-label { 326 text-align: right; 327 font-size: 0.75rem; 328 color: var(--text-dim); 329 margin-top: 0.3rem; 330 } 331 332 .upload-btn { 333 display: block; 334 width: 100%; 335 margin-top: 1rem; 336 padding: 0.875rem 1.5rem; 337 background: var(--accent); 338 color: var(--bg); 339 border: none; 340 border-radius: 999px; 341 font-family: var(--font-sans); 342 font-size: 0.9375rem; 343 font-weight: 500; 344 cursor: pointer; 345 transition: opacity var(--transition), transform var(--transition); 346 } 347 .upload-btn:hover { opacity: 0.85; transform: translateY(-1px); } 348 .upload-btn:active { transform: scale(0.99); } 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 } 367 368 .status-msg { 369 text-align: center; 370 margin-top: 0.75rem; 371 font-size: 0.875rem; 372 min-height: 1.3em; 373 color: var(--accent); 374 } 375 376 .result-box { 377 display: none; 378 margin-top: 1.25rem; 379 padding: 1.25rem; 380 background: var(--bg-card); 381 border: 1px solid var(--green); 382 border-radius: var(--radius); 383 animation: slideIn 0.25s ease; 384 } 385 .result-box.show { display: block; } 386 387 @keyframes slideIn { 388 from { opacity: 0; transform: translateY(-6px); } 389 to { opacity: 1; transform: translateY(0); } 390 } 391 392 .result-label { 393 font-size: 0.75rem; 394 text-transform: uppercase; 395 letter-spacing: 0.06em; 396 color: var(--text-dim); 397 margin-bottom: 0.625rem; 398 } 399 400 .result-url { 401 font-family: var(--font-mono); 402 font-size: 0.8125rem; 403 padding: 0.625rem 0.875rem; 404 background: var(--bg-raised); 405 border: 1px solid var(--border); 406 border-radius: 6px; 407 word-break: break-all; 408 color: var(--text); 409 margin-bottom: 0.875rem; 410 } 411 412 .result-actions { display: flex; gap: 0.625rem; } 413 414 .copy-btn, .open-btn { 415 display: inline-flex; 416 align-items: center; 417 gap: 0.4rem; 418 padding: 0.5rem 1rem; 419 background: var(--bg-raised); 420 border: 1px solid var(--border); 421 border-radius: 999px; 422 color: var(--text-muted); 423 font-family: var(--font-sans); 424 font-size: 0.8125rem; 425 font-weight: 500; 426 cursor: pointer; 427 transition: border-color var(--transition), color var(--transition); 428 text-decoration: none; 429 } 430 .copy-btn:hover, .open-btn:hover { border-color: var(--accent); color: var(--accent); opacity: 1; } 431 .copy-btn.copied { background: var(--green); border-color: var(--green); color: var(--bg); } 432 433 /* ── About tab ── */ 434 .prose { 435 font-size: 0.9375rem; 436 color: var(--text-muted); 437 line-height: 1.8; 438 } 439 .prose p + p { margin-top: 1rem; } 440 .prose a { color: var(--accent); border-bottom: 1px solid var(--accent-dim); } 441 .prose a:hover { border-bottom-color: var(--accent); opacity: 1; } 442 443 /* ── How it works tab ── */ 444 .steps { 445 display: flex; 446 flex-direction: column; 447 gap: 0.125rem; 448 margin-bottom: 2rem; 449 } 450 451 .step { 452 display: flex; 453 gap: 1rem; 454 padding: 1.125rem 0; 455 border-bottom: 1px solid var(--border); 456 } 457 .step:last-child { border-bottom: none; } 458 459 .step-num { 460 flex-shrink: 0; 461 width: 1.75rem; 462 height: 1.75rem; 463 border-radius: 50%; 464 background: var(--bg-card); 465 border: 1px solid var(--border); 466 display: flex; 467 align-items: center; 468 justify-content: center; 469 font-size: 0.75rem; 470 font-weight: 500; 471 color: var(--accent); 472 margin-top: 0.125rem; 473 } 474 475 .step-body h3 { 476 font-family: var(--font-serif); 477 font-size: 1rem; 478 font-weight: 400; 479 color: var(--text); 480 margin-bottom: 0.25rem; 481 } 482 .step-body p { 483 font-size: 0.875rem; 484 color: var(--text-muted); 485 line-height: 1.7; 486 } 487 488 .how-grid { 489 display: grid; 490 grid-template-columns: repeat(3, 1fr); 491 gap: 0.75rem; 492 } 493 494 .how-card { 495 background: var(--bg-card); 496 border: 1px solid var(--border); 497 border-radius: var(--radius); 498 padding: 1.125rem; 499 transition: border-color var(--transition); 500 } 501 .how-card:hover { border-color: var(--border-hover); } 502 503 .how-card i { 504 font-size: 1.125rem; 505 color: var(--accent); 506 opacity: 0.8; 507 margin-bottom: 0.625rem; 508 display: block; 509 } 510 511 .how-card h3 { 512 font-size: 0.875rem; 513 font-weight: 500; 514 color: var(--text); 515 margin-bottom: 0.375rem; 516 } 517 518 .how-card p { 519 font-size: 0.8125rem; 520 color: var(--text-muted); 521 line-height: 1.6; 522 } 523 524 /* ── Limits tab ── */ 525 .limits-grid { 526 display: grid; 527 grid-template-columns: repeat(3, 1fr); 528 gap: 0.75rem; 529 margin-bottom: 1.5rem; 530 } 531 532 .limit-card { 533 background: var(--bg-card); 534 border-radius: var(--radius); 535 padding: 1.25rem; 536 text-align: center; 537 } 538 539 .limit-card i { 540 font-size: 1.25rem; 541 color: var(--accent); 542 opacity: 0.7; 543 margin-bottom: 0.625rem; 544 display: block; 545 } 546 547 .limit-value { 548 font-family: var(--font-serif); 549 font-size: 1.5rem; 550 color: var(--text); 551 letter-spacing: -0.02em; 552 margin-bottom: 0.25rem; 553 } 554 555 .limit-label { 556 font-size: 0.75rem; 557 color: var(--text-dim); 558 text-transform: uppercase; 559 letter-spacing: 0.05em; 560 font-weight: 500; 561 } 562 563 .limit-note { 564 font-size: 0.7rem; 565 color: var(--text-dim); 566 margin-top: 0.25rem; 567 opacity: 0.7; 568 } 569 570 .limits-note { 571 padding: 0.875rem 1.125rem; 572 background: var(--bg-card); 573 border-radius: var(--radius); 574 font-size: 0.8375rem; 575 color: var(--text-muted); 576 line-height: 1.65; 577 margin-bottom: 2rem; 578 } 579 580 .usage-section { margin-top: 1.75rem; } 581 582 .usage-item { margin-bottom: 1.25rem; } 583 584 .usage-row { 585 display: flex; 586 justify-content: space-between; 587 font-size: 0.8125rem; 588 color: var(--text-muted); 589 margin-bottom: 0.5rem; 590 } 591 .usage-row span:first-child { color: var(--text); font-weight: 500; } 592 593 .usage-track { 594 height: 6px; 595 background: var(--bg-raised); 596 border-radius: 999px; 597 overflow: hidden; 598 } 599 .usage-fill { 600 height: 100%; 601 border-radius: 999px; 602 background: linear-gradient(90deg, var(--accent), var(--green)); 603 transition: width 0.6s cubic-bezier(.4,0,.2,1); 604 } 605 .usage-fill.warn { background: linear-gradient(90deg, #d4882a, #e07a2a); } 606 .usage-fill.danger { background: linear-gradient(90deg, var(--red), #c05050); } 607 608 .usage-loading { font-size: 0.8125rem; color: var(--text-dim); font-style: italic; } 609 610 .divider { border: none; border-top: 1px solid var(--border); margin: 1.75rem 0; } 611 612 .file-types { 613 font-size: 0.875rem; 614 color: var(--text-muted); 615 line-height: 1.8; 616 } 617 .file-types strong { color: var(--text); } 618 619 /* ── Footer ── */ 620 .site-footer { 621 max-width: var(--max-w); 622 margin: 0 auto; 623 padding: 1.75rem 1.5rem 2.5rem; 624 border-top: 1px solid var(--border); 625 font-size: 0.8125rem; 626 color: var(--text-dim); 627 } 628 .site-footer a { color: var(--text-muted); } 629 .site-footer a:hover { color: var(--text); opacity: 1; } 630 631 /* ── Responsive ── */ 632 @media (max-width: 600px) { 633 .stats-grid { grid-template-columns: repeat(2, 1fr); } 634 .how-grid, .limits-grid { grid-template-columns: 1fr; } 635 .nav-title { display: none; } 636 } 637 638 .hero-badges { 639 display: flex; 640 flex-wrap: wrap; 641 gap: 0.5rem; 642 margin-top: 1.25rem; 643 } 644 .badge { 645 display: inline-flex; 646 align-items: center; 647 gap: 0.35rem; 648 padding: 0.3rem 0.75rem; 649 border: 1px solid var(--border); 650 border-radius: 999px; 651 font-size: 0.75rem; 652 color: var(--text-muted); 653 background: var(--bg-card); 654 } 655 .badge i { font-size: 0.7rem; color: var(--accent); } 656 </style> 657</head> 658<body> 659 660 <header class="site-header"> 661 <nav class="nav-container"> 662 <a href="/" class="nav-logo"><a href="/" class="nav-logo">Daniel Morrisey <i>.com</i></a></a> 663 <span class="nav-title">MBD CDN</span> 664 </nav> 665 </header> 666 667 <main class="main-content"> 668 669 <!-- Hero --> 670 <section class="page-hero"> 671 <p class="page-hero-eyebrow">madebydanny.uk</p> 672 <h1>MBD CDN</h1> 673 <p>A simple, fast CDN powered by Cloudflare R2. Free for Life.</p> 674 <p>Are you a developer looking for a CDN with an API? <a href="/cdn/api.html"><b>Try the new API!</b></a></p> 675 </section> 676 677 <!-- Stats --> 678 <section class="stats-section"> 679 <p class="section-label">what's been uploaded</p> 680 <div class="stats-grid"> 681 <div class="stat-card"> 682 <div class="stat-icon"><i class="fa-regular fa-image"></i></div> 683 <div class="stat-value loading" id="stat-images"></div> 684 <div class="stat-label">Images</div> 685 </div> 686 <div class="stat-card"> 687 <div class="stat-icon"><i class="fa-solid fa-video"></i></div> 688 <div class="stat-value loading" id="stat-videos"></div> 689 <div class="stat-label">Videos</div> 690 </div> 691 <div class="stat-card"> 692 <div class="stat-icon"><i class="fa-solid fa-photo-film"></i></div> 693 <div class="stat-value loading" id="stat-gifs"></div> 694 <div class="stat-label">GIFs</div> 695 </div> 696 <div class="stat-card"> 697 <div class="stat-icon"><i class="fa-solid fa-file-code"></i></div> 698 <div class="stat-value loading" id="stat-documents"></div> 699 <div class="stat-label">Documents</div> 700 </div> 701 </div> 702 <div class="storage-row"> 703 <div class="storage-card"> 704 <div class="stat-icon"><i class="fa-solid fa-database"></i></div> 705 <div> 706 <div class="stat-value loading" id="stat-storage"></div> 707 <div class="stat-label">Storage Used</div> 708 </div> 709 </div> 710 </div> 711 </section> 712 713 <!-- Tabs --> 714 <section class="tabs-section"> 715 <div class="tab-bar"> 716 <button class="tab-btn active" onclick="switchTab('upload', this)"> 717 <i class="fa-solid fa-cloud-arrow-up"></i> Upload 718 </button> 719 <button class="tab-btn" onclick="switchTab('about', this)"> 720 <i class="fa-solid fa-circle-info"></i> About 721 </button> 722 <button class="tab-btn" onclick="switchTab('how', this)"> 723 <i class="fa-solid fa-gears"></i> How it Works 724 </button> 725 <button class="tab-btn" onclick="switchTab('limits', this)"> 726 <i class="fa-solid fa-gauge-high"></i> Limits 727 </button> 728 </div> 729 730 <!-- Upload --> 731 <div class="tab-pane active" id="tab-upload"> 732 <label class="drop-zone" id="drop-zone" for="file-input"> 733 <i class="fa-solid fa-cloud-arrow-up" id="drop-icon"></i> 734 <span id="file-name">Click to select or drag a file here</span> 735 <input type="file" id="file-input" accept="image/*,video/*,text/html,text/css,text/javascript,text/plain,text/csv,text/xml,text/markdown,text/yaml,application/json,application/xml,application/pdf,application/javascript,application/x-yaml"> 736 </label> 737 738 <div class="file-info" id="file-info"> 739 <strong id="detail-name"></strong> 740 <span style="color:var(--text-dim)"> · </span><span id="detail-size"></span> 741 <span style="color:var(--text-dim)"> · </span><span id="detail-type"></span> 742 </div> 743 744 <div class="progress-wrap" id="progress-wrap"> 745 <div class="progress-track"> 746 <div class="progress-fill" id="progress-fill"></div> 747 </div> 748 <div class="progress-label" id="progress-label">0%</div> 749 </div> 750 751 <div class="turnstile-wrap" id="turnstile-wrap"> 752 <div class="cf-turnstile" id="cf-turnstile" data-sitekey="0x4AAAAAADElA2DcjN_ENLMX" data-theme="dark"></div> 753 </div> 754 755 <button class="upload-btn" id="upload-btn"> 756 <i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN 757 </button> 758 759 <div class="status-msg" id="status"></div> 760 761 <div class="result-box" id="result-box"> 762 <div class="result-label">✓ Your file is live</div> 763 <div class="result-url" id="result-url"></div> 764 <div class="result-actions"> 765 <button class="copy-btn" id="copy-btn" onclick="copyUrl()"> 766 <i class="fa-solid fa-copy"></i> Copy URL 767 </button> 768 <a class="open-btn" id="open-link" href="#" target="_blank" rel="noopener"> 769 <i class="fa-solid fa-arrow-up-right-from-square"></i> Open 770 </a> 771 </div> 772 </div> 773 </div> 774 775 <!-- About --> 776 <div class="tab-pane" id="tab-about"> 777 <div class="prose"> 778 <p>MBD CDN is a content delivery network built by <a href="https://madebydanny.uk">madebydanny.uk</a> 779 to host and serve media files — images, GIFs, videos, and documents — at fast speeds with global availability.</p> 780 <p>Files uploaded here are stored in <strong style="color:var(--text)">Cloudflare R2</strong> object storage 781 and served from Cloudflare's global edge network, which spans over 310 cities worldwide. Your media is delivered 782 from a server close to whoever is viewing it, minimising latency.</p> 783 <p>The platform is designed to be simple and permanent. Files are stored indefinitely once uploaded 784 and immediately available via a public URL — no sign-up, no expiry, no catch.</p> 785 <p>The underlying stack is a <strong style="color:var(--text)">Cloudflare Worker</strong> with R2 for storage and 786 D1 for metadata. The whole thing runs at the edge with no cold starts.</p> 787 </div> 788 </div> 789 790 <!-- How it Works --> 791 <div class="tab-pane" id="tab-how"> 792 <div class="steps"> 793 <div class="step"> 794 <div class="step-num">1</div> 795 <div class="step-body"> 796 <h3>You select a file</h3> 797 <p>Your file is read locally in the browser and sent directly to the CDN API over HTTPS — straight to the Cloudflare edge, no intermediate servers.</p> 798 </div> 799 </div> 800 <div class="step"> 801 <div class="step-num">2</div> 802 <div class="step-body"> 803 <h3>A Worker receives it</h3> 804 <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 with no cold starts.</p> 805 </div> 806 </div> 807 <div class="step"> 808 <div class="step-num">3</div> 809 <div class="step-body"> 810 <h3>R2 stores it permanently</h3> 811 <p>The file is written to Cloudflare R2 — S3-compatible storage with zero egress fees and 11 nines of durability. Metadata is logged to D1 to track stats.</p> 812 </div> 813 </div> 814 <div class="step"> 815 <div class="step-num">4</div> 816 <div class="step-body"> 817 <h3>You get a public URL</h3> 818 <p>A permanent <code>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> 819 </div> 820 </div> 821 </div> 822 823 <div class="how-grid"> 824 <div class="how-card"> 825 <i class="fa-brands fa-cloudflare"></i> 826 <h3>310+ Edge Locations</h3> 827 <p>Files cached and served globally — sub-50ms for most users.</p> 828 </div> 829 <div class="how-card"> 830 <i class="fa-solid fa-database"></i> 831 <h3>R2 Object Storage</h3> 832 <p>Zero egress fees, no expiry. Enterprise-grade durability.</p> 833 </div> 834 <div class="how-card"> 835 <i class="fa-solid fa-bolt"></i> 836 <h3>Zero Cold Starts</h3> 837 <p>Workers run at the edge — every request is handled immediately.</p> 838 </div> 839 </div> 840 </div> 841 842 <!-- Limits --> 843 <div class="tab-pane" id="tab-limits"> 844 <div class="limits-grid"> 845 <div class="limit-card"> 846 <i class="fa-solid fa-file-arrow-up"></i> 847 <div class="limit-value" id="limit-max-file"></div> 848 <div class="limit-label">Max File Size</div> 849 <div class="limit-note">Per individual upload</div> 850 </div> 851 <div class="limit-card"> 852 <i class="fa-solid fa-hard-drive"></i> 853 <div class="limit-value" id="limit-max-bytes"></div> 854 <div class="limit-label">Daily Storage</div> 855 <div class="limit-note">Total uploads per day</div> 856 </div> 857 <div class="limit-card"> 858 <i class="fa-solid fa-arrow-up-from-bracket"></i> 859 <div class="limit-value" id="limit-max-files"></div> 860 <div class="limit-label">Uploads Per Day</div> 861 <div class="limit-note">Resets at midnight UTC</div> 862 </div> 863 </div> 864 865 <div class="limits-note"> 866 All limits reset daily at <strong>midnight UTC</strong> and are enforced per IP to protect performance for all users. 867 <p><b>Need higher limits? <a href="/cdn/api.html">Try the new API!</b></a></p> 868 </div> 869 870 <div class="usage-section"> 871 <p class="section-label">your usage today</p> 872 <div id="usage-loading" class="usage-loading">Loading your usage…</div> 873 <div id="usage-bars" style="display:none"> 874 <div class="usage-item"> 875 <div class="usage-row"> 876 <span>Files Uploaded</span> 877 <span id="usage-files-label">0 / 25</span> 878 </div> 879 <div class="usage-track"> 880 <div class="usage-fill" id="usage-files-fill" style="width:0%"></div> 881 </div> 882 </div> 883 <div class="usage-item"> 884 <div class="usage-row"> 885 <span>Storage Used</span> 886 <span id="usage-bytes-label">0 B / 200 MB</span> 887 </div> 888 <div class="usage-track"> 889 <div class="usage-fill" id="usage-bytes-fill" style="width:0%"></div> 890 </div> 891 </div> 892 </div> 893 </div> 894 895 <hr class="divider"> 896 897 <p class="section-label">accepted file types</p> 898 <p class="file-types"> 899 <strong>Images</strong> — JPEG, PNG, WebP, AVIF, SVG &nbsp;·&nbsp; 900 <strong>Animated</strong> — GIF &nbsp;·&nbsp; 901 <strong>Video</strong> — MP4, WebM, MOV &nbsp;·&nbsp; 902 <strong>Documents</strong> — PDF, JSON, HTML, CSS, JS, CSV, Markdown 903 </p> 904 </div> 905 906 </section> 907 </main> 908 909 <footer class="site-footer"> 910 <p>© 2024–26 Daniel Morrisey · <a href="https://madebydanny.uk">madebydanny.uk</a></p> 911 </footer> 912 913 <script> 914 const API = 'https://cdn.madebydanny.uk'; 915 916 // ── Utilities ────────────────────────────────────────────────────────── 917 918 function formatBytes(b) { 919 if (!b) return '0 B'; 920 const k = 1024, s = ['B','KB','MB','GB','TB']; 921 const i = Math.floor(Math.log(b) / Math.log(k)); 922 return (b / Math.pow(k, i)).toFixed(1).replace(/\.0$/, '') + ' ' + s[i]; 923 } 924 925 function fmt(n) { return Number(n).toLocaleString(); } 926 927 // ── Tabs ─────────────────────────────────────────────────────────────── 928 929 function switchTab(name, btn) { 930 document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); 931 document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); 932 document.getElementById('tab-' + name).classList.add('active'); 933 if (btn) btn.classList.add('active'); 934 if (name === 'limits') loadLimits(); 935 } 936 937 // ── Stats ────────────────────────────────────────────────────────────── 938 939 async function loadStats() { 940 try { 941 const r = await fetch(`${API}/stats`); 942 const d = await r.json(); 943 if (!d.success) throw new Error(); 944 const cat = d.stats.byCategory || {}; 945 document.getElementById('stat-images').textContent = fmt(cat.image || 0); 946 document.getElementById('stat-videos').textContent = fmt(cat.video || 0); 947 document.getElementById('stat-gifs').textContent = fmt(cat.gif || 0); 948 document.getElementById('stat-documents').textContent = fmt(cat.document || 0); 949 document.getElementById('stat-storage').textContent = formatBytes(d.stats.totalSize); 950 document.querySelectorAll('.stat-value').forEach(v => v.classList.remove('loading')); 951 } catch { 952 document.querySelectorAll('.stat-value').forEach(v => { 953 v.textContent = '—'; 954 v.classList.remove('loading'); 955 }); 956 } 957 } 958 959 // ── Limits ───────────────────────────────────────────────────────────── 960 961 async function loadLimits() { 962 try { 963 const r = await fetch(`${API}/limits`); 964 const d = await r.json(); 965 if (!d.success) throw new Error(); 966 const { file_count, total_size, max_files, max_bytes, max_file } = d.limits; 967 968 document.getElementById('limit-max-file').textContent = formatBytes(max_file); 969 document.getElementById('limit-max-bytes').textContent = formatBytes(max_bytes); 970 document.getElementById('limit-max-files').textContent = max_files; 971 972 const filePct = Math.min((file_count / max_files) * 100, 100); 973 const bytesPct = Math.min((total_size / max_bytes) * 100, 100); 974 975 const filesFill = document.getElementById('usage-files-fill'); 976 const bytesFill = document.getElementById('usage-bytes-fill'); 977 978 filesFill.style.width = filePct + '%'; 979 bytesFill.style.width = bytesPct + '%'; 980 981 function fillClass(pct) { 982 return pct >= 90 ? 'usage-fill danger' : pct >= 70 ? 'usage-fill warn' : 'usage-fill'; 983 } 984 985 filesFill.className = fillClass(filePct); 986 bytesFill.className = fillClass(bytesPct); 987 988 document.getElementById('usage-files-label').textContent = 989 `${fmt(file_count)} / ${fmt(max_files)}`; 990 document.getElementById('usage-bytes-label').textContent = 991 `${formatBytes(total_size)} / ${formatBytes(max_bytes)}`; 992 993 document.getElementById('usage-loading').style.display = 'none'; 994 document.getElementById('usage-bars').style.display = 'block'; 995 } catch { 996 document.getElementById('usage-loading').textContent = 'Could not load usage data.'; 997 } 998 } 999 1000 // ── File icon helper ─────────────────────────────────────────────────── 1001 1002 function getFileIcon(type) { 1003 if (!type) return 'fa-solid fa-cloud-arrow-up'; 1004 if (type.startsWith('video/')) return 'fa-solid fa-film'; 1005 if (type === 'image/gif') return 'fa-solid fa-photo-film'; 1006 if (type.startsWith('image/')) return 'fa-regular fa-image'; 1007 if (type === 'application/pdf') return 'fa-solid fa-file-pdf'; 1008 if (type === 'text/csv') return 'fa-solid fa-file-csv'; 1009 if (type === 'text/plain') return 'fa-solid fa-file-lines'; 1010 return 'fa-solid fa-file-code'; 1011 } 1012 1013 // ── File select / drag-drop ──────────────────────────────────────────── 1014 1015 const fileInput = document.getElementById('file-input'); 1016 const dropZone = document.getElementById('drop-zone'); 1017 const fileInfo = document.getElementById('file-info'); 1018 1019 function showFile(file) { 1020 document.getElementById('drop-icon').className = getFileIcon(file.type); 1021 document.getElementById('file-name').textContent = file.name; 1022 document.getElementById('detail-name').textContent = file.name; 1023 document.getElementById('detail-size').textContent = formatBytes(file.size); 1024 document.getElementById('detail-type').textContent = file.type || 'unknown'; 1025 fileInfo.classList.add('show'); 1026 } 1027 1028 fileInput.addEventListener('change', () => { if (fileInput.files[0]) showFile(fileInput.files[0]); }); 1029 1030 dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); }); 1031 dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); }); 1032 dropZone.addEventListener('drop', e => { 1033 e.preventDefault(); 1034 dropZone.classList.remove('drag-over'); 1035 const f = e.dataTransfer.files[0]; 1036 if (f) { fileInput.files = e.dataTransfer.files; showFile(f); } 1037 }); 1038 1039 // ── Upload ───────────────────────────────────────────────────────────── 1040 1041 const uploadBtn = document.getElementById('upload-btn'); 1042 const progressWrap = document.getElementById('progress-wrap'); 1043 const progressFill = document.getElementById('progress-fill'); 1044 const progressLabel = document.getElementById('progress-label'); 1045 const statusEl = document.getElementById('status'); 1046 const resultBox = document.getElementById('result-box'); 1047 const resultUrl = document.getElementById('result-url'); 1048 const turnstileWrap = document.getElementById('turnstile-wrap'); 1049 1050 function setStatus(msg, color) { 1051 statusEl.textContent = msg; 1052 statusEl.style.color = color || 'var(--accent)'; 1053 } 1054 1055 function setProgress(pct) { 1056 progressFill.style.width = pct + '%'; 1057 progressLabel.textContent = Math.round(pct) + '%'; 1058 } 1059 1060 function resetUploadUI(delay = 0) { 1061 setTimeout(() => { 1062 fileInput.value = ''; 1063 document.getElementById('drop-icon').className = 'fa-solid fa-cloud-arrow-up'; 1064 document.getElementById('file-name').textContent = 'Click to select or drag a file here'; 1065 fileInfo.classList.remove('show'); 1066 progressWrap.classList.remove('show'); 1067 setProgress(0); 1068 // Reset Turnstile 1069 if (window.turnstile) { 1070 window.turnstile.reset(); 1071 } 1072 }, delay); 1073 } 1074 1075 uploadBtn.addEventListener('click', () => { 1076 if (!fileInput.files[0]) { 1077 setStatus('Please select a file first.', 'var(--red)'); 1078 return; 1079 } 1080 1081 // Get Turnstile token 1082 const token = window.turnstile?.getResponse?.(); 1083 1084 if (!token) { 1085 setStatus('Please complete the verification.', 'var(--red)'); 1086 return; 1087 } 1088 1089 const file = fileInput.files[0]; 1090 uploadBtn.disabled = true; 1091 uploadBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Uploading…'; 1092 setStatus(''); 1093 resultBox.classList.remove('show'); 1094 progressWrap.classList.add('show'); 1095 setProgress(0); 1096 1097 const xhr = new XMLHttpRequest(); 1098 1099 xhr.upload.addEventListener('progress', e => { 1100 if (e.lengthComputable) setProgress((e.loaded / e.total) * 100); 1101 }); 1102 1103 xhr.addEventListener('load', () => { 1104 setProgress(100); 1105 try { 1106 const data = JSON.parse(xhr.responseText); 1107 if (data.success) { 1108 resultUrl.textContent = data.url; 1109 document.getElementById('open-link').href = data.url; 1110 resultBox.classList.add('show'); 1111 setStatus(''); 1112 setTimeout(() => loadStats(), 600); 1113 resetUploadUI(3000); 1114 } else { 1115 throw new Error(data.error || 'Upload failed'); 1116 } 1117 } catch (err) { 1118 progressWrap.classList.remove('show'); 1119 setStatus('Error: ' + err.message, 'var(--red)'); 1120 } 1121 uploadBtn.disabled = false; 1122 uploadBtn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN'; 1123 }); 1124 1125 xhr.addEventListener('error', () => { 1126 progressWrap.classList.remove('show'); 1127 setStatus('Network error. Please try again.', 'var(--red)'); 1128 uploadBtn.disabled = false; 1129 uploadBtn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN'; 1130 }); 1131 1132 const apiUrl = API + (API.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(token); 1133 xhr.open('POST', apiUrl); 1134 xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream'); 1135 xhr.send(file); 1136 }); 1137 1138 // ── Copy URL ─────────────────────────────────────────────────────────── 1139 1140 function copyUrl() { 1141 navigator.clipboard.writeText(resultUrl.textContent); 1142 const btn = document.getElementById('copy-btn'); 1143 btn.classList.add('copied'); 1144 btn.innerHTML = '<i class="fa-solid fa-check"></i> Copied'; 1145 setTimeout(() => { 1146 btn.classList.remove('copied'); 1147 btn.innerHTML = '<i class="fa-solid fa-copy"></i> Copy URL'; 1148 }, 2000); 1149 } 1150 1151 // ── Init ─────────────────────────────────────────────────────────────── 1152 1153 loadStats(); 1154 </script> 1155 1156</body> 1157</html>