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.

new website design

+1932 -2717
.DS_Store

This is a binary file and will not be displayed.

+1105
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 + <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://imrs.madebydanny.uk?url=https://public-cdn.madebydanny.uk/user-content/2026-01-30/cb09a559-ae35-4617-971c-9230521f7a9c.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 + 19 + <style> 20 + /* ── Design tokens (match main site) ── */ 21 + :root { 22 + --bg: #0e0d0c; 23 + --bg-raised: #171512; 24 + --bg-card: #1a1815; 25 + --border: #2d2926; 26 + --border-hover:#4a4238; 27 + 28 + --text: #e8e0d8; 29 + --text-muted: #8a7f74; 30 + --text-dim: #584f47; 31 + 32 + --accent: #c9a96e; 33 + --accent-dim: rgba(201,169,110,0.12); 34 + --accent-glow: rgba(201,169,110,0.08); 35 + 36 + --green: #4caf7d; 37 + --red: #e06c6c; 38 + 39 + --font-serif: 'Lora', Georgia, serif; 40 + --font-sans: 'DM Sans', system-ui, sans-serif; 41 + --font-mono: 'Monaco', 'Courier New', monospace; 42 + 43 + --radius: 10px; 44 + --max-w: 720px; 45 + --transition: 0.2s ease; 46 + } 47 + 48 + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 49 + html { scroll-behavior: smooth; } 50 + 51 + body { 52 + font-family: var(--font-sans); 53 + font-size: 16px; 54 + line-height: 1.7; 55 + color: var(--text); 56 + background: var(--bg); 57 + -webkit-font-smoothing: antialiased; 58 + } 59 + 60 + a { color: var(--accent); text-decoration: none; transition: opacity var(--transition); } 61 + a:hover { opacity: 0.75; } 62 + 63 + code { 64 + font-family: var(--font-mono); 65 + font-size: 0.85em; 66 + background: var(--bg-raised); 67 + padding: 0.15em 0.4em; 68 + border-radius: 4px; 69 + color: var(--accent); 70 + } 71 + 72 + /* ── Header ── */ 73 + .site-header { 74 + position: sticky; 75 + top: 0; 76 + z-index: 100; 77 + background: rgba(14,13,12,0.92); 78 + backdrop-filter: blur(12px); 79 + border-bottom: 1px solid var(--border); 80 + } 81 + 82 + .nav-container { 83 + max-width: var(--max-w); 84 + margin: 0 auto; 85 + padding: 0.875rem 1.5rem; 86 + display: flex; 87 + justify-content: space-between; 88 + align-items: center; 89 + } 90 + 91 + .nav-logo { 92 + font-family: var(--font-serif); 93 + font-style: italic; 94 + font-size: 1.1rem; 95 + color: var(--text-muted); 96 + } 97 + .nav-logo:hover { color: var(--text); opacity: 1; } 98 + 99 + .nav-title { 100 + font-family: var(--font-serif); 101 + font-size: 1.125rem; 102 + color: var(--text); 103 + } 104 + 105 + /* ── Main ── */ 106 + .main-content { 107 + max-width: var(--max-w); 108 + margin: 0 auto; 109 + padding: 0 1.5rem; 110 + } 111 + 112 + /* ── Page hero ── */ 113 + .page-hero { 114 + padding: 3rem 0 2rem; 115 + border-bottom: 1px solid var(--border); 116 + } 117 + 118 + .page-hero-eyebrow { 119 + font-size: 0.75rem; 120 + text-transform: uppercase; 121 + letter-spacing: 0.1em; 122 + color: var(--text-dim); 123 + margin-bottom: 0.5rem; 124 + } 125 + 126 + .page-hero h1 { 127 + font-family: var(--font-serif); 128 + font-size: clamp(2rem, 5vw, 2.75rem); 129 + font-weight: 400; 130 + color: var(--text); 131 + letter-spacing: -0.02em; 132 + margin-bottom: 0.625rem; 133 + } 134 + 135 + .page-hero p { 136 + font-size: 0.9375rem; 137 + color: var(--text-muted); 138 + max-width: 520px; 139 + } 140 + 141 + /* ── Stats ── */ 142 + .stats-section { 143 + padding: 1.75rem 0; 144 + border-bottom: 1px solid var(--border); 145 + } 146 + 147 + .section-label { 148 + font-family: var(--font-sans); 149 + font-size: 0.75rem; 150 + font-weight: 500; 151 + text-transform: uppercase; 152 + letter-spacing: 0.1em; 153 + color: var(--text-dim); 154 + margin-bottom: 1rem; 155 + } 156 + 157 + .stats-grid { 158 + display: grid; 159 + grid-template-columns: repeat(4, 1fr); 160 + gap: 0.75rem; 161 + } 162 + 163 + .stat-card { 164 + background: var(--bg-card); 165 + border: 1px solid var(--border); 166 + border-radius: var(--radius); 167 + padding: 1rem; 168 + text-align: center; 169 + transition: border-color var(--transition); 170 + } 171 + .stat-card:hover { border-color: var(--border-hover); } 172 + 173 + .stat-icon { 174 + font-size: 1.1rem; 175 + color: var(--accent); 176 + opacity: 0.7; 177 + margin-bottom: 0.375rem; 178 + } 179 + 180 + .stat-value { 181 + font-family: var(--font-serif); 182 + font-size: 1.5rem; 183 + color: var(--text); 184 + letter-spacing: -0.02em; 185 + line-height: 1.2; 186 + } 187 + 188 + .stat-value.loading { 189 + color: var(--text-dim); 190 + animation: pulse 1.6s ease-in-out infinite; 191 + } 192 + 193 + @keyframes pulse { 0%,100%{opacity:.4} 50%{opacity:.8} } 194 + 195 + .stat-label { 196 + font-size: 0.75rem; 197 + color: var(--text-dim); 198 + text-transform: uppercase; 199 + letter-spacing: 0.05em; 200 + margin-top: 0.2rem; 201 + } 202 + 203 + .storage-row { 204 + margin-top: 0.75rem; 205 + } 206 + 207 + .storage-card { 208 + background: var(--bg-card); 209 + border: 1px solid var(--border); 210 + border-radius: var(--radius); 211 + padding: 1rem 1.25rem; 212 + display: flex; 213 + align-items: center; 214 + gap: 1rem; 215 + transition: border-color var(--transition); 216 + } 217 + .storage-card:hover { border-color: var(--border-hover); } 218 + 219 + .storage-card .stat-icon { margin-bottom: 0; font-size: 1.25rem; flex-shrink: 0; } 220 + 221 + .storage-card .stat-value { font-size: 1.25rem; } 222 + 223 + /* ── Tabs ── */ 224 + .tabs-section { 225 + padding-top: 1.75rem; 226 + } 227 + 228 + .tab-bar { 229 + display: flex; 230 + gap: 0; 231 + border-bottom: 1px solid var(--border); 232 + overflow-x: auto; 233 + scrollbar-width: none; 234 + margin-bottom: 2rem; 235 + } 236 + .tab-bar::-webkit-scrollbar { display: none; } 237 + 238 + .tab-btn { 239 + font-family: var(--font-sans); 240 + font-size: 0.875rem; 241 + font-weight: 500; 242 + color: var(--text-muted); 243 + background: none; 244 + border: none; 245 + border-bottom: 2px solid transparent; 246 + padding: 0.625rem 1.125rem; 247 + cursor: pointer; 248 + white-space: nowrap; 249 + margin-bottom: -1px; 250 + transition: color var(--transition), border-color var(--transition); 251 + } 252 + .tab-btn:hover { color: var(--text); } 253 + .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); } 254 + 255 + .tab-pane { display: none; padding-bottom: 4rem; } 256 + .tab-pane.active { display: block; } 257 + 258 + /* ── Upload tab ── */ 259 + .upload-area { 260 + margin-bottom: 1.5rem; 261 + } 262 + 263 + .drop-zone { 264 + display: block; 265 + border: 1px dashed var(--border-hover); 266 + border-radius: var(--radius); 267 + padding: 3rem 1.5rem; 268 + text-align: center; 269 + cursor: pointer; 270 + color: var(--text-muted); 271 + transition: border-color var(--transition), background var(--transition); 272 + } 273 + .drop-zone:hover { 274 + border-color: var(--accent); 275 + background: var(--accent-glow); 276 + } 277 + .drop-zone.drag-over { 278 + border-color: var(--green); 279 + background: rgba(76,175,125,0.05); 280 + } 281 + .drop-zone i { 282 + font-size: 2rem; 283 + color: var(--accent); 284 + opacity: 0.6; 285 + display: block; 286 + margin-bottom: 0.75rem; 287 + } 288 + .drop-zone input { display: none; } 289 + 290 + #file-name { 291 + font-size: 0.9rem; 292 + display: block; 293 + margin-top: 0.25rem; 294 + } 295 + 296 + .file-info { 297 + display: none; 298 + margin-top: 0.875rem; 299 + padding: 0.75rem 1rem; 300 + background: var(--bg-card); 301 + border: 1px solid var(--border); 302 + border-radius: var(--radius); 303 + font-size: 0.8125rem; 304 + color: var(--text-muted); 305 + } 306 + .file-info.show { display: block; } 307 + 308 + .progress-wrap { display: none; margin-top: 0.875rem; } 309 + .progress-wrap.show { display: block; } 310 + 311 + .progress-track { 312 + height: 5px; 313 + background: var(--bg-raised); 314 + border-radius: 999px; 315 + overflow: hidden; 316 + } 317 + .progress-fill { 318 + height: 100%; 319 + width: 0%; 320 + background: linear-gradient(90deg, var(--accent), var(--green)); 321 + border-radius: 999px; 322 + transition: width 0.2s ease; 323 + } 324 + .progress-label { 325 + text-align: right; 326 + font-size: 0.75rem; 327 + color: var(--text-dim); 328 + margin-top: 0.3rem; 329 + } 330 + 331 + .upload-btn { 332 + display: block; 333 + width: 100%; 334 + margin-top: 1rem; 335 + padding: 0.875rem 1.5rem; 336 + background: var(--accent); 337 + color: var(--bg); 338 + border: none; 339 + border-radius: 999px; 340 + font-family: var(--font-sans); 341 + font-size: 0.9375rem; 342 + font-weight: 500; 343 + cursor: pointer; 344 + transition: opacity var(--transition), transform var(--transition); 345 + } 346 + .upload-btn:hover { opacity: 0.85; transform: translateY(-1px); } 347 + .upload-btn:active { transform: scale(0.99); } 348 + .upload-btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none; } 349 + 350 + .status-msg { 351 + text-align: center; 352 + margin-top: 0.75rem; 353 + font-size: 0.875rem; 354 + min-height: 1.3em; 355 + color: var(--accent); 356 + } 357 + 358 + .result-box { 359 + display: none; 360 + margin-top: 1.25rem; 361 + padding: 1.25rem; 362 + background: var(--bg-card); 363 + border: 1px solid var(--green); 364 + border-radius: var(--radius); 365 + animation: slideIn 0.25s ease; 366 + } 367 + .result-box.show { display: block; } 368 + 369 + @keyframes slideIn { 370 + from { opacity: 0; transform: translateY(-6px); } 371 + to { opacity: 1; transform: translateY(0); } 372 + } 373 + 374 + .result-label { 375 + font-size: 0.75rem; 376 + text-transform: uppercase; 377 + letter-spacing: 0.06em; 378 + color: var(--text-dim); 379 + margin-bottom: 0.625rem; 380 + } 381 + 382 + .result-url { 383 + font-family: var(--font-mono); 384 + font-size: 0.8125rem; 385 + padding: 0.625rem 0.875rem; 386 + background: var(--bg-raised); 387 + border: 1px solid var(--border); 388 + border-radius: 6px; 389 + word-break: break-all; 390 + color: var(--text); 391 + margin-bottom: 0.875rem; 392 + } 393 + 394 + .result-actions { display: flex; gap: 0.625rem; } 395 + 396 + .copy-btn, .open-btn { 397 + display: inline-flex; 398 + align-items: center; 399 + gap: 0.4rem; 400 + padding: 0.5rem 1rem; 401 + background: var(--bg-raised); 402 + border: 1px solid var(--border); 403 + border-radius: 999px; 404 + color: var(--text-muted); 405 + font-family: var(--font-sans); 406 + font-size: 0.8125rem; 407 + font-weight: 500; 408 + cursor: pointer; 409 + transition: border-color var(--transition), color var(--transition); 410 + text-decoration: none; 411 + } 412 + .copy-btn:hover, .open-btn:hover { border-color: var(--accent); color: var(--accent); opacity: 1; } 413 + .copy-btn.copied { background: var(--green); border-color: var(--green); color: var(--bg); } 414 + 415 + /* ── About tab ── */ 416 + .prose { 417 + font-size: 0.9375rem; 418 + color: var(--text-muted); 419 + line-height: 1.8; 420 + } 421 + .prose p + p { margin-top: 1rem; } 422 + .prose a { color: var(--accent); border-bottom: 1px solid var(--accent-dim); } 423 + .prose a:hover { border-bottom-color: var(--accent); opacity: 1; } 424 + 425 + /* ── How it works tab ── */ 426 + .steps { 427 + display: flex; 428 + flex-direction: column; 429 + gap: 0.125rem; 430 + margin-bottom: 2rem; 431 + } 432 + 433 + .step { 434 + display: flex; 435 + gap: 1rem; 436 + padding: 1.125rem 0; 437 + border-bottom: 1px solid var(--border); 438 + } 439 + .step:last-child { border-bottom: none; } 440 + 441 + .step-num { 442 + flex-shrink: 0; 443 + width: 1.75rem; 444 + height: 1.75rem; 445 + border-radius: 50%; 446 + background: var(--bg-card); 447 + border: 1px solid var(--border); 448 + display: flex; 449 + align-items: center; 450 + justify-content: center; 451 + font-size: 0.75rem; 452 + font-weight: 500; 453 + color: var(--accent); 454 + margin-top: 0.125rem; 455 + } 456 + 457 + .step-body h3 { 458 + font-family: var(--font-serif); 459 + font-size: 1rem; 460 + font-weight: 400; 461 + color: var(--text); 462 + margin-bottom: 0.25rem; 463 + } 464 + .step-body p { 465 + font-size: 0.875rem; 466 + color: var(--text-muted); 467 + line-height: 1.7; 468 + } 469 + 470 + .how-grid { 471 + display: grid; 472 + grid-template-columns: repeat(3, 1fr); 473 + gap: 0.75rem; 474 + } 475 + 476 + .how-card { 477 + background: var(--bg-card); 478 + border: 1px solid var(--border); 479 + border-radius: var(--radius); 480 + padding: 1.125rem; 481 + transition: border-color var(--transition); 482 + } 483 + .how-card:hover { border-color: var(--border-hover); } 484 + 485 + .how-card i { 486 + font-size: 1.125rem; 487 + color: var(--accent); 488 + opacity: 0.8; 489 + margin-bottom: 0.625rem; 490 + display: block; 491 + } 492 + 493 + .how-card h3 { 494 + font-size: 0.875rem; 495 + font-weight: 500; 496 + color: var(--text); 497 + margin-bottom: 0.375rem; 498 + } 499 + 500 + .how-card p { 501 + font-size: 0.8125rem; 502 + color: var(--text-muted); 503 + line-height: 1.6; 504 + } 505 + 506 + /* ── Limits tab ── */ 507 + .limits-grid { 508 + display: grid; 509 + grid-template-columns: repeat(3, 1fr); 510 + gap: 0.75rem; 511 + margin-bottom: 1.5rem; 512 + } 513 + 514 + .limit-card { 515 + background: var(--bg-card); 516 + border: 1px solid var(--border); 517 + border-top: 2px solid var(--accent); 518 + border-radius: var(--radius); 519 + padding: 1.25rem; 520 + text-align: center; 521 + } 522 + 523 + .limit-card i { 524 + font-size: 1.25rem; 525 + color: var(--accent); 526 + opacity: 0.7; 527 + margin-bottom: 0.625rem; 528 + display: block; 529 + } 530 + 531 + .limit-value { 532 + font-family: var(--font-serif); 533 + font-size: 1.5rem; 534 + color: var(--text); 535 + letter-spacing: -0.02em; 536 + margin-bottom: 0.25rem; 537 + } 538 + 539 + .limit-label { 540 + font-size: 0.75rem; 541 + color: var(--text-dim); 542 + text-transform: uppercase; 543 + letter-spacing: 0.05em; 544 + font-weight: 500; 545 + } 546 + 547 + .limit-note { 548 + font-size: 0.7rem; 549 + color: var(--text-dim); 550 + margin-top: 0.25rem; 551 + opacity: 0.7; 552 + } 553 + 554 + .limits-note { 555 + padding: 0.875rem 1.125rem; 556 + background: var(--bg-card); 557 + border: 1px solid var(--border); 558 + border-left: 3px solid var(--accent); 559 + border-radius: var(--radius); 560 + font-size: 0.8375rem; 561 + color: var(--text-muted); 562 + line-height: 1.65; 563 + margin-bottom: 2rem; 564 + } 565 + 566 + .usage-section { margin-top: 1.75rem; } 567 + 568 + .usage-item { margin-bottom: 1.25rem; } 569 + 570 + .usage-row { 571 + display: flex; 572 + justify-content: space-between; 573 + font-size: 0.8125rem; 574 + color: var(--text-muted); 575 + margin-bottom: 0.5rem; 576 + } 577 + .usage-row span:first-child { color: var(--text); font-weight: 500; } 578 + 579 + .usage-track { 580 + height: 6px; 581 + background: var(--bg-raised); 582 + border-radius: 999px; 583 + overflow: hidden; 584 + } 585 + .usage-fill { 586 + height: 100%; 587 + border-radius: 999px; 588 + background: linear-gradient(90deg, var(--accent), var(--green)); 589 + transition: width 0.6s cubic-bezier(.4,0,.2,1); 590 + } 591 + .usage-fill.warn { background: linear-gradient(90deg, #d4882a, #e07a2a); } 592 + .usage-fill.danger { background: linear-gradient(90deg, var(--red), #c05050); } 593 + 594 + .usage-loading { font-size: 0.8125rem; color: var(--text-dim); font-style: italic; } 595 + 596 + .divider { border: none; border-top: 1px solid var(--border); margin: 1.75rem 0; } 597 + 598 + .file-types { 599 + font-size: 0.875rem; 600 + color: var(--text-muted); 601 + line-height: 1.8; 602 + } 603 + .file-types strong { color: var(--text); } 604 + 605 + /* ── Footer ── */ 606 + .site-footer { 607 + max-width: var(--max-w); 608 + margin: 0 auto; 609 + padding: 1.75rem 1.5rem 2.5rem; 610 + border-top: 1px solid var(--border); 611 + font-size: 0.8125rem; 612 + color: var(--text-dim); 613 + } 614 + .site-footer a { color: var(--text-muted); } 615 + .site-footer a:hover { color: var(--text); opacity: 1; } 616 + 617 + /* ── Responsive ── */ 618 + @media (max-width: 600px) { 619 + .stats-grid { grid-template-columns: repeat(2, 1fr); } 620 + .how-grid, .limits-grid { grid-template-columns: 1fr; } 621 + .nav-title { display: none; } 622 + } 623 + </style> 624 + </head> 625 + <body> 626 + 627 + <header class="site-header"> 628 + <nav class="nav-container"> 629 + <a href="/" class="nav-logo"><a href="/" class="nav-logo">Daniel Morrisey <i>.com</i></a></a> 630 + <span class="nav-title">MBD CDN</span> 631 + </nav> 632 + </header> 633 + 634 + <main class="main-content"> 635 + 636 + <!-- Hero --> 637 + <section class="page-hero"> 638 + <p class="page-hero-eyebrow">madebydanny.uk</p> 639 + <h1>MBD CDN</h1> 640 + <p>A simple, fast CDN powered by Cloudflare R2 — free to use, no account needed.</p> 641 + </section> 642 + 643 + <!-- Stats --> 644 + <section class="stats-section"> 645 + <p class="section-label">what's been uploaded</p> 646 + <div class="stats-grid"> 647 + <div class="stat-card"> 648 + <div class="stat-icon"><i class="fa-regular fa-image"></i></div> 649 + <div class="stat-value loading" id="stat-images">—</div> 650 + <div class="stat-label">Images</div> 651 + </div> 652 + <div class="stat-card"> 653 + <div class="stat-icon"><i class="fa-solid fa-video"></i></div> 654 + <div class="stat-value loading" id="stat-videos">—</div> 655 + <div class="stat-label">Videos</div> 656 + </div> 657 + <div class="stat-card"> 658 + <div class="stat-icon"><i class="fa-solid fa-photo-film"></i></div> 659 + <div class="stat-value loading" id="stat-gifs">—</div> 660 + <div class="stat-label">GIFs</div> 661 + </div> 662 + <div class="stat-card"> 663 + <div class="stat-icon"><i class="fa-solid fa-file-code"></i></div> 664 + <div class="stat-value loading" id="stat-documents">—</div> 665 + <div class="stat-label">Documents</div> 666 + </div> 667 + </div> 668 + <div class="storage-row"> 669 + <div class="storage-card"> 670 + <div class="stat-icon"><i class="fa-solid fa-database"></i></div> 671 + <div> 672 + <div class="stat-value loading" id="stat-storage">—</div> 673 + <div class="stat-label">Storage Used</div> 674 + </div> 675 + </div> 676 + </div> 677 + </section> 678 + 679 + <!-- Tabs --> 680 + <section class="tabs-section"> 681 + <div class="tab-bar"> 682 + <button class="tab-btn active" onclick="switchTab('upload', this)"> 683 + <i class="fa-solid fa-cloud-arrow-up"></i> Upload 684 + </button> 685 + <button class="tab-btn" onclick="switchTab('about', this)"> 686 + <i class="fa-solid fa-circle-info"></i> About 687 + </button> 688 + <button class="tab-btn" onclick="switchTab('how', this)"> 689 + <i class="fa-solid fa-gears"></i> How it Works 690 + </button> 691 + <button class="tab-btn" onclick="switchTab('limits', this)"> 692 + <i class="fa-solid fa-gauge-high"></i> Limits 693 + </button> 694 + </div> 695 + 696 + <!-- Upload --> 697 + <div class="tab-pane active" id="tab-upload"> 698 + <label class="drop-zone" id="drop-zone" for="file-input"> 699 + <i class="fa-solid fa-cloud-arrow-up" id="drop-icon"></i> 700 + <span id="file-name">Click to select or drag a file here</span> 701 + <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"> 702 + </label> 703 + 704 + <div class="file-info" id="file-info"> 705 + <strong id="detail-name"></strong> 706 + <span style="color:var(--text-dim)"> · </span><span id="detail-size"></span> 707 + <span style="color:var(--text-dim)"> · </span><span id="detail-type"></span> 708 + </div> 709 + 710 + <div class="progress-wrap" id="progress-wrap"> 711 + <div class="progress-track"> 712 + <div class="progress-fill" id="progress-fill"></div> 713 + </div> 714 + <div class="progress-label" id="progress-label">0%</div> 715 + </div> 716 + 717 + <button class="upload-btn" id="upload-btn"> 718 + <i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN 719 + </button> 720 + 721 + <div class="status-msg" id="status"></div> 722 + 723 + <div class="result-box" id="result-box"> 724 + <div class="result-label">✓ Your file is live</div> 725 + <div class="result-url" id="result-url"></div> 726 + <div class="result-actions"> 727 + <button class="copy-btn" id="copy-btn" onclick="copyUrl()"> 728 + <i class="fa-solid fa-copy"></i> Copy URL 729 + </button> 730 + <a class="open-btn" id="open-link" href="#" target="_blank" rel="noopener"> 731 + <i class="fa-solid fa-arrow-up-right-from-square"></i> Open 732 + </a> 733 + </div> 734 + </div> 735 + </div> 736 + 737 + <!-- About --> 738 + <div class="tab-pane" id="tab-about"> 739 + <div class="prose"> 740 + <p>MBD CDN is a content delivery network built by <a href="https://madebydanny.uk">madebydanny.uk</a> 741 + to host and serve media files — images, GIFs, videos, and documents — at fast speeds with global availability.</p> 742 + <p>Files uploaded here are stored in <strong style="color:var(--text)">Cloudflare R2</strong> object storage 743 + and served from Cloudflare's global edge network, which spans over 310 cities worldwide. Your media is delivered 744 + from a server close to whoever is viewing it, minimising latency.</p> 745 + <p>The platform is designed to be simple and permanent. Files are stored indefinitely once uploaded 746 + and immediately available via a public URL — no sign-up, no expiry, no catch.</p> 747 + <p>The underlying stack is a <strong style="color:var(--text)">Cloudflare Worker</strong> with R2 for storage and 748 + D1 for metadata. The whole thing runs at the edge with no cold starts.</p> 749 + </div> 750 + </div> 751 + 752 + <!-- How it Works --> 753 + <div class="tab-pane" id="tab-how"> 754 + <div class="steps"> 755 + <div class="step"> 756 + <div class="step-num">1</div> 757 + <div class="step-body"> 758 + <h3>You select a file</h3> 759 + <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> 760 + </div> 761 + </div> 762 + <div class="step"> 763 + <div class="step-num">2</div> 764 + <div class="step-body"> 765 + <h3>A Worker receives it</h3> 766 + <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> 767 + </div> 768 + </div> 769 + <div class="step"> 770 + <div class="step-num">3</div> 771 + <div class="step-body"> 772 + <h3>R2 stores it permanently</h3> 773 + <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> 774 + </div> 775 + </div> 776 + <div class="step"> 777 + <div class="step-num">4</div> 778 + <div class="step-body"> 779 + <h3>You get a public URL</h3> 780 + <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> 781 + </div> 782 + </div> 783 + </div> 784 + 785 + <div class="how-grid"> 786 + <div class="how-card"> 787 + <i class="fa-brands fa-cloudflare"></i> 788 + <h3>310+ Edge Locations</h3> 789 + <p>Files cached and served globally — sub-50ms for most users.</p> 790 + </div> 791 + <div class="how-card"> 792 + <i class="fa-solid fa-database"></i> 793 + <h3>R2 Object Storage</h3> 794 + <p>Zero egress fees, no expiry. Enterprise-grade durability.</p> 795 + </div> 796 + <div class="how-card"> 797 + <i class="fa-solid fa-bolt"></i> 798 + <h3>Zero Cold Starts</h3> 799 + <p>Workers run at the edge — every request is handled immediately.</p> 800 + </div> 801 + </div> 802 + </div> 803 + 804 + <!-- Limits --> 805 + <div class="tab-pane" id="tab-limits"> 806 + <div class="limits-grid"> 807 + <div class="limit-card"> 808 + <i class="fa-solid fa-file-arrow-up"></i> 809 + <div class="limit-value" id="limit-max-file">—</div> 810 + <div class="limit-label">Max File Size</div> 811 + <div class="limit-note">Per individual upload</div> 812 + </div> 813 + <div class="limit-card"> 814 + <i class="fa-solid fa-hard-drive"></i> 815 + <div class="limit-value" id="limit-max-bytes">—</div> 816 + <div class="limit-label">Daily Storage</div> 817 + <div class="limit-note">Total uploads per day</div> 818 + </div> 819 + <div class="limit-card"> 820 + <i class="fa-solid fa-arrow-up-from-bracket"></i> 821 + <div class="limit-value" id="limit-max-files">—</div> 822 + <div class="limit-label">Uploads Per Day</div> 823 + <div class="limit-note">Resets at midnight UTC</div> 824 + </div> 825 + </div> 826 + 827 + <div class="limits-note"> 828 + All limits reset daily at <strong>midnight UTC</strong> and are enforced per IP to protect performance for all users. 829 + Need higher limits? Consider self-hosting the stack, or get in touch via Bluesky. 830 + </div> 831 + 832 + <div class="usage-section"> 833 + <p class="section-label">your usage today</p> 834 + <div id="usage-loading" class="usage-loading">Loading your usage…</div> 835 + <div id="usage-bars" style="display:none"> 836 + <div class="usage-item"> 837 + <div class="usage-row"> 838 + <span>Files Uploaded</span> 839 + <span id="usage-files-label">0 / 30</span> 840 + </div> 841 + <div class="usage-track"> 842 + <div class="usage-fill" id="usage-files-fill" style="width:0%"></div> 843 + </div> 844 + </div> 845 + <div class="usage-item"> 846 + <div class="usage-row"> 847 + <span>Storage Used</span> 848 + <span id="usage-bytes-label">0 B / 1 GB</span> 849 + </div> 850 + <div class="usage-track"> 851 + <div class="usage-fill" id="usage-bytes-fill" style="width:0%"></div> 852 + </div> 853 + </div> 854 + </div> 855 + </div> 856 + 857 + <hr class="divider"> 858 + 859 + <p class="section-label">accepted file types</p> 860 + <p class="file-types"> 861 + <strong>Images</strong> — JPEG, PNG, WebP, AVIF, SVG &nbsp;·&nbsp; 862 + <strong>Animated</strong> — GIF &nbsp;·&nbsp; 863 + <strong>Video</strong> — MP4, WebM, MOV &nbsp;·&nbsp; 864 + <strong>Documents</strong> — PDF, JSON, HTML, CSS, JS, CSV, Markdown 865 + </p> 866 + </div> 867 + 868 + </section> 869 + </main> 870 + 871 + <footer class="site-footer"> 872 + <p>© 2024–26 Daniel Morrisey · <a href="https://madebydanny.uk">madebydanny.uk</a></p> 873 + </footer> 874 + 875 + <script> 876 + const API = 'https://cdn.madebydanny.uk'; 877 + 878 + // ── Utilities ────────────────────────────────────────────────────────── 879 + 880 + function formatBytes(b) { 881 + if (!b) return '0 B'; 882 + const k = 1024, s = ['B','KB','MB','GB','TB']; 883 + const i = Math.floor(Math.log(b) / Math.log(k)); 884 + return (b / Math.pow(k, i)).toFixed(1).replace(/\.0$/, '') + ' ' + s[i]; 885 + } 886 + 887 + function fmt(n) { return Number(n).toLocaleString(); } 888 + 889 + // ── Tabs ─────────────────────────────────────────────────────────────── 890 + 891 + function switchTab(name, btn) { 892 + document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); 893 + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); 894 + document.getElementById('tab-' + name).classList.add('active'); 895 + if (btn) btn.classList.add('active'); 896 + if (name === 'limits') loadLimits(); 897 + } 898 + 899 + // ── Stats ────────────────────────────────────────────────────────────── 900 + 901 + async function loadStats() { 902 + try { 903 + const r = await fetch(`${API}/stats`); 904 + const d = await r.json(); 905 + if (!d.success) throw new Error(); 906 + const cat = d.stats.byCategory || {}; 907 + document.getElementById('stat-images').textContent = fmt(cat.image || 0); 908 + document.getElementById('stat-videos').textContent = fmt(cat.video || 0); 909 + document.getElementById('stat-gifs').textContent = fmt(cat.gif || 0); 910 + document.getElementById('stat-documents').textContent = fmt(cat.document || 0); 911 + document.getElementById('stat-storage').textContent = formatBytes(d.stats.totalSize); 912 + document.querySelectorAll('.stat-value').forEach(v => v.classList.remove('loading')); 913 + } catch { 914 + document.querySelectorAll('.stat-value').forEach(v => { 915 + v.textContent = '—'; 916 + v.classList.remove('loading'); 917 + }); 918 + } 919 + } 920 + 921 + // ── Limits ───────────────────────────────────────────────────────────── 922 + 923 + async function loadLimits() { 924 + try { 925 + const r = await fetch(`${API}/limits`); 926 + const d = await r.json(); 927 + if (!d.success) throw new Error(); 928 + const { file_count, total_size, max_files, max_bytes, max_file } = d.limits; 929 + 930 + document.getElementById('limit-max-file').textContent = formatBytes(max_file); 931 + document.getElementById('limit-max-bytes').textContent = formatBytes(max_bytes); 932 + document.getElementById('limit-max-files').textContent = max_files; 933 + 934 + const filePct = Math.min((file_count / max_files) * 100, 100); 935 + const bytesPct = Math.min((total_size / max_bytes) * 100, 100); 936 + 937 + const filesFill = document.getElementById('usage-files-fill'); 938 + const bytesFill = document.getElementById('usage-bytes-fill'); 939 + 940 + filesFill.style.width = filePct + '%'; 941 + bytesFill.style.width = bytesPct + '%'; 942 + 943 + function fillClass(pct) { 944 + return pct >= 90 ? 'usage-fill danger' : pct >= 70 ? 'usage-fill warn' : 'usage-fill'; 945 + } 946 + 947 + filesFill.className = fillClass(filePct); 948 + bytesFill.className = fillClass(bytesPct); 949 + 950 + document.getElementById('usage-files-label').textContent = 951 + `${fmt(file_count)} / ${fmt(max_files)}`; 952 + document.getElementById('usage-bytes-label').textContent = 953 + `${formatBytes(total_size)} / ${formatBytes(max_bytes)}`; 954 + 955 + document.getElementById('usage-loading').style.display = 'none'; 956 + document.getElementById('usage-bars').style.display = 'block'; 957 + } catch { 958 + document.getElementById('usage-loading').textContent = 'Could not load usage data.'; 959 + } 960 + } 961 + 962 + // ── File icon helper ─────────────────────────────────────────────────── 963 + 964 + function getFileIcon(type) { 965 + if (!type) return 'fa-solid fa-cloud-arrow-up'; 966 + if (type.startsWith('video/')) return 'fa-solid fa-film'; 967 + if (type === 'image/gif') return 'fa-solid fa-photo-film'; 968 + if (type.startsWith('image/')) return 'fa-regular fa-image'; 969 + if (type === 'application/pdf') return 'fa-solid fa-file-pdf'; 970 + if (type === 'text/csv') return 'fa-solid fa-file-csv'; 971 + if (type === 'text/plain') return 'fa-solid fa-file-lines'; 972 + return 'fa-solid fa-file-code'; 973 + } 974 + 975 + // ── File select / drag-drop ──────────────────────────────────────────── 976 + 977 + const fileInput = document.getElementById('file-input'); 978 + const dropZone = document.getElementById('drop-zone'); 979 + const fileInfo = document.getElementById('file-info'); 980 + 981 + function showFile(file) { 982 + document.getElementById('drop-icon').className = getFileIcon(file.type); 983 + document.getElementById('file-name').textContent = file.name; 984 + document.getElementById('detail-name').textContent = file.name; 985 + document.getElementById('detail-size').textContent = formatBytes(file.size); 986 + document.getElementById('detail-type').textContent = file.type || 'unknown'; 987 + fileInfo.classList.add('show'); 988 + } 989 + 990 + fileInput.addEventListener('change', () => { if (fileInput.files[0]) showFile(fileInput.files[0]); }); 991 + 992 + dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); }); 993 + dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); }); 994 + dropZone.addEventListener('drop', e => { 995 + e.preventDefault(); 996 + dropZone.classList.remove('drag-over'); 997 + const f = e.dataTransfer.files[0]; 998 + if (f) { fileInput.files = e.dataTransfer.files; showFile(f); } 999 + }); 1000 + 1001 + // ── Upload ───────────────────────────────────────────────────────────── 1002 + 1003 + const uploadBtn = document.getElementById('upload-btn'); 1004 + const progressWrap = document.getElementById('progress-wrap'); 1005 + const progressFill = document.getElementById('progress-fill'); 1006 + const progressLabel = document.getElementById('progress-label'); 1007 + const statusEl = document.getElementById('status'); 1008 + const resultBox = document.getElementById('result-box'); 1009 + const resultUrl = document.getElementById('result-url'); 1010 + 1011 + function setStatus(msg, color) { 1012 + statusEl.textContent = msg; 1013 + statusEl.style.color = color || 'var(--accent)'; 1014 + } 1015 + 1016 + function setProgress(pct) { 1017 + progressFill.style.width = pct + '%'; 1018 + progressLabel.textContent = Math.round(pct) + '%'; 1019 + } 1020 + 1021 + function resetUploadUI(delay = 0) { 1022 + setTimeout(() => { 1023 + fileInput.value = ''; 1024 + document.getElementById('drop-icon').className = 'fa-solid fa-cloud-arrow-up'; 1025 + document.getElementById('file-name').textContent = 'Click to select or drag a file here'; 1026 + fileInfo.classList.remove('show'); 1027 + progressWrap.classList.remove('show'); 1028 + setProgress(0); 1029 + }, delay); 1030 + } 1031 + 1032 + uploadBtn.addEventListener('click', () => { 1033 + if (!fileInput.files[0]) { 1034 + setStatus('Please select a file first.', 'var(--red)'); 1035 + return; 1036 + } 1037 + 1038 + const file = fileInput.files[0]; 1039 + uploadBtn.disabled = true; 1040 + uploadBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Uploading…'; 1041 + setStatus(''); 1042 + resultBox.classList.remove('show'); 1043 + progressWrap.classList.add('show'); 1044 + setProgress(0); 1045 + 1046 + const xhr = new XMLHttpRequest(); 1047 + 1048 + xhr.upload.addEventListener('progress', e => { 1049 + if (e.lengthComputable) setProgress((e.loaded / e.total) * 100); 1050 + }); 1051 + 1052 + xhr.addEventListener('load', () => { 1053 + setProgress(100); 1054 + try { 1055 + const data = JSON.parse(xhr.responseText); 1056 + if (data.success) { 1057 + resultUrl.textContent = data.url; 1058 + document.getElementById('open-link').href = data.url; 1059 + resultBox.classList.add('show'); 1060 + setStatus(''); 1061 + setTimeout(() => loadStats(), 600); 1062 + resetUploadUI(3000); 1063 + } else { 1064 + throw new Error(data.error || 'Upload failed'); 1065 + } 1066 + } catch (err) { 1067 + progressWrap.classList.remove('show'); 1068 + setStatus('Error: ' + err.message, 'var(--red)'); 1069 + } 1070 + uploadBtn.disabled = false; 1071 + uploadBtn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN'; 1072 + }); 1073 + 1074 + xhr.addEventListener('error', () => { 1075 + progressWrap.classList.remove('show'); 1076 + setStatus('Network error. Please try again.', 'var(--red)'); 1077 + uploadBtn.disabled = false; 1078 + uploadBtn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN'; 1079 + }); 1080 + 1081 + xhr.open('POST', API); 1082 + xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream'); 1083 + xhr.send(file); 1084 + }); 1085 + 1086 + // ── Copy URL ─────────────────────────────────────────────────────────── 1087 + 1088 + function copyUrl() { 1089 + navigator.clipboard.writeText(resultUrl.textContent); 1090 + const btn = document.getElementById('copy-btn'); 1091 + btn.classList.add('copied'); 1092 + btn.innerHTML = '<i class="fa-solid fa-check"></i> Copied'; 1093 + setTimeout(() => { 1094 + btn.classList.remove('copied'); 1095 + btn.innerHTML = '<i class="fa-solid fa-copy"></i> Copy URL'; 1096 + }, 2000); 1097 + } 1098 + 1099 + // ── Init ─────────────────────────────────────────────────────────────── 1100 + 1101 + loadStats(); 1102 + </script> 1103 + 1104 + </body> 1105 + </html>
-134
cdn/about.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>About 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="About the MBD CDN network and what it is built on."> 10 - <meta property="og:title" content="About MBD CDN - madebydanny.uk"> 11 - <meta property="og:description" content="About the MBD CDN network and what it is built on."> 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 - <style> 15 - :root { 16 - --bg: #121212; 17 - --card-bg: #4a2b32; 18 - --text: #fff; 19 - --subtext: #dcbaba; 20 - --link: #ffcccc; 21 - --border: rgba(255,255,255,0.1); 22 - } 23 - 24 - * { box-sizing: border-box; margin: 0; padding: 0; } 25 - body { 26 - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 27 - background: var(--bg); 28 - color: var(--text); 29 - line-height: 1.5; 30 - min-height: 100vh; 31 - } 32 - 33 - a { color: var(--link); text-decoration: none; } 34 - a:hover { text-decoration: underline; } 35 - 36 - .site-header { 37 - text-align: center; 38 - padding: 44px 20px 10px; 39 - max-width: 900px; 40 - margin: 0 auto; 41 - } 42 - 43 - .site-header h1 { font-size: 2rem; margin-bottom: 8px; } 44 - .site-header p { color: var(--subtext); font-size: 0.95rem; } 45 - 46 - .tabs-wrap { max-width: 900px; margin: 20px auto 0; padding: 0 20px; } 47 - .tab-bar { 48 - display: flex; 49 - gap: 4px; 50 - border-bottom: 1px solid var(--border); 51 - overflow-x: auto; 52 - scrollbar-width: none; 53 - } 54 - .tab-bar::-webkit-scrollbar { display: none; } 55 - .tab-btn { 56 - font-size: 0.9rem; 57 - font-weight: 600; 58 - color: var(--subtext); 59 - border-bottom: 2px solid transparent; 60 - padding: 10px 18px; 61 - white-space: nowrap; 62 - margin-bottom: -1px; 63 - display: inline-block; 64 - } 65 - .tab-btn.active { color: var(--link); border-bottom-color: var(--link); } 66 - 67 - .card { 68 - background: var(--card-bg); 69 - border: 1px solid var(--border); 70 - border-radius: 16px; 71 - padding: 32px; 72 - box-shadow: 0 6px 12px rgba(0,0,0,0.35); 73 - margin-top: 26px; 74 - } 75 - 76 - .card h2 { font-size: 1.25rem; margin-bottom: 12px; } 77 - .about-text p { color: var(--subtext); font-size: 0.95rem; } 78 - .about-text p + p { margin-top: 12px; } 79 - 80 - hr.divider { border: none; border-top: 1px solid var(--border); margin: 28px 0; } 81 - 82 - .social-row { 83 - display: flex; 84 - gap: 10px; 85 - flex-wrap: wrap; 86 - justify-content: center; 87 - } 88 - 89 - .site-footer { 90 - max-width: 900px; 91 - margin: 26px auto 40px; 92 - padding: 0 20px; 93 - text-align: center; 94 - color: var(--subtext); 95 - font-size: 0.85rem; 96 - } 97 - </style> 98 - </head> 99 - <body> 100 - <header class="site-header"> 101 - <h1><i class="fa-solid fa-database" style="color:#fff"></i> MBD CDN</h1> 102 - <p>A simple easy to use CDN, free for life</p> 103 - </header> 104 - 105 - <div class="tabs-wrap"> 106 - <div class="tab-bar"> 107 - <a class="tab-btn" href="/cdn/index.html"><i class="fa-solid fa-cloud-arrow-up"></i> Upload</a> 108 - <a class="tab-btn active" href="/cdn/about.html"><i class="fa-solid fa-circle-info"></i> About</a> 109 - <a class="tab-btn" href="/cdn/how-it-works.html"><i class="fa-solid fa-gears"></i> How it Works</a> 110 - <a class="tab-btn" href="/cdn/limits.html"><i class="fa-solid fa-gauge-high"></i> Limits</a> 111 - </div> 112 - 113 - <div class="card"> 114 - <h2>About MBD CDN</h2> 115 - <div class="about-text"> 116 - <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> 117 - <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> 118 - <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> 119 - </div> 120 - 121 - <hr class="divider"> 122 - 123 - <h2 style="margin-bottom:16px;">Social Links</h2> 124 - <div id="social-links" class="social-row"></div> 125 - </div> 126 - </div> 127 - 128 - <footer class="site-footer"> 129 - &copy; <script>document.write(new Date().getFullYear())</script> <a href="https://madebydanny.uk" target="_blank">madebydanny.uk</a> 130 - </footer> 131 - 132 - <script src="/js/social-links.js"></script> 133 - </body> 134 - </html>
-210
cdn/how-it-works.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>How MBD CDN Works - 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="How uploads and delivery work in the MBD CDN."> 10 - <meta property="og:title" content="How MBD CDN Works - madebydanny.uk"> 11 - <meta property="og:description" content="How uploads and delivery work in the MBD CDN."> 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 - <style> 15 - :root { 16 - --bg: #121212; 17 - --card-bg: #4a2b32; 18 - --stat-bg: #2a1a21; 19 - --text: #fff; 20 - --subtext: #dcbaba; 21 - --link: #ffcccc; 22 - --border: rgba(255,255,255,0.1); 23 - } 24 - 25 - * { box-sizing: border-box; margin: 0; padding: 0; } 26 - body { 27 - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 28 - background: var(--bg); 29 - color: var(--text); 30 - line-height: 1.5; 31 - min-height: 100vh; 32 - } 33 - 34 - a { color: var(--link); text-decoration: none; } 35 - a:hover { text-decoration: underline; } 36 - 37 - .site-header { 38 - text-align: center; 39 - padding: 44px 20px 10px; 40 - max-width: 900px; 41 - margin: 0 auto; 42 - } 43 - 44 - .site-header h1 { font-size: 2rem; margin-bottom: 8px; } 45 - .site-header p { color: var(--subtext); font-size: 0.95rem; } 46 - 47 - .tabs-wrap { max-width: 900px; margin: 20px auto 0; padding: 0 20px; } 48 - .tab-bar { 49 - display: flex; 50 - gap: 4px; 51 - border-bottom: 1px solid var(--border); 52 - overflow-x: auto; 53 - scrollbar-width: none; 54 - } 55 - .tab-bar::-webkit-scrollbar { display: none; } 56 - .tab-btn { 57 - font-size: 0.9rem; 58 - font-weight: 600; 59 - color: var(--subtext); 60 - border-bottom: 2px solid transparent; 61 - padding: 10px 18px; 62 - white-space: nowrap; 63 - margin-bottom: -1px; 64 - display: inline-block; 65 - } 66 - .tab-btn.active { color: var(--link); border-bottom-color: var(--link); } 67 - 68 - .card { 69 - background: var(--card-bg); 70 - border: 1px solid var(--border); 71 - border-radius: 16px; 72 - padding: 32px; 73 - box-shadow: 0 6px 12px rgba(0,0,0,0.35); 74 - margin-top: 26px; 75 - } 76 - 77 - .card h2 { font-size: 1.25rem; margin-bottom: 8px; } 78 - .desc { color: var(--subtext); font-size: 0.9rem; margin-bottom: 20px; } 79 - 80 - .steps { display: grid; gap: 12px; margin-bottom: 22px; } 81 - .step { 82 - display: grid; 83 - grid-template-columns: 34px 1fr; 84 - gap: 12px; 85 - background: rgba(0,0,0,0.18); 86 - border: 1px solid var(--border); 87 - border-radius: 12px; 88 - padding: 12px; 89 - } 90 - .step-num { 91 - width: 34px; 92 - height: 34px; 93 - border-radius: 50%; 94 - background: #623640; 95 - border: 1px solid var(--border); 96 - display: grid; 97 - place-items: center; 98 - font-weight: 700; 99 - color: var(--link); 100 - } 101 - .step-body h3 { font-size: 1rem; margin-bottom: 4px; } 102 - .step-body p { color: var(--subtext); font-size: 0.9rem; } 103 - 104 - .how-grid { 105 - display: grid; 106 - grid-template-columns: repeat(3, minmax(0, 1fr)); 107 - gap: 12px; 108 - margin-top: 18px; 109 - } 110 - 111 - .how-card { 112 - background: linear-gradient(135deg, var(--stat-bg) 0%, #3f242b 100%); 113 - border: 1px solid var(--border); 114 - border-radius: 12px; 115 - padding: 16px; 116 - } 117 - 118 - .how-card i { color: var(--link); font-size: 1.2rem; margin-bottom: 8px; } 119 - .how-card h3 { font-size: 0.95rem; margin-bottom: 4px; } 120 - .how-card p { color: var(--subtext); font-size: 0.83rem; } 121 - 122 - .site-footer { 123 - max-width: 900px; 124 - margin: 26px auto 40px; 125 - padding: 0 20px; 126 - text-align: center; 127 - color: var(--subtext); 128 - font-size: 0.85rem; 129 - } 130 - 131 - @media (max-width: 780px) { 132 - .how-grid { grid-template-columns: 1fr; } 133 - .card { padding: 24px; } 134 - } 135 - </style> 136 - </head> 137 - <body> 138 - <header class="site-header"> 139 - <h1><i class="fa-solid fa-database" style="color:#fff"></i> MBD CDN</h1> 140 - <p>A simple easy to use CDN, free for life</p> 141 - </header> 142 - 143 - <div class="tabs-wrap"> 144 - <div class="tab-bar"> 145 - <a class="tab-btn" href="/cdn/index.html"><i class="fa-solid fa-cloud-arrow-up"></i> Upload</a> 146 - <a class="tab-btn" href="/cdn/about.html"><i class="fa-solid fa-circle-info"></i> About</a> 147 - <a class="tab-btn active" href="/cdn/how-it-works.html"><i class="fa-solid fa-gears"></i> How it Works</a> 148 - <a class="tab-btn" href="/cdn/limits.html"><i class="fa-solid fa-gauge-high"></i> Limits</a> 149 - </div> 150 - 151 - <div class="card"> 152 - <h2>How it Works</h2> 153 - <p class="desc">From the moment you click Upload to the moment someone loads your file - here's what happens.</p> 154 - 155 - <div class="steps"> 156 - <div class="step"> 157 - <div class="step-num">1</div> 158 - <div class="step-body"> 159 - <h3>You select a file</h3> 160 - <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> 161 - </div> 162 - </div> 163 - <div class="step"> 164 - <div class="step-num">2</div> 165 - <div class="step-body"> 166 - <h3>The Worker receives it</h3> 167 - <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> 168 - </div> 169 - </div> 170 - <div class="step"> 171 - <div class="step-num">3</div> 172 - <div class="step-body"> 173 - <h3>R2 stores it permanently</h3> 174 - <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> 175 - </div> 176 - </div> 177 - <div class="step"> 178 - <div class="step-num">4</div> 179 - <div class="step-body"> 180 - <h3>You get a public URL</h3> 181 - <p>A permanent <code style="font-size:0.8rem;color:var(--link)">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> 182 - </div> 183 - </div> 184 - </div> 185 - 186 - <div class="how-grid"> 187 - <div class="how-card"> 188 - <i class="fa-brands fa-cloudflare"></i> 189 - <h3>310+ Edge Locations</h3> 190 - <p>Files are cached and served globally - sub-50ms for most users regardless of where they are.</p> 191 - </div> 192 - <div class="how-card"> 193 - <i class="fa-solid fa-database"></i> 194 - <h3>R2 Object Storage</h3> 195 - <p>Zero egress fees, no expiry. Files are stored in Cloudflare R2 with enterprise-grade durability.</p> 196 - </div> 197 - <div class="how-card"> 198 - <i class="fa-solid fa-bolt"></i> 199 - <h3>Zero Cold Starts</h3> 200 - <p>Workers run at the edge with no spin-up delay - every upload and file request is handled immediately.</p> 201 - </div> 202 - </div> 203 - </div> 204 - </div> 205 - 206 - <footer class="site-footer"> 207 - &copy; <script>document.write(new Date().getFullYear())</script> <a href="https://madebydanny.uk" target="_blank">madebydanny.uk</a> 208 - </footer> 209 - </body> 210 - </html>
-964
cdn/index.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(4, 1fr); 74 - gap: 14px; 75 - } 76 - 77 - .stats-grid-storage { 78 - display: grid; 79 - grid-template-columns: 1fr; 80 - gap: 14px; 81 - margin-top: 14px; 82 - } 83 - 84 - .stat-card.storage-card { 85 - display: flex; 86 - align-items: center; 87 - justify-content: center; 88 - gap: 16px; 89 - padding: 18px 28px; 90 - text-align: left; 91 - } 92 - 93 - .stat-card.storage-card .stat-icon { 94 - font-size: 2rem; 95 - margin-bottom: 0; 96 - flex-shrink: 0; 97 - } 98 - 99 - .stat-card.storage-card .stat-value { 100 - font-size: 2rem; 101 - margin: 0; 102 - } 103 - 104 - .stat-card.storage-card .stat-label { 105 - margin-top: 2px; 106 - } 107 - 108 - .stat-card { 109 - background: linear-gradient(135deg, var(--stat-bg) 0%, var(--card-bg) 100%); 110 - border: 1px solid var(--border); 111 - border-radius: 12px; 112 - padding: 20px; 113 - text-align: center; 114 - position: relative; 115 - overflow: hidden; 116 - transition: transform 0.2s, box-shadow 0.2s; 117 - } 118 - 119 - .stat-card::before { 120 - content: ''; 121 - position: absolute; 122 - top: 0; left: 0; right: 0; 123 - height: 3px; 124 - background: linear-gradient(90deg, var(--link), rgba(255,204,204,0.25)); 125 - opacity: 0; 126 - transition: opacity 0.3s; 127 - } 128 - 129 - .stat-card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0,0,0,0.4); } 130 - .stat-card:hover::before { opacity: 1; } 131 - 132 - .stat-icon { font-size: 1.8rem; margin-bottom: 8px; opacity: 0.75; } 133 - 134 - .stat-value { 135 - font-size: 2rem; 136 - font-weight: 700; 137 - letter-spacing: -1px; 138 - margin: 4px 0; 139 - } 140 - 141 - .stat-value.loading { opacity: 0.35; animation: blink 1.4s ease-in-out infinite; } 142 - @keyframes blink { 0%,100%{opacity:.35} 50%{opacity:.7} } 143 - 144 - .stat-label { 145 - font-size: 0.8rem; 146 - color: var(--subtext); 147 - text-transform: uppercase; 148 - letter-spacing: 0.5px; 149 - font-weight: 600; 150 - } 151 - 152 - /* ── TABS ───────────────────────────────── */ 153 - .tabs-wrap { 154 - max-width: 900px; 155 - margin: 36px auto 0; 156 - padding: 0 20px; 157 - } 158 - 159 - .tab-bar { 160 - display: flex; 161 - gap: 4px; 162 - border-bottom: 1px solid var(--border); 163 - overflow-x: auto; 164 - scrollbar-width: none; 165 - } 166 - 167 - .tab-bar::-webkit-scrollbar { display: none; } 168 - 169 - .tab-btn { 170 - font-family: inherit; 171 - font-size: 0.9rem; 172 - font-weight: 600; 173 - color: var(--subtext); 174 - background: none; 175 - border: none; 176 - border-bottom: 2px solid transparent; 177 - padding: 10px 18px; 178 - cursor: pointer; 179 - white-space: nowrap; 180 - transition: color 0.2s, border-color 0.2s; 181 - margin-bottom: -1px; 182 - } 183 - 184 - .tab-btn:hover { color: var(--text); } 185 - .tab-btn.active { color: var(--link); border-bottom-color: var(--link); } 186 - 187 - .tab-content { padding: 32px 0 80px; } 188 - 189 - .tab-pane { display: none; } 190 - .tab-pane.active { display: block; } 191 - 192 - /* ── CARD ───────────────────────────────── */ 193 - .card { 194 - background: var(--card-bg); 195 - border: 1px solid var(--border); 196 - border-radius: 16px; 197 - padding: 32px; 198 - box-shadow: 0 6px 12px rgba(0,0,0,0.35); 199 - } 200 - 201 - .card + .card { margin-top: 20px; } 202 - 203 - .card h2 { 204 - font-size: 1.25rem; 205 - font-weight: 700; 206 - letter-spacing: -0.4px; 207 - margin-bottom: 8px; 208 - } 209 - 210 - .card .desc { 211 - color: var(--subtext); 212 - font-size: 0.9rem; 213 - margin-bottom: 20px; 214 - } 215 - 216 - hr.divider { 217 - border: none; 218 - border-top: 1px solid var(--border); 219 - margin: 28px 0; 220 - } 221 - 222 - /* ── UPLOAD ─────────────────────────────── */ 223 - .drop-zone { 224 - display: block; 225 - border: 2px dashed var(--border); 226 - border-radius: 12px; 227 - padding: 50px 20px; 228 - text-align: center; 229 - cursor: pointer; 230 - background: rgba(0,0,0,0.2); 231 - color: var(--subtext); 232 - transition: all 0.25s; 233 - } 234 - 235 - .drop-zone:hover { 236 - border-color: var(--link); 237 - background: rgba(255,255,255,0.02); 238 - transform: scale(1.005); 239 - } 240 - 241 - .drop-zone.drag-over { 242 - border-color: var(--green); 243 - background: rgba(52,199,89,0.05); 244 - transform: scale(1.01); 245 - } 246 - 247 - .drop-zone i { font-size: 2.4rem; display: block; margin-bottom: 12px; opacity: 0.65; } 248 - .drop-zone input { display: none; } 249 - 250 - #file-name { font-size: 0.95rem; font-weight: 500; display: block; margin-top: 4px; } 251 - 252 - .file-info { 253 - display: none; 254 - margin-top: 14px; 255 - padding: 12px 16px; 256 - background: var(--post-bg); 257 - border: 1px solid var(--border); 258 - border-radius: 8px; 259 - font-size: 0.82rem; 260 - color: var(--subtext); 261 - } 262 - 263 - .file-info.show { display: block; } 264 - 265 - .progress-wrap { display: none; margin-top: 14px; } 266 - .progress-wrap.show { display: block; } 267 - 268 - .progress-track { 269 - height: 7px; 270 - background: var(--post-bg); 271 - border-radius: 999px; 272 - overflow: hidden; 273 - border: 1px solid var(--border); 274 - } 275 - 276 - .progress-fill { 277 - height: 100%; 278 - width: 0%; 279 - background: linear-gradient(90deg, var(--link), var(--green)); 280 - border-radius: 999px; 281 - transition: width 0.2s ease; 282 - } 283 - 284 - .progress-label { 285 - text-align: right; 286 - font-size: 0.75rem; 287 - color: var(--subtext); 288 - margin-top: 5px; 289 - } 290 - 291 - .upload-btn { 292 - display: block; 293 - width: 100%; 294 - margin-top: 18px; 295 - padding: 14px 24px; 296 - background: linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%); 297 - color: #000; 298 - border: none; 299 - border-radius: 999px; 300 - font-family: inherit; 301 - font-size: 1rem; 302 - font-weight: 600; 303 - cursor: pointer; 304 - box-shadow: 0 2px 8px rgba(255,255,255,0.08); 305 - transition: all 0.2s; 306 - } 307 - 308 - .upload-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 14px rgba(255,255,255,0.18); } 309 - .upload-btn:active { transform: scale(0.985); } 310 - .upload-btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; box-shadow: none; } 311 - 312 - .status-msg { 313 - text-align: center; 314 - margin-top: 14px; 315 - font-size: 0.9rem; 316 - font-weight: 500; 317 - min-height: 1.3em; 318 - color: var(--link); 319 - } 320 - 321 - .result-box { 322 - display: none; 323 - margin-top: 18px; 324 - padding: 18px; 325 - background: var(--post-bg); 326 - border: 1px solid var(--green); 327 - border-radius: 12px; 328 - animation: slideIn 0.3s ease; 329 - } 330 - 331 - .result-box.show { display: block; } 332 - 333 - @keyframes slideIn { 334 - from { opacity: 0; transform: translateY(-8px); } 335 - to { opacity: 1; transform: translateY(0); } 336 - } 337 - 338 - .result-label { 339 - font-size: 0.72rem; 340 - font-weight: 700; 341 - text-transform: uppercase; 342 - letter-spacing: 0.6px; 343 - color: var(--subtext); 344 - margin-bottom: 8px; 345 - } 346 - 347 - .result-url { 348 - font-family: 'Monaco', 'Courier New', monospace; 349 - font-size: 0.85rem; 350 - padding: 10px 12px; 351 - background: rgba(0,0,0,0.3); 352 - border: 1px solid var(--border); 353 - border-radius: 6px; 354 - word-break: break-all; 355 - color: var(--text); 356 - margin-bottom: 12px; 357 - } 358 - 359 - .copy-btn { 360 - display: inline-flex; 361 - align-items: center; 362 - gap: 7px; 363 - padding: 9px 16px; 364 - background: rgba(255,255,255,0.05); 365 - border: 1px solid var(--border); 366 - border-radius: 8px; 367 - color: var(--link); 368 - font-family: inherit; 369 - font-size: 0.85rem; 370 - font-weight: 600; 371 - cursor: pointer; 372 - transition: all 0.15s; 373 - margin-right: 8px; 374 - } 375 - 376 - .copy-btn:hover { background: rgba(255,255,255,0.09); } 377 - .copy-btn.copied { background: var(--green); color: #000; border-color: var(--green); } 378 - 379 - .open-btn { 380 - display: inline-flex; 381 - align-items: center; 382 - gap: 7px; 383 - padding: 9px 16px; 384 - background: rgba(255,255,255,0.05); 385 - border: 1px solid var(--border); 386 - border-radius: 8px; 387 - color: var(--link); 388 - font-size: 0.85rem; 389 - font-weight: 600; 390 - transition: all 0.15s; 391 - } 392 - 393 - .open-btn:hover { background: rgba(255,255,255,0.09); text-decoration: none; } 394 - 395 - /* ── ABOUT ──────────────────────────────── */ 396 - .about-text { 397 - color: var(--subtext); 398 - font-size: 0.92rem; 399 - line-height: 1.7; 400 - } 401 - 402 - .about-text p + p { margin-top: 12px; } 403 - 404 - .social-row { 405 - display: flex; 406 - flex-wrap: wrap; 407 - gap: 10px; 408 - justify-content: center; 409 - } 410 - 411 - .social-btn { 412 - display: inline-flex; 413 - align-items: center; 414 - gap: 8px; 415 - padding: 9px 16px; 416 - background: rgba(255,255,255,0.04); 417 - border: 1px solid var(--border); 418 - border-radius: 999px; 419 - color: var(--text); 420 - font-size: 0.88rem; 421 - font-weight: 600; 422 - transition: all 0.2s; 423 - } 424 - 425 - .social-btn:hover { 426 - background: rgba(255,255,255,0.08); 427 - transform: translateY(-2px); 428 - box-shadow: 0 4px 12px rgba(0,0,0,0.3); 429 - text-decoration: none; 430 - } 431 - 432 - /* ── HOW IT WORKS ───────────────────────── */ 433 - .steps { 434 - display: flex; 435 - flex-direction: column; 436 - gap: 18px; 437 - margin-bottom: 28px; 438 - } 439 - 440 - .step { 441 - display: flex; 442 - align-items: flex-start; 443 - gap: 16px; 444 - } 445 - 446 - .step-num { 447 - flex-shrink: 0; 448 - width: 34px; height: 34px; 449 - border-radius: 50%; 450 - background: linear-gradient(135deg, var(--stat-bg), var(--card-bg)); 451 - border: 1px solid var(--border); 452 - display: flex; 453 - align-items: center; 454 - justify-content: center; 455 - font-size: 0.78rem; 456 - font-weight: 700; 457 - color: var(--link); 458 - } 459 - 460 - .step-body h3 { font-size: 0.95rem; font-weight: 700; margin-bottom: 3px; } 461 - .step-body p { color: var(--subtext); font-size: 0.85rem; line-height: 1.55; } 462 - 463 - .how-grid { 464 - display: grid; 465 - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 466 - gap: 14px; 467 - } 468 - 469 - .how-card { 470 - background: var(--post-bg); 471 - border: 1px solid var(--border); 472 - border-radius: 10px; 473 - padding: 18px; 474 - } 475 - 476 - .how-card i { font-size: 1.5rem; color: var(--link); margin-bottom: 10px; display: block; } 477 - .how-card h3 { font-size: 0.9rem; font-weight: 700; margin-bottom: 5px; } 478 - .how-card p { font-size: 0.8rem; color: var(--subtext); line-height: 1.55; } 479 - 480 - /* ── LIMITS ─────────────────────────────── */ 481 - .limits-grid { 482 - display: grid; 483 - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 484 - gap: 14px; 485 - margin-bottom: 22px; 486 - } 487 - 488 - .limit-card { 489 - background: var(--post-bg); 490 - border: 1px solid var(--border); 491 - border-radius: 12px; 492 - padding: 22px 18px; 493 - text-align: center; 494 - position: relative; 495 - overflow: hidden; 496 - } 497 - 498 - .limit-card::before { 499 - content: ''; 500 - position: absolute; 501 - top: 0; left: 0; right: 0; 502 - height: 3px; 503 - background: linear-gradient(90deg, var(--link), rgba(255,204,204,0.2)); 504 - } 505 - 506 - .limit-card i { font-size: 1.8rem; color: var(--link); margin-bottom: 12px; display: block; opacity: 0.8; } 507 - .limit-value { font-size: 1.7rem; font-weight: 700; letter-spacing: -0.5px; margin-bottom: 4px; } 508 - .limit-label { font-size: 0.82rem; color: var(--subtext); font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; } 509 - .limit-note { margin-top: 5px; font-size: 0.72rem; color: rgba(220,186,186,0.5); } 510 - 511 - /* ── USAGE BARS ─────────────────────────── */ 512 - .usage-section { margin-top: 28px; } 513 - 514 - .usage-section h2 { font-size: 1rem; font-weight: 700; margin-bottom: 16px; } 515 - 516 - .usage-item { margin-bottom: 16px; } 517 - 518 - .usage-row { 519 - display: flex; 520 - justify-content: space-between; 521 - font-size: 0.82rem; 522 - color: var(--subtext); 523 - margin-bottom: 6px; 524 - } 525 - 526 - .usage-row span:first-child { font-weight: 600; color: var(--text); } 527 - 528 - .usage-track { 529 - height: 8px; 530 - background: var(--post-bg); 531 - border-radius: 999px; 532 - overflow: hidden; 533 - border: 1px solid var(--border); 534 - } 535 - 536 - .usage-fill { 537 - height: 100%; 538 - border-radius: 999px; 539 - background: linear-gradient(90deg, var(--link), var(--green)); 540 - transition: width 0.6s cubic-bezier(.4,0,.2,1); 541 - } 542 - 543 - .usage-fill.warn { background: linear-gradient(90deg, #ffaa00, #ff6b00); } 544 - .usage-fill.danger { background: linear-gradient(90deg, var(--red), #cc0000); } 545 - 546 - .usage-loading { font-size: 0.8rem; color: var(--subtext); opacity: 0.5; } 547 - 548 - .limits-note { 549 - padding: 14px 18px; 550 - background: rgba(255,107,107,0.06); 551 - border: 1px solid rgba(255,107,107,0.18); 552 - border-radius: 10px; 553 - font-size: 0.83rem; 554 - color: var(--subtext); 555 - line-height: 1.6; 556 - } 557 - 558 - .limits-note i { color: var(--red); margin-right: 4px; } 559 - 560 - /* ── FOOTER ─────────────────────────────── */ 561 - .site-footer { 562 - text-align: center; 563 - padding: 0 20px 32px; 564 - font-size: 0.82rem; 565 - color: var(--subtext); 566 - } 567 - 568 - /* ── RESPONSIVE ─────────────────────────── */ 569 - @media (max-width: 600px) { 570 - .stats-grid { grid-template-columns: repeat(2, 1fr); } 571 - .stat-card.storage-card { flex-direction: column; text-align: center; gap: 8px; padding: 20px; } 572 - .stat-card.storage-card .stat-icon { margin-bottom: 0; } 573 - .site-header h1 { font-size: 1.8rem; } 574 - .stat-value { font-size: 1.5rem; } 575 - .how-grid, .limits-grid { grid-template-columns: 1fr; } 576 - .card { padding: 22px 18px; } 577 - } 578 - </style> 579 - </head> 580 - <body> 581 - 582 - <header class="site-header"> 583 - <h1><i class="fa-solid fa-database" style="color:#fff"></i> MBD CDN</h1> 584 - <p>A simple easy to use CDN, free for life</p> 585 - <div id="total-visitors" style="font-size: 0.8em; color: gray; text-align: center;"> 586 - Calculating total visits... 587 - </div> 588 - </header> 589 - 590 - <!-- STATS --> 591 - <div class="stats-wrap"> 592 - <div class="stats-grid"> 593 - <div class="stat-card"> 594 - <div class="stat-icon"><i class="fa-regular fa-image"></i></div> 595 - <div class="stat-value loading" id="stat-images">--</div> 596 - <div class="stat-label">Images</div> 597 - </div> 598 - <div class="stat-card"> 599 - <div class="stat-icon"><i class="fa-solid fa-video"></i></div> 600 - <div class="stat-value loading" id="stat-videos">--</div> 601 - <div class="stat-label">Videos</div> 602 - </div> 603 - <div class="stat-card"> 604 - <div class="stat-icon"><i class="fa-solid fa-photo-film"></i></div> 605 - <div class="stat-value loading" id="stat-gifs">--</div> 606 - <div class="stat-label">GIFs</div> 607 - </div> 608 - <div class="stat-card"> 609 - <div class="stat-icon"><i class="fa-solid fa-file-code"></i></div> 610 - <div class="stat-value loading" id="stat-documents">--</div> 611 - <div class="stat-label">Documents</div> 612 - </div> 613 - </div> 614 - <div class="stats-grid-storage"> 615 - <div class="stat-card storage-card"> 616 - <div class="stat-icon"><i class="fa-solid fa-database"></i></div> 617 - <div> 618 - <div class="stat-value loading" id="stat-storage">--</div> 619 - <div class="stat-label">Storage Used</div> 620 - </div> 621 - </div> 622 - </div> 623 - </div> 624 - 625 - <!-- TABS --> 626 - <div class="tabs-wrap"> 627 - <div class="tab-bar"> 628 - <button class="tab-btn active" onclick="switchTab('upload', this)"> 629 - <i class="fa-solid fa-cloud-arrow-up"></i> Upload 630 - </button> 631 - <a class="tab-btn" href="/cdn/about.html"> 632 - <i class="fa-solid fa-circle-info"></i> About 633 - </a> 634 - <a class="tab-btn" href="/cdn/how-it-works.html"> 635 - <i class="fa-solid fa-gears"></i> How it Works 636 - </a> 637 - <a class="tab-btn" href="/cdn/limits.html"> 638 - <i class="fa-solid fa-gauge-high"></i> Limits 639 - </a> 640 - </div> 641 - 642 - <div class="tab-content"> 643 - 644 - <!-- UPLOAD --> 645 - <div class="tab-pane active" id="tab-upload"> 646 - <div class="card"> 647 - <h2>Upload a File</h2> 648 - <p class="desc">Images, GIFs, videos, and documents accepted. Files are served globally via Cloudflare immediately after upload.</p> 649 - 650 - <label class="drop-zone" id="drop-zone" for="file-input"> 651 - <i class="fa-solid fa-cloud-arrow-up" id="drop-icon"></i> 652 - <span id="file-name">Click to select or drag a file here</span> 653 - <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"> 654 - </label> 655 - 656 - <div class="file-info" id="file-info"> 657 - <div><b>File:</b> <span id="detail-name"></span></div> 658 - <div style="margin-top:4px"><b>Size:</b> <span id="detail-size"></span> &nbsp;·&nbsp; <b>Type:</b> <span id="detail-type"></span></div> 659 - </div> 660 - 661 - <div class="progress-wrap" id="progress-wrap"> 662 - <div class="progress-track"> 663 - <div class="progress-fill" id="progress-fill"></div> 664 - </div> 665 - <div class="progress-label" id="progress-label">0%</div> 666 - </div> 667 - 668 - <button class="upload-btn" id="upload-btn"> 669 - <i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN 670 - </button> 671 - 672 - <div class="status-msg" id="status"></div> 673 - 674 - <div class="result-box" id="result-box"> 675 - <div class="result-label">✅ Public URL</div> 676 - <div class="result-url" id="result-url"></div> 677 - <button class="copy-btn" id="copy-btn" onclick="copyUrl()"> 678 - <i class="fa-solid fa-copy"></i> Copy URL 679 - </button> 680 - <a class="open-btn" id="open-link" href="#" target="_blank" rel="noopener"> 681 - <i class="fa-solid fa-arrow-up-right-from-square"></i> Open 682 - </a> 683 - </div> 684 - </div> 685 - </div> 686 - 687 - </div> 688 - </div> 689 - 690 - <footer class="site-footer"> 691 - &copy; <script>document.write(new Date().getFullYear())</script> <a href="https://madebydanny.uk" target="_blank">madebydanny.uk</a> 692 - </footer> 693 - <script src="https://visit-counter.madebydanny.uk?url=mbd-cdn"></script> 694 - <script> 695 - const API = 'https://cdn.madebydanny.uk'; 696 - 697 - function formatBytes(b) { 698 - if (!b) return '0 B'; 699 - const k = 1024, s = ['B','KB','MB','GB','TB']; 700 - const i = Math.floor(Math.log(b) / Math.log(k)); 701 - return (b / Math.pow(k, i)).toFixed(1).replace(/\.0$/,'') + ' ' + s[i]; 702 - } 703 - 704 - function fmt(n) { return Number(n).toLocaleString(); } 705 - 706 - // ── TABS ───────────────────────────────────────────── 707 - function switchTab(name, btn) { 708 - document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); 709 - document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); 710 - document.getElementById('tab-' + name).classList.add('active'); 711 - if (btn) btn.classList.add('active'); 712 - if (name === 'limits') loadLimits(); 713 - } 714 - 715 - // ── STATS ───────────────────────────────────────────── 716 - async function loadStats() { 717 - try { 718 - const r = await fetch(`${API}/stats`); 719 - const d = await r.json(); 720 - if (d.success) { 721 - const cat = d.stats.byCategory || {}; 722 - document.getElementById('stat-images').textContent = fmt(cat.image || 0); 723 - document.getElementById('stat-videos').textContent = fmt(cat.video || 0); 724 - document.getElementById('stat-gifs').textContent = fmt(cat.gif || 0); 725 - document.getElementById('stat-documents').textContent = fmt(cat.document || 0); 726 - document.getElementById('stat-storage').textContent = formatBytes(d.stats.totalSize); 727 - document.querySelectorAll('.stat-value').forEach(v => v.classList.remove('loading')); 728 - } 729 - } catch { 730 - document.querySelectorAll('.stat-value').forEach(v => { 731 - v.textContent = '—'; 732 - v.classList.remove('loading'); 733 - }); 734 - } 735 - } 736 - 737 - // ── LIMITS (live from API) ──────────────────────────── 738 - async function loadLimits() { 739 - try { 740 - const r = await fetch(`${API}/limits`); 741 - const d = await r.json(); 742 - if (!d.success) throw new Error(); 743 - 744 - const { file_count, total_size, max_files, max_bytes, max_file } = d.limits; 745 - 746 - document.getElementById('limit-max-file').textContent = formatBytes(max_file); 747 - document.getElementById('limit-max-bytes').textContent = formatBytes(max_bytes); 748 - document.getElementById('limit-max-files').textContent = max_files; 749 - 750 - const filePct = Math.min((file_count / max_files) * 100, 100); 751 - const bytesPct = Math.min((total_size / max_bytes) * 100, 100); 752 - 753 - const filesFill = document.getElementById('usage-files-fill'); 754 - const bytesFill = document.getElementById('usage-bytes-fill'); 755 - 756 - filesFill.style.width = filePct + '%'; 757 - bytesFill.style.width = bytesPct + '%'; 758 - 759 - function fillClass(pct) { 760 - if (pct >= 90) return 'danger'; 761 - if (pct >= 70) return 'warn'; 762 - return ''; 763 - } 764 - 765 - filesFill.className = 'usage-fill ' + fillClass(filePct); 766 - bytesFill.className = 'usage-fill ' + fillClass(bytesPct); 767 - 768 - document.getElementById('usage-files-label').textContent = 769 - `${fmt(file_count)} / ${fmt(max_files)}`; 770 - document.getElementById('usage-bytes-label').textContent = 771 - `${formatBytes(total_size)} / ${formatBytes(max_bytes)}`; 772 - 773 - document.getElementById('usage-loading').style.display = 'none'; 774 - document.getElementById('usage-bars').style.display = 'block'; 775 - 776 - } catch { 777 - document.getElementById('usage-loading').textContent = 'Could not load usage data.'; 778 - } 779 - } 780 - 781 - // ── FILE TYPE HELPERS ───────────────────────────────── 782 - const DOCUMENT_TYPES = new Set([ 783 - 'text/html','text/css','text/javascript','text/plain','text/csv', 784 - 'text/xml','text/markdown','text/yaml','application/json', 785 - 'application/xml','application/pdf','application/javascript', 786 - 'application/x-yaml', 787 - ]); 788 - 789 - function getFileIcon(type) { 790 - if (!type) return 'fa-solid fa-cloud-arrow-up'; 791 - if (type.startsWith('video/')) return 'fa-solid fa-film'; 792 - if (type === 'image/gif') return 'fa-solid fa-photo-film'; 793 - if (type.startsWith('image/')) return 'fa-regular fa-image'; 794 - if (type === 'application/pdf') return 'fa-solid fa-file-pdf'; 795 - if (type === 'application/json') return 'fa-solid fa-file-code'; 796 - if (type === 'text/html') return 'fa-solid fa-file-code'; 797 - if (type === 'text/css') return 'fa-solid fa-file-code'; 798 - if (type === 'text/javascript' || 799 - type === 'application/javascript') return 'fa-solid fa-file-code'; 800 - if (type === 'text/csv') return 'fa-solid fa-file-csv'; 801 - if (type === 'text/plain') return 'fa-solid fa-file-lines'; 802 - if (DOCUMENT_TYPES.has(type)) return 'fa-solid fa-file-code'; 803 - return 'fa-solid fa-cloud-arrow-up'; 804 - } 805 - 806 - // ── FILE SELECT ─────────────────────────────────────── 807 - const fileInput = document.getElementById('file-input'); 808 - const dropZone = document.getElementById('drop-zone'); 809 - const fileInfo = document.getElementById('file-info'); 810 - 811 - function showFile(file) { 812 - const type = file.type || ''; 813 - document.getElementById('drop-icon').className = getFileIcon(type); 814 - document.getElementById('file-name').textContent = file.name; 815 - document.getElementById('detail-name').textContent = file.name; 816 - document.getElementById('detail-size').textContent = formatBytes(file.size); 817 - document.getElementById('detail-type').textContent = type || 'Unknown'; 818 - fileInfo.classList.add('show'); 819 - } 820 - 821 - fileInput.addEventListener('change', () => { 822 - if (fileInput.files[0]) showFile(fileInput.files[0]); 823 - }); 824 - 825 - dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); }); 826 - dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); }); 827 - dropZone.addEventListener('drop', e => { 828 - e.preventDefault(); 829 - dropZone.classList.remove('drag-over'); 830 - const f = e.dataTransfer.files[0]; 831 - if (f) { fileInput.files = e.dataTransfer.files; showFile(f); } 832 - }); 833 - 834 - // ── UPLOAD (XHR for real progress) ─────────────────── 835 - const uploadBtn = document.getElementById('upload-btn'); 836 - const progressWrap = document.getElementById('progress-wrap'); 837 - const progressFill = document.getElementById('progress-fill'); 838 - const progressLabel= document.getElementById('progress-label'); 839 - const statusEl = document.getElementById('status'); 840 - const resultBox = document.getElementById('result-box'); 841 - const resultUrl = document.getElementById('result-url'); 842 - 843 - function setStatus(msg, color) { 844 - statusEl.textContent = msg; 845 - statusEl.style.color = color || 'var(--link)'; 846 - } 847 - 848 - function setProgress(pct) { 849 - progressFill.style.width = pct + '%'; 850 - progressLabel.textContent = Math.round(pct) + '%'; 851 - } 852 - 853 - function resetUploadUI(delay = 0) { 854 - setTimeout(() => { 855 - fileInput.value = ''; 856 - document.getElementById('drop-icon').className = 'fa-solid fa-cloud-arrow-up'; 857 - document.getElementById('file-name').textContent = 'Click to select or drag a file here'; 858 - fileInfo.classList.remove('show'); 859 - progressWrap.classList.remove('show'); 860 - setProgress(0); 861 - }, delay); 862 - } 863 - 864 - uploadBtn.addEventListener('click', () => { 865 - if (!fileInput.files[0]) { 866 - setStatus('⚠️ Please select a file first.', 'var(--red)'); 867 - return; 868 - } 869 - 870 - const file = fileInput.files[0]; 871 - uploadBtn.disabled = true; 872 - uploadBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Uploading…'; 873 - setStatus(''); 874 - resultBox.classList.remove('show'); 875 - progressWrap.classList.add('show'); 876 - setProgress(0); 877 - 878 - const xhr = new XMLHttpRequest(); 879 - 880 - xhr.upload.addEventListener('progress', e => { 881 - if (e.lengthComputable) setProgress((e.loaded / e.total) * 100); 882 - }); 883 - 884 - xhr.addEventListener('load', () => { 885 - setProgress(100); 886 - try { 887 - const data = JSON.parse(xhr.responseText); 888 - if (data.success) { 889 - resultUrl.textContent = data.url; 890 - document.getElementById('open-link').href = data.url; 891 - resultBox.classList.add('show'); 892 - setStatus(''); 893 - setTimeout(() => loadStats(), 600); 894 - resetUploadUI(3000); 895 - } else { 896 - throw new Error(data.error || 'Upload failed'); 897 - } 898 - } catch (err) { 899 - progressWrap.classList.remove('show'); 900 - setStatus('❌ ' + err.message, 'var(--red)'); 901 - } 902 - 903 - uploadBtn.disabled = false; 904 - uploadBtn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN'; 905 - }); 906 - 907 - xhr.addEventListener('error', () => { 908 - progressWrap.classList.remove('show'); 909 - setStatus('❌ Network error. Please try again.', 'var(--red)'); 910 - uploadBtn.disabled = false; 911 - uploadBtn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN'; 912 - }); 913 - 914 - xhr.addEventListener('abort', () => { 915 - progressWrap.classList.remove('show'); 916 - setStatus('Upload cancelled.', 'var(--subtext)'); 917 - uploadBtn.disabled = false; 918 - uploadBtn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> Upload to MBD CDN'; 919 - }); 920 - 921 - xhr.open('POST', API); 922 - xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream'); 923 - xhr.send(file); 924 - }); 925 - 926 - // ── COPY ───────────────────────────────────────────── 927 - function copyUrl() { 928 - navigator.clipboard.writeText(resultUrl.textContent); 929 - const btn = document.getElementById('copy-btn'); 930 - btn.classList.add('copied'); 931 - btn.innerHTML = '<i class="fa-solid fa-check"></i> Copied!'; 932 - setTimeout(() => { 933 - btn.classList.remove('copied'); 934 - btn.innerHTML = '<i class="fa-solid fa-copy"></i> Copy URL'; 935 - }, 2000); 936 - } 937 - 938 - // ── SOCIAL LINKS FALLBACK ───────────────────────────── 939 - window.addEventListener('load', () => { 940 - const el = document.getElementById('social-links'); 941 - if (el && !el.children.length) { 942 - el.innerHTML = ` 943 - <a class="social-btn" href="https://madebydanny.uk" target="_blank" rel="noopener"> 944 - <i class="fa-solid fa-globe"></i> madebydanny.uk 945 - </a>`; 946 - } 947 - }); 948 - 949 - // ── INIT ───────────────────────────────────────────── 950 - loadStats(); 951 - </script> 952 - 953 - <!-- social-links.js is optional; silence 404s by loading it async --> 954 - <script> 955 - (function() { 956 - const s = document.createElement('script'); 957 - s.src = '/js/social-links.js'; 958 - s.onerror = () => {}; 959 - document.body.appendChild(s); 960 - })(); 961 - </script> 962 - 963 - </body> 964 - </html>
-152
cdn/limits.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 Limits - 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="MBD CDN usage and upload limits."> 10 - <meta property="og:title" content="MBD CDN Limits - madebydanny.uk"> 11 - <meta property="og:description" content="MBD CDN usage and upload limits."> 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 - <link rel="stylesheet" href="/css/cdn/limits.css"> 15 - </head> 16 - <body> 17 - <header class="site-header"> 18 - <h1><i class="fa-solid fa-database" style="color:#fff"></i> MBD CDN</h1> 19 - <p>A simple easy to use CDN, free for life</p> 20 - </header> 21 - 22 - <div class="tabs-wrap"> 23 - <div class="tab-bar"> 24 - <a class="tab-btn" href="/cdn/index.html"><i class="fa-solid fa-cloud-arrow-up"></i> Upload</a> 25 - <a class="tab-btn" href="/cdn/about.html"><i class="fa-solid fa-circle-info"></i> About</a> 26 - <a class="tab-btn" href="/cdn/how-it-works.html"><i class="fa-solid fa-gears"></i> How it Works</a> 27 - <a class="tab-btn active" href="/cdn/limits.html"><i class="fa-solid fa-gauge-high"></i> Limits</a> 28 - </div> 29 - 30 - <div class="card"> 31 - <h2>Usage Limits</h2> 32 - <p class="desc">Fair-use limits are in place to keep the CDN reliable and available for everyone.</p> 33 - 34 - <div class="limits-grid"> 35 - <div class="limit-card"> 36 - <i class="fa-solid fa-file-arrow-up"></i> 37 - <div class="limit-value" id="limit-max-file">100 MB</div> 38 - <div class="limit-label">Max File Size</div> 39 - <div class="limit-note">Per individual upload</div> 40 - </div> 41 - <div class="limit-card"> 42 - <i class="fa-solid fa-hard-drive"></i> 43 - <div class="limit-value" id="limit-max-bytes">-</div> 44 - <div class="limit-label">Daily Storage</div> 45 - <div class="limit-note">Total uploads per day</div> 46 - </div> 47 - <div class="limit-card"> 48 - <i class="fa-solid fa-arrow-up-from-bracket"></i> 49 - <div class="limit-value" id="limit-max-files">-</div> 50 - <div class="limit-label">Uploads Per Day</div> 51 - <div class="limit-note">Resets at midnight UTC</div> 52 - </div> 53 - </div> 54 - 55 - <div class="limits-note"> 56 - <i class="fa-solid fa-circle-exclamation"></i> 57 - 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. 58 - </div> 59 - 60 - <div class="usage-section"> 61 - <h2>Your Usage Today</h2> 62 - <div id="usage-loading" class="usage-loading">Loading your usage...</div> 63 - <div id="usage-bars" style="display:none"> 64 - <div class="usage-item"> 65 - <div class="usage-row"> 66 - <span>Files Uploaded</span> 67 - <span id="usage-files-label">0 / 30</span> 68 - </div> 69 - <div class="usage-track"><div class="usage-fill" id="usage-files-fill" style="width:0%"></div></div> 70 - </div> 71 - <div class="usage-item" style="margin-top:14px"> 72 - <div class="usage-row"> 73 - <span>Storage Used</span> 74 - <span id="usage-bytes-label">0 B / 1 GB</span> 75 - </div> 76 - <div class="usage-track"><div class="usage-fill" id="usage-bytes-fill" style="width:0%"></div></div> 77 - </div> 78 - </div> 79 - </div> 80 - 81 - <hr class="divider"> 82 - 83 - <h2 style="font-size:1rem; margin-bottom:10px;">Accepted File Types</h2> 84 - <p style="color:var(--subtext); font-size:0.85rem; line-height:1.7;"> 85 - <strong style="color:var(--text)">Images:</strong> JPEG, PNG, WebP, AVIF, SVG &nbsp;&nbsp; 86 - <strong style="color:var(--text)">Animated:</strong> GIF &nbsp;&nbsp; 87 - <strong style="color:var(--text)">Video:</strong> MP4, WebM, MOV 88 - </p> 89 - </div> 90 - </div> 91 - 92 - <footer class="site-footer"> 93 - &copy; <script>document.write(new Date().getFullYear())</script> <a href="https://madebydanny.uk" target="_blank">madebydanny.uk</a> 94 - </footer> 95 - 96 - <script> 97 - const API = 'https://cdn.madebydanny.uk'; 98 - 99 - function formatBytes(b) { 100 - if (!b) return '0 B'; 101 - const k = 1024; 102 - const s = ['B','KB','MB','GB','TB']; 103 - const i = Math.floor(Math.log(b) / Math.log(k)); 104 - return (b / Math.pow(k, i)).toFixed(1).replace(/\.0$/, '') + ' ' + s[i]; 105 - } 106 - 107 - function fmt(n) { return Number(n).toLocaleString(); } 108 - 109 - async function loadLimits() { 110 - try { 111 - const r = await fetch(`${API}/limits`); 112 - const d = await r.json(); 113 - if (!d.success) throw new Error(); 114 - 115 - const { file_count, total_size, max_files, max_bytes, max_file } = d.limits; 116 - 117 - document.getElementById('limit-max-file').textContent = formatBytes(max_file); 118 - document.getElementById('limit-max-bytes').textContent = formatBytes(max_bytes); 119 - document.getElementById('limit-max-files').textContent = max_files; 120 - 121 - const filePct = Math.min((file_count / max_files) * 100, 100); 122 - const bytesPct = Math.min((total_size / max_bytes) * 100, 100); 123 - 124 - const filesFill = document.getElementById('usage-files-fill'); 125 - const bytesFill = document.getElementById('usage-bytes-fill'); 126 - 127 - filesFill.style.width = filePct + '%'; 128 - bytesFill.style.width = bytesPct + '%'; 129 - 130 - function fillClass(pct) { 131 - if (pct >= 90) return 'danger'; 132 - if (pct >= 70) return 'warn'; 133 - return ''; 134 - } 135 - 136 - filesFill.className = 'usage-fill ' + fillClass(filePct); 137 - bytesFill.className = 'usage-fill ' + fillClass(bytesPct); 138 - 139 - document.getElementById('usage-files-label').textContent = `${fmt(file_count)} / ${fmt(max_files)}`; 140 - document.getElementById('usage-bytes-label').textContent = `${formatBytes(total_size)} / ${formatBytes(max_bytes)}`; 141 - 142 - document.getElementById('usage-loading').style.display = 'none'; 143 - document.getElementById('usage-bars').style.display = 'block'; 144 - } catch { 145 - document.getElementById('usage-loading').textContent = 'Could not load usage data.'; 146 - } 147 - } 148 - 149 - loadLimits(); 150 - </script> 151 - </body> 152 - </html>
-287
cdn/worker.js
··· 1 - /** 2 - * MBD CDN — Cloudflare Worker 3 - * 4 - * Endpoints: 5 - * POST / — upload file (enforces per-IP limits) 6 - * GET /stats — global stats (images / videos / gifs / totalSize) 7 - * GET /limits — today's usage for the requesting IP 8 - * 9 - * D1 tables required: 10 - * uploads (id, filename, path, content_type, file_type, size, upload_date) 11 - * upload_limits (ip TEXT, date TEXT, file_count INT DEFAULT 0, total_size INT DEFAULT 0, PRIMARY KEY (ip, date)) 12 - */ 13 - 14 - // ── LIMIT CONSTANTS ──────────────────────────────────────────────────────── 15 - const MAX_FILE_BYTES = 70 * 1024 * 1024; // 70 MB per file 16 - const MAX_DAY_BYTES = 300 * 1024 * 1024; // 300 MB per IP per day 17 - const MAX_DAY_FILES = 20; // 20 files per IP per day 18 - 19 - const CDN_BASE_URL = "https://cdn.madebydanny.uk"; 20 - 21 - export default { 22 - async fetch(request, env, ctx) { 23 - const url = new URL(request.url); 24 - 25 - const cors = { 26 - "Access-Control-Allow-Origin": "*", 27 - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", 28 - "Access-Control-Allow-Headers": "Content-Type", 29 - }; 30 - 31 - const json = (data, status = 200, extra = {}) => 32 - new Response(JSON.stringify(data), { 33 - status, 34 - headers: { "Content-Type": "application/json", ...cors, ...extra }, 35 - }); 36 - 37 - // ── Sanity check bindings ─────────────────── 38 - if (!env.DB) { 39 - return json({ success: false, error: "DB binding not configured" }, 500); 40 - } 41 - if (!env.MY_BUCKET) { 42 - return json({ success: false, error: "MY_BUCKET binding not configured" }, 500); 43 - } 44 - 45 - // ── CORS preflight ────────────────────────────── 46 - if (request.method === "OPTIONS") { 47 - return new Response(null, { headers: cors }); 48 - } 49 - 50 - // ── GET /user-content/… — Serve file from R2 ─── 51 - if (request.method === "GET" && url.pathname.startsWith("/user-content/")) { 52 - const key = url.pathname.slice(1); // strip leading / 53 - try { 54 - const object = await env.MY_BUCKET.get(key); 55 - if (!object) { 56 - return new Response("Not found", { status: 404, headers: cors }); 57 - } 58 - const headers = new Headers(cors); 59 - object.writeHttpMetadata(headers); 60 - headers.set("Cache-Control", "public, max-age=31536000, immutable"); 61 - headers.set("ETag", object.httpEtag); 62 - return new Response(object.body, { headers }); 63 - } catch (e) { 64 - return new Response("Failed to fetch file", { status: 500, headers: cors }); 65 - } 66 - } 67 - 68 - // ── GET /stats ────────────────────────────────── 69 - if (request.method === "GET" && url.pathname === "/stats") { 70 - try { 71 - const stats = await getStatistics(env); 72 - return json( 73 - { success: true, stats }, 74 - 200, 75 - { "Cache-Control": "public, max-age=30, stale-while-revalidate=60" } 76 - ); 77 - } catch (e) { 78 - return json({ success: false, error: `Stats failed: ${e.message}` }, 500); 79 - } 80 - } 81 - 82 - // ── GET /limits ───────────────────────────────── 83 - if (request.method === "GET" && url.pathname === "/limits") { 84 - try { 85 - const ip = clientIp(request); 86 - const today = todayStr(); 87 - const row = await env.DB.prepare( 88 - `SELECT file_count, total_size FROM upload_limits WHERE ip = ? AND date = ?` 89 - ).bind(ip, today).first(); 90 - 91 - return json( 92 - { 93 - success: true, 94 - limits: { 95 - file_count: row?.file_count || 0, 96 - total_size: row?.total_size || 0, 97 - max_files: MAX_DAY_FILES, 98 - max_bytes: MAX_DAY_BYTES, 99 - max_file: MAX_FILE_BYTES, 100 - }, 101 - }, 102 - 200, 103 - { "Cache-Control": "private, max-age=10" } 104 - ); 105 - } catch (e) { 106 - return json({ success: false, error: `Limits check failed: ${e.message}` }, 500); 107 - } 108 - } 109 - 110 - // ── POST / — Upload ───────────────────────────── 111 - if (request.method === "POST") { 112 - const ip = clientIp(request); 113 - const today = todayStr(); 114 - const contentType = request.headers.get("Content-Type") || "application/octet-stream"; 115 - const contentLength = parseInt(request.headers.get("Content-Length") || "0"); 116 - 117 - // ── 1. Per-file size check (fast, no I/O) ──── 118 - if (contentLength > MAX_FILE_BYTES) { 119 - return json({ 120 - success: false, 121 - error: `File too large. Max ${MAX_FILE_BYTES / 1024 / 1024} MB per file.`, 122 - }, 413); 123 - } 124 - 125 - try { 126 - const now = new Date(); 127 - const dateStr = now.toISOString().split("T")[0]; 128 - const randomId = crypto.randomUUID(); 129 - const fileInfo = getFileInfo(contentType); 130 - const path = `user-content/${dateStr}/${randomId}${fileInfo.extension}`; 131 - 132 - // ── 2. Race: DB limit check vs R2 upload ──── 133 - const [usageResult, uploadResult] = await Promise.allSettled([ 134 - env.DB.prepare( 135 - `SELECT file_count, total_size FROM upload_limits WHERE ip = ? AND date = ?` 136 - ).bind(ip, today).first(), 137 - env.MY_BUCKET.put(path, request.body, { httpMetadata: { contentType } }), 138 - ]); 139 - 140 - // Bubble up R2 errors 141 - if (uploadResult.status === "rejected") { 142 - throw new Error(`R2 upload failed: ${uploadResult.reason?.message}`); 143 - } 144 - 145 - // obj.size is ground truth — fall back to Content-Length only if absent 146 - const actualSize = uploadResult.value?.size ?? contentLength; 147 - 148 - // ── 3. Enforce limits post-upload ─────────── 149 - const usage = usageResult.status === "fulfilled" ? usageResult.value : null; 150 - const usedFiles = usage?.file_count || 0; 151 - const usedBytes = usage?.total_size || 0; 152 - 153 - if (usedFiles >= MAX_DAY_FILES) { 154 - ctx.waitUntil(env.MY_BUCKET.delete(path)); 155 - return json({ 156 - success: false, 157 - error: `Daily file limit reached (${MAX_DAY_FILES} files/day). Resets at midnight UTC.`, 158 - }, 429); 159 - } 160 - 161 - if (usedBytes + actualSize > MAX_DAY_BYTES) { 162 - ctx.waitUntil(env.MY_BUCKET.delete(path)); 163 - const remaining = MAX_DAY_BYTES - usedBytes; 164 - return json({ 165 - success: false, 166 - error: `Daily storage limit reached. You have ${fmtBytes(remaining)} remaining today. Resets at midnight UTC.`, 167 - }, 429); 168 - } 169 - 170 - // ── 4. Batch both D1 writes in one round-trip 171 - ctx.waitUntil( 172 - env.DB.batch([ 173 - env.DB.prepare( 174 - `INSERT INTO uploads (id, filename, path, content_type, file_type, size, upload_date) 175 - VALUES (?, ?, ?, ?, ?, ?, ?)` 176 - ).bind( 177 - randomId, 178 - `${randomId}${fileInfo.extension}`, 179 - path, 180 - contentType, 181 - fileInfo.type, 182 - actualSize, 183 - now.toISOString() 184 - ), 185 - 186 - env.DB.prepare( 187 - `INSERT INTO upload_limits (ip, date, file_count, total_size) 188 - VALUES (?, ?, 1, ?) 189 - ON CONFLICT(ip, date) DO UPDATE SET 190 - file_count = file_count + 1, 191 - total_size = total_size + excluded.total_size` 192 - ).bind(ip, today, actualSize), 193 - ]) 194 - ); 195 - 196 - const fileUrl = `${CDN_BASE_URL}/${path}`; 197 - 198 - return json({ 199 - success: true, 200 - url: fileUrl, 201 - path, 202 - contentType, 203 - fileType: fileInfo.type, 204 - size: actualSize, 205 - }); 206 - 207 - } catch (e) { 208 - return json({ success: false, error: `Upload failed: ${e.message}` }, 500); 209 - } 210 - } 211 - 212 - return json( 213 - { success: false, error: "Invalid request. POST to upload, GET /stats or GET /limits." }, 214 - 405 215 - ); 216 - }, 217 - }; 218 - 219 - // ── HELPERS ──────────────────────────────────────────────────────────────── 220 - 221 - /** Run all stat queries in parallel */ 222 - async function getStatistics(env) { 223 - const [images, videos, gifs, storage] = await Promise.all([ 224 - env.DB.prepare(`SELECT COUNT(*) AS c FROM uploads WHERE file_type = 'image'`).first(), 225 - env.DB.prepare(`SELECT COUNT(*) AS c FROM uploads WHERE file_type = 'video'`).first(), 226 - env.DB.prepare(`SELECT COUNT(*) AS c FROM uploads WHERE file_type = 'gif'`).first(), 227 - env.DB.prepare(`SELECT COALESCE(SUM(size), 0) AS s FROM uploads`).first(), 228 - ]); 229 - 230 - return { 231 - images: images?.c || 0, 232 - videos: videos?.c || 0, 233 - gifs: gifs?.c || 0, 234 - totalSize: storage?.s || 0, 235 - totalFiles: (images?.c || 0) + (videos?.c || 0) + (gifs?.c || 0), 236 - }; 237 - } 238 - 239 - /** Best-effort client IP: Cloudflare header → fallback */ 240 - function clientIp(request) { 241 - return ( 242 - request.headers.get("CF-Connecting-IP") || 243 - request.headers.get("X-Forwarded-For")?.split(",")[0].trim() || 244 - "unknown" 245 - ); 246 - } 247 - 248 - /** Today's date as YYYY-MM-DD in UTC */ 249 - function todayStr() { 250 - return new Date().toISOString().split("T")[0]; 251 - } 252 - 253 - /** Human-readable bytes (for error messages) */ 254 - function fmtBytes(b) { 255 - if (!b) return "0 B"; 256 - const k = 1024, s = ["B", "KB", "MB", "GB", "TB"]; 257 - const i = Math.floor(Math.log(b) / Math.log(k)); 258 - return (b / Math.pow(k, i)).toFixed(1).replace(/\.0$/, "") + " " + s[i]; 259 - } 260 - 261 - /** Map content-type → { type, extension } */ 262 - function getFileInfo(contentType) { 263 - const t = contentType.toLowerCase(); 264 - 265 - if (t.includes("image/jpeg") || t.includes("image/jpg")) return { type: "image", extension: ".jpg" }; 266 - if (t.includes("image/png")) return { type: "image", extension: ".png" }; 267 - if (t.includes("image/gif")) return { type: "gif", extension: ".gif" }; 268 - if (t.includes("image/webp")) return { type: "image", extension: ".webp" }; 269 - if (t.includes("image/svg")) return { type: "image", extension: ".svg" }; 270 - if (t.includes("image/avif")) return { type: "image", extension: ".avif" }; 271 - 272 - if (t.includes("video/mp4")) return { type: "video", extension: ".mp4" }; 273 - if (t.includes("video/webm")) return { type: "video", extension: ".webm" }; 274 - if (t.includes("video/quicktime")) return { type: "video", extension: ".mov" }; 275 - if (t.includes("video/")) return { type: "video", extension: "" }; 276 - 277 - if (t.includes("application/pdf")) return { type: "document", extension: ".pdf" }; 278 - 279 - if (t.includes("audio/mpeg")) return { type: "audio", extension: ".mp3" }; 280 - if (t.includes("audio/ogg")) return { type: "audio", extension: ".ogg" }; 281 - if (t.includes("audio/wav")) return { type: "audio", extension: ".wav" }; 282 - if (t.includes("audio/")) return { type: "audio", extension: "" }; 283 - 284 - if (t.includes("image/")) return { type: "image", extension: "" }; 285 - 286 - return { type: "other", extension: "" }; 287 - }
-14
cf-domain-info/madebydanny.co.uk.html
··· 1 - <!-- index.html --> 2 - <!DOCTYPE html> 3 - <html lang="en"> 4 - <head> 5 - <meta charset="UTF-8"> 6 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 - <body> 8 - <h1>This Domain will expire soon!</h1> 9 - <p>The domain <strong>madebydanny.co.uk</strong> is set to expire on 2025-12-14.</p> 10 - <hr> 11 - <p>Daniel Morrisey <i>(<a href="mailto:danielmorrisey@pm.me">danielmorrisey@pm.me)</a></i> is the curent owner of this domain.</p> 12 - <p>If you are interested in acquiring this domain and use Cloudflare, please contact Daniel via the provided email address.</p> 13 - </body> 14 - </html>
css/.DS_Store

This is a binary file and will not be displayed.

-212
css/bsky.css
··· 1 - :root{ 2 - --bg: #0a0a0f; 3 - --card: #1a1a24; 4 - --card-secondary: #13131d; 5 - --text-primary: #e8e8f0; 6 - --text-muted: #9494a8; 7 - --accent: #6366f1; 8 - --accent-hover: #818cf8; 9 - --glass: rgba(99, 102, 241, 0.05); 10 - --border: rgba(99, 102, 241, 0.15); 11 - --radius: 16px; 12 - --maxw: 420px; 13 - } 14 - 15 - html,body{height:100%;margin:0} 16 - body{ 17 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; 18 - display:flex; 19 - align-items:center; 20 - justify-content:center; 21 - padding:24px; 22 - background: radial-gradient(ellipse at top, #1a1a2e 0%, var(--bg) 50%); 23 - color: var(--text-primary); 24 - line-height: 1.6; 25 - } 26 - 27 - .card{ 28 - width:100%; 29 - max-width:var(--maxw); 30 - background: var(--card); 31 - border: 1px solid var(--border); 32 - border-radius:var(--radius); 33 - padding:32px 28px; 34 - box-shadow: 0 20px 60px rgba(0,0,0,0.5), 0 0 1px rgba(99, 102, 241, 0.3); 35 - text-align:center; 36 - transition: transform 0.2s ease, box-shadow 0.2s ease; 37 - } 38 - .card:hover{ 39 - transform:translateY(-2px); 40 - box-shadow: 0 24px 70px rgba(0,0,0,0.6), 0 0 2px rgba(99, 102, 241, 0.5); 41 - } 42 - 43 - .profile{ 44 - display:flex; 45 - gap:16px; 46 - align-items:center; 47 - justify-content:center; 48 - flex-direction:column; 49 - margin-bottom: 24px; 50 - } 51 - .avatar{ 52 - width:96px; 53 - height:96px; 54 - border-radius:50%; 55 - object-fit:cover; 56 - border:3px solid var(--accent); 57 - background: var(--card-secondary); 58 - box-shadow: 0 8px 24px rgba(99, 102, 241, 0.3); 59 - } 60 - .name{ 61 - margin:4px 0 0; 62 - font-size:1.35rem; 63 - font-weight:700; 64 - letter-spacing: -0.02em; 65 - } 66 - .tagline{ 67 - margin:6px 0 0; 68 - color:var(--text-muted); 69 - font-size:0.95rem; 70 - line-height: 1.5; 71 - } 72 - 73 - .links{ 74 - margin-top:20px; 75 - display:grid; 76 - grid-template-columns:repeat(2,1fr); 77 - gap:12px; 78 - } 79 - .link{ 80 - display:flex; 81 - gap:12px; 82 - align-items:center; 83 - padding:14px 12px; 84 - border-radius:12px; 85 - text-decoration:none; 86 - background: var(--glass); 87 - border: 1px solid var(--border); 88 - color: var(--text-primary); 89 - transition: all 0.2s ease; 90 - } 91 - .link img{ 92 - width:32px; 93 - height:32px; 94 - border-radius:8px; 95 - flex:0 0 32px; 96 - } 97 - .link .label{ 98 - font-weight:600; 99 - font-size:0.95rem; 100 - text-align: left; 101 - } 102 - .link:hover{ 103 - transform:translateY(-2px); 104 - background: rgba(255, 255, 255, 0.08); 105 - border-color: var(--accent); 106 - } 107 - 108 - /* Brand hover colors */ 109 - .link.bsky:hover{color:#1083fd;border-color:#1083fd;box-shadow:0 4px 16px rgba(16,131,253,0.3)} 110 - .link.deer:hover{color:#749f7a;border-color:#749f7a;box-shadow:0 4px 16px rgba(116,159,122,0.3)} 111 - .link.catsky:hover{color:#cca6f7;border-color:#cca6f7;box-shadow:0 4px 16px rgba(204,166,247,0.3)} 112 - .link.blacksky:hover{color:#ffffff;border-color:#ffffff;box-shadow:0 4px 16px rgba(255,255,255,0.3)} 113 - .link.anisota:hover{color:#c28431;border-color:#c28431;box-shadow:0 4px 16px rgba(194,132,49,0.3)} 114 - .link.tangled:hover{color:#ffffff;border-color:#ffffff;box-shadow:0 4px 16px rgba(255,255,255,0.3)} 115 - .link.nooki:hover{color:#FDC417;border-color:#FDC417;box-shadow:0 4px 16px rgba(253,196,23,0.3)} 116 - .link.splace:hover{color:#f8baca;border-color:#f8baca;box-shadow:0 4px 16px rgba(248,186,202,0.3)} 117 - .link.bitch:hover{color:#9b7ba0;border-color:#9b7ba0;box-shadow:0 4px 16px rgba(155,123,160,0.3)} 118 - .link.redd:hover{color:#ff4242;border-color:#ff4242;box-shadow:0 4px 16px rgba(255,66,66,0.3)} 119 - .link.semble:hover{color: orange; border-color:orange;box-shadow:0 4px 16px rgba(255, 111, 0, 0.3)} 120 - 121 - #more-clients{ 122 - display: none; 123 - } 124 - #more-clients.show{ 125 - display: grid; 126 - } 127 - 128 - .show-more-btn{ 129 - margin-top: 12px; 130 - padding: 12px 20px; 131 - background: var(--glass); 132 - border: 1px solid var(--border); 133 - border-radius: 10px; 134 - color: var(--accent-hover); 135 - font-weight: 600; 136 - cursor: pointer; 137 - transition: all 0.2s ease; 138 - font-size: 0.9rem; 139 - } 140 - .show-more-btn:hover{ 141 - background: rgba(99, 102, 241, 0.1); 142 - border-color: var(--accent); 143 - transform: translateY(-1px); 144 - } 145 - 146 - .controls{ 147 - margin-top:24px; 148 - display:flex; 149 - flex-direction:column; 150 - gap:10px; 151 - padding-top: 24px; 152 - border-top: 1px solid var(--border); 153 - } 154 - .control-row{ 155 - display:flex; 156 - gap:10px; 157 - align-items:center; 158 - } 159 - .control-row input{ 160 - flex:1; 161 - padding:12px 14px; 162 - border-radius:10px; 163 - border:1px solid var(--border); 164 - background: var(--card-secondary); 165 - color: var(--text-primary); 166 - outline:none; 167 - font-size: 0.95rem; 168 - transition: border-color 0.2s ease; 169 - } 170 - .control-row input:focus{ 171 - border-color: var(--accent); 172 - } 173 - .control-row input::placeholder{ 174 - color: var(--text-muted); 175 - } 176 - .control-row button{ 177 - padding:12px 18px; 178 - border-radius:10px; 179 - border:0; 180 - cursor:pointer; 181 - background: var(--accent); 182 - color: white; 183 - font-weight:600; 184 - font-size: 0.95rem; 185 - transition: background 0.2s ease, transform 0.2s ease; 186 - } 187 - .control-row button:hover{ 188 - background: var(--accent-hover); 189 - transform: translateY(-1px); 190 - } 191 - 192 - .meta{ 193 - margin-top:20px; 194 - font-size:0.85rem; 195 - color:var(--text-muted); 196 - } 197 - .meta a{ 198 - color:var(--accent-hover); 199 - text-decoration:none; 200 - transition: color 0.2s ease; 201 - } 202 - .meta a:hover{ 203 - color: var(--accent); 204 - text-decoration:underline; 205 - } 206 - 207 - @media(max-width:480px){ 208 - :root{--maxw:100%} 209 - .links, #more-clients{grid-template-columns:1fr} 210 - body{padding: 16px} 211 - .card{padding: 24px 20px} 212 - }
css/cdn/limits.css cdn/limits.css
+462 -161
css/index.css
··· 1 - /* --- THEME VARIABLES --- */ 2 - :root { 3 - --bsky-card-bg: #4a2b32; 4 - --bsky-post-bg: #1e1e1e; 5 - --bsky-text-color: #ffffff; 6 - --bsky-subtext-color: #dcbaba; 7 - --bsky-link-color: #ffcccc; 8 - --bsky-border: rgba(255,255,255,0.1); 9 - } 1 + /* ============================================ 2 + madebydanny.uk — personal stylesheet 3 + ============================================ */ 4 + 5 + :root { 6 + --bg: #0e0d0c; 7 + --bg-raised: #171512; 8 + --bg-card: #1a1815; 9 + --border: #2d2926; 10 + --border-hover:#4a4238; 11 + 12 + --text: #e8e0d8; 13 + --text-muted: #8a7f74; 14 + --text-dim: #584f47; 15 + 16 + --accent: #c9a96e; /* warm gold */ 17 + --accent-dim: rgba(201, 169, 110, 0.12); 18 + 19 + --font-serif: 'Lora', Georgia, 'Times New Roman', serif; 20 + --font-sans: 'DM Sans', system-ui, sans-serif; 21 + --font-mono: 'Monaco', 'Courier New', monospace; 22 + 23 + --radius: 10px; 24 + --transition: 0.2s ease; 25 + 26 + --max-w: 680px; 27 + } 28 + 29 + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 30 + 31 + html { scroll-behavior: smooth; } 32 + 33 + body { 34 + font-family: var(--font-sans); 35 + font-size: 16px; 36 + line-height: 1.7; 37 + color: var(--text); 38 + background-color: var(--bg); 39 + -webkit-font-smoothing: antialiased; 40 + } 41 + 42 + /* ── Typography ── */ 43 + 44 + h1, h2, h3 { 45 + font-family: var(--font-serif); 46 + font-weight: 400; 47 + line-height: 1.2; 48 + } 49 + 50 + a { 51 + color: var(--accent); 52 + text-decoration: none; 53 + transition: opacity var(--transition); 54 + } 55 + a:hover { opacity: 0.75; } 56 + 57 + code { 58 + font-family: var(--font-mono); 59 + font-size: 0.875em; 60 + background: var(--bg-raised); 61 + padding: 0.15em 0.4em; 62 + border-radius: 4px; 63 + } 64 + 65 + /* ── Header ── */ 66 + 67 + .site-header { 68 + position: sticky; 69 + top: 0; 70 + z-index: 100; 71 + background: rgba(14, 13, 12, 0.92); 72 + backdrop-filter: blur(12px); 73 + border-bottom: 1px solid var(--border); 74 + } 75 + 76 + .nav-container { 77 + max-width: var(--max-w); 78 + margin: 0 auto; 79 + padding: 0.875rem 1.5rem; 80 + display: flex; 81 + justify-content: space-between; 82 + align-items: center; 83 + } 84 + 85 + .nav-logo { 86 + font-family: var(--font-serif); 87 + font-style: italic; 88 + font-size: 1.25rem; 89 + color: var(--text); 90 + } 91 + .nav-logo:hover { opacity: 0.6; } 92 + 93 + .nav-menu { 94 + list-style: none; 95 + display: flex; 96 + gap: 1.5rem; 97 + } 98 + 99 + .nav-menu a { 100 + font-size: 0.875rem; 101 + color: var(--text-muted); 102 + letter-spacing: 0.01em; 103 + } 104 + .nav-menu a:hover { color: var(--text); opacity: 1; } 105 + 106 + /* ── Main ── */ 107 + 108 + .main-content { 109 + max-width: var(--max-w); 110 + margin: 0 auto; 111 + padding: 0 1.5rem; 112 + } 113 + 114 + /* ── Hero ── */ 115 + 116 + .hero { 117 + padding: 4rem 0 3rem; 118 + } 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 { 148 + opacity: 1; 149 + border-bottom-color: var(--accent); 150 + } 151 + 152 + .visitor-badge { 153 + display: inline-block; 154 + font-size: 0.8125rem; 155 + color: var(--text-muted); 156 + background: var(--bg-raised); 157 + border: 1px solid var(--border); 158 + padding: 0.25rem 0.875rem; 159 + border-radius: 999px; 160 + letter-spacing: 0.01em; 161 + } 162 + 163 + /* ── Sections ── */ 164 + 165 + .content-section { 166 + padding: 2.5rem 0; 167 + border-top: 1px solid var(--border); 168 + } 169 + 170 + .section-label { 171 + font-family: var(--font-sans); 172 + font-size: 0.75rem; 173 + font-weight: 500; 174 + text-transform: uppercase; 175 + letter-spacing: 0.1em; 176 + color: var(--text-dim); 177 + margin-bottom: 1.25rem; 178 + } 179 + 180 + /* ── Cards ── */ 181 + 182 + .card { 183 + background: var(--bg-card); 184 + border: 1px solid var(--border); 185 + border-radius: var(--radius); 186 + overflow: hidden; 187 + transition: border-color var(--transition); 188 + } 189 + .card:hover { border-color: var(--border-hover); } 190 + 191 + .card-inner { 192 + display: flex; 193 + align-items: flex-start; 194 + gap: 1rem; 195 + padding: 1.25rem; 196 + } 197 + 198 + .card-icon { 199 + font-size: 1.25rem; 200 + color: var(--accent); 201 + flex-shrink: 0; 202 + margin-top: 0.1rem; 203 + } 204 + 205 + .card-text { 206 + flex: 1; 207 + font-size: 0.9375rem; 208 + color: var(--text-muted); 209 + padding: 1.25rem; 210 + line-height: 1.75; 211 + } 212 + 213 + /* when card-text is inside card-inner, don't double-pad */ 214 + .card-inner .card-text { 215 + padding: 0; 216 + } 217 + 218 + .card-meta { 219 + font-size: 0.8125rem; 220 + color: var(--text-dim); 221 + padding: 0.75rem 1.25rem; 222 + border-top: 1px solid var(--border); 223 + } 224 + 225 + .card-text a { 226 + color: var(--accent); 227 + } 228 + 229 + .loading-text { 230 + color: var(--text-dim); 231 + font-style: italic; 232 + } 233 + 234 + /* Music card specifics */ 235 + .track-name { 236 + display: block; 237 + font-family: var(--font-serif); 238 + font-size: 1.0625rem; 239 + color: var(--text); 240 + margin-bottom: 0.2rem; 241 + } 242 + 243 + .track-artist { 244 + font-size: 0.875rem; 245 + color: var(--text-muted); 246 + } 247 + 248 + /* Post images */ 249 + #post-media img { 250 + width: 100%; 251 + height: auto; 252 + display: block; 253 + border-top: 1px solid var(--border); 254 + } 255 + 256 + /* ── Social list ── */ 257 + 258 + .social-list { 259 + list-style: none; 260 + display: flex; 261 + flex-direction: column; 262 + gap: 0.125rem; 263 + } 264 + 265 + .social-item { 266 + display: flex; 267 + align-items: center; 268 + gap: 0.875rem; 269 + padding: 0.875rem 1rem; 270 + border-radius: var(--radius); 271 + color: var(--text); 272 + transition: background var(--transition), color var(--transition); 273 + text-decoration: none; 274 + border: 1px solid transparent; 275 + } 276 + .social-item:hover { 277 + background: var(--bg-card); 278 + border-color: var(--border); 279 + opacity: 1; 280 + } 281 + 282 + .social-item i { 283 + font-size: 1.125rem; 284 + color: var(--accent); 285 + width: 1.5rem; 286 + text-align: center; 287 + flex-shrink: 0; 288 + } 289 + 290 + .social-item > span:first-of-type { 291 + font-size: 0.9375rem; 292 + font-weight: 500; 293 + color: var(--text); 294 + min-width: 5rem; 295 + } 296 + 297 + .social-handle { 298 + font-size: 0.8125rem; 299 + color: var(--text-muted); 300 + margin-left: auto; 301 + font-family: var(--font-mono); 302 + } 303 + 304 + /* ── Link grid ── */ 305 + 306 + .link-grid { 307 + display: grid; 308 + grid-template-columns: repeat(2, 1fr); 309 + gap: 0.75rem; 310 + } 311 + 312 + .grid-link { 313 + display: flex; 314 + flex-direction: column; 315 + gap: 0.25rem; 316 + padding: 1.125rem; 317 + background: var(--bg-card); 318 + border: 1px solid var(--border); 319 + border-radius: var(--radius); 320 + text-decoration: none; 321 + transition: border-color var(--transition), transform var(--transition); 322 + } 323 + .grid-link:hover { 324 + border-color: var(--accent); 325 + transform: translateY(-1px); 326 + opacity: 1; 327 + } 10 328 11 - body { 12 - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 13 - background-color: #121212; 14 - color: white; 15 - padding: 20px; 16 - line-height: 1.5; 17 - margin: auto; 18 - text-align: center; 19 - max-width: 600px; 20 - } 329 + .grid-link-title { 330 + font-family: var(--font-serif); 331 + font-size: 1rem; 332 + color: var(--text); 333 + line-height: 1.3; 334 + } 21 335 22 - /* --- CARD STYLING --- */ 23 - #music-status-card, #latest-post-card { 24 - background-color: var(--bsky-card-bg); 25 - color: var(--bsky-text-color); 26 - border-radius: 12px; 27 - padding: 16px; 28 - margin: 20px auto; 29 - display: none; 30 - box-shadow: 0 4px 6px rgba(0,0,0,0.3); 31 - max-width: 500px; 32 - border: 1px solid var(--bsky-border); 33 - text-align: center; 34 - } 336 + .grid-link-desc { 337 + font-size: 0.8125rem; 338 + color: var(--text-muted); 339 + line-height: 1.4; 340 + } 35 341 36 - .bsky-header { 37 - font-size: 0.75rem; 38 - text-transform: uppercase; 39 - letter-spacing: 0.05em; 40 - color: var(--bsky-subtext-color); 41 - margin-bottom: 12px; 42 - display: flex; 43 - justify-content: center; 44 - align-items: center; 45 - gap: 8px; 46 - } 342 + /* ── Footer ── */ 47 343 48 - .bsky-header a { 49 - color: var(--bsky-subtext-color); 50 - text-decoration: none; 51 - } 344 + .site-footer { 345 + max-width: var(--max-w); 346 + margin: 0 auto; 347 + padding: 2.5rem 1.5rem 3rem; 348 + border-top: 1px solid var(--border); 349 + font-size: 0.8125rem; 350 + color: var(--text-dim); 351 + line-height: 1.8; 352 + } 52 353 53 - .bsky-content-body { 54 - margin-bottom: 12px; 55 - word-wrap: break-word; 56 - text-align: center; 57 - } 354 + .site-footer a { color: var(--text-muted); } 355 + .site-footer a:hover { color: var(--text); opacity: 1; } 58 356 59 - /* --- POST IMAGE STYLING --- */ 60 - .post-image { 61 - width: 100%; 62 - border-radius: 8px; 63 - margin: 10px auto 0 auto; 64 - border: 1px solid var(--bsky-border); 65 - display: block; 66 - } 357 + .footer-tor { margin-top: 0.25rem; } 67 358 68 - .bsky-trackname { 69 - font-weight: 700; 70 - font-size: 1.1rem; 71 - display: block; 72 - color: var(--bsky-text-color); 73 - text-decoration: none; 74 - margin-bottom: 2px; 75 - } 359 + /* ── Responsive ── */ 76 360 77 - .bsky-artist { 78 - color: var(--bsky-subtext-color); 79 - font-size: 0.95rem; 80 - } 361 + @media (max-width: 600px) { 362 + .hero { padding: 2.5rem 0 2rem; } 81 363 82 - .bsky-footer { 83 - border-top: 1px solid var(--bsky-border); 84 - padding-top: 10px; 85 - margin-top: 12px; 86 - font-size: 0.8rem; 87 - color: var(--bsky-subtext-color); 88 - display: flex; 89 - align-items: center; 90 - justify-content: center; 91 - gap: 8px; 92 - } 364 + .link-grid { grid-template-columns: 1fr; } 365 + 366 + .social-handle { display: none; } 367 + 368 + .nav-menu { display: none; } 369 + } 370 + 371 + /* ── Section label "more" link ── */ 372 + 373 + .section-label { 374 + display: flex; 375 + align-items: baseline; 376 + gap: 0.75rem; 377 + } 378 + 379 + .section-more { 380 + font-size: 0.75rem; 381 + color: var(--text-dim); 382 + font-weight: 400; 383 + letter-spacing: 0.02em; 384 + text-transform: none; 385 + border-bottom: 1px solid transparent; 386 + transition: color var(--transition), border-color var(--transition); 387 + } 388 + .section-more:hover { 389 + color: var(--accent); 390 + border-bottom-color: var(--accent); 391 + opacity: 1; 392 + } 393 + 394 + /* ── Home photo strip ── */ 93 395 94 - a { color: var(--bsky-link-color); } 95 - 96 - .error-msg { 97 - color: #ff6b6b; 98 - font-size: 0.9rem; 99 - } 100 - 101 - hr { 102 - border: none; 103 - border-top: 1px solid var(--bsky-border); 104 - margin: 20px 0; 105 - } 396 + .photo-strip { 397 + display: grid; 398 + grid-template-columns: repeat(4, 1fr); 399 + gap: 0.5rem; 400 + } 106 401 107 - /* Social button row */ 108 - .social-row { 109 - display: flex; 110 - gap: 10px; 111 - flex-wrap: wrap; 112 - margin: 12px 0 20px 0; 113 - justify-content: center; 114 - } 402 + .photo-strip-item { 403 + aspect-ratio: 1 / 1; 404 + overflow: hidden; 405 + border-radius: 6px; 406 + border: 1px solid var(--border); 407 + cursor: pointer; 408 + background: var(--bg-card); 409 + transition: border-color var(--transition), transform var(--transition); 410 + } 411 + .photo-strip-item:hover { 412 + border-color: var(--accent); 413 + transform: scale(1.02); 414 + } 115 415 116 - .social-btn { 117 - display: inline-flex; 118 - align-items: center; 119 - justify-content: center; 120 - gap: 8px; 121 - padding: 8px 14px; /* keep expanded size by default */ 122 - border-radius: 999px; 123 - background: rgba(255,255,255,0.04); 124 - color: var(--bsky-text-color); 125 - text-decoration: none; 126 - border: 1px solid var(--bsky-border); 127 - font-size: 0.95rem; 128 - flex: 0 0 auto; /* don't stretch; allow pushing */ 129 - min-width: 44px; 130 - transition: all 0.2s ease; 131 - } 132 - 133 - .social-btn:hover { 134 - background: var(--btn-bg); 135 - border-color: var(--btn-border); 136 - transform: translateY(-2px); 137 - box-shadow: 0 4px 12px rgba(0,0,0,0.3); 138 - } 416 + .photo-strip-item img { 417 + width: 100%; 418 + height: 100%; 419 + object-fit: cover; 420 + display: block; 421 + transition: opacity var(--transition); 422 + } 139 423 140 - .social-btn i { 141 - width: 1.1em; 142 - text-align: center; 143 - font-size: 1.05em; 144 - } 424 + /* ── Photo modal ── */ 145 425 146 - /* label hidden by default, revealed on hover */ 147 - .social-btn .label { 148 - display: inline-block; 149 - /* always visible (no hover reveal) */ 150 - opacity: 1; 151 - transform: translateX(0); 152 - margin-left: 8px; 153 - white-space: nowrap; 154 - color: inherit; 155 - font-weight: 600; 156 - font-size: 0.95rem; 157 - max-width: 180px; 158 - overflow: hidden; 159 - vertical-align: middle; 160 - } 426 + .photo-modal { 427 + position: fixed; 428 + inset: 0; 429 + z-index: 200; 430 + background: rgba(10, 9, 8, 0.92); 431 + backdrop-filter: blur(8px); 432 + display: flex; 433 + align-items: center; 434 + justify-content: center; 435 + padding: 1.5rem; 436 + opacity: 0; 437 + pointer-events: none; 438 + transition: opacity 0.2s ease; 439 + } 440 + .photo-modal.active { 441 + opacity: 1; 442 + pointer-events: all; 443 + } 161 444 162 - /* hover styles removed - buttons are static */ 445 + .modal-img { 446 + max-width: 100%; 447 + max-height: 90vh; 448 + border-radius: 8px; 449 + object-fit: contain; 450 + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6); 451 + } 163 452 164 - /* brand color variables per button */ 165 - .btn-bluesky { --btn-bg: #0077ff; --btn-border: rgba(0,119,255,0.6); } 166 - .btn-instagram { --btn-bg: #E1306C; --btn-border: rgba(225,48,108,0.5); } 167 - .btn-tangled { --btn-bg: #000000; --btn-border: rgba(107,114,128,0.4); } 168 - .btn-threads { --btn-bg: #000000; --btn-border: rgba(17,24,39,0.4); } 169 - .btn-mastodon { --btn-bg: #6364FF; --btn-border: rgba(48,136,212,0.4); } 453 + .modal-close { 454 + position: absolute; 455 + top: 1.25rem; 456 + right: 1.25rem; 457 + background: var(--bg-card); 458 + border: 1px solid var(--border); 459 + border-radius: 50%; 460 + width: 2.25rem; 461 + height: 2.25rem; 462 + display: flex; 463 + align-items: center; 464 + justify-content: center; 465 + color: var(--text-muted); 466 + font-size: 0.875rem; 467 + cursor: pointer; 468 + transition: border-color var(--transition), color var(--transition); 469 + } 470 + .modal-close:hover { 471 + border-color: var(--accent); 472 + color: var(--accent); 473 + } 170 474 171 - /* Screen-reader only text (keep for accessibility) */ 172 - .sr-only { 173 - position: absolute !important; 174 - height: 1px; width: 1px; 175 - overflow: hidden; 176 - clip: rect(1px, 1px, 1px, 1px); 177 - white-space: nowrap; 178 - } 475 + @media (max-width: 600px) { 476 + .photo-strip { 477 + grid-template-columns: repeat(3, 1fr); 478 + } 479 + }
css/style.css

This is a binary file and will not be displayed.

favicon.ico

This is a binary file and will not be displayed.

-59
followonbsky.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" /> 6 - <title>Follow on Bluesky</title> 7 - <meta name="description" content="Quick profile card to open an AT Protocol (Bluesky) profile in multiple clients." /> 8 - <meta name="robots" content="index, follow" /> 9 - <link rel="stylesheet" href="/css/bsky.css" /> 10 - </head> 11 - <body> 12 - <main class="card" role="main" aria-labelledby="displayName"> 13 - <section class="profile" aria-live="polite"> 14 - <img id="avatar" class="avatar" src="" alt="Profile avatar" /> 15 - <h2 id="displayName" class="name">Loading…</h2> 16 - <div id="tagline" class="tagline"></div> 17 - </section> 18 - 19 - <nav id="links" class="links" aria-label="Open in client"></nav> 20 - <div id="more-clients" class="links"></div> 21 - <button id="show-more" class="show-more-btn" style="display:none;">Show More Clients</button> 22 - 23 - <section class="controls" aria-label="Actions"> 24 - <div class="control-row"> 25 - <input id="try-handle" type="text" placeholder="yourhandle.com or did:plc:..." aria-label="Try with your profile" /> 26 - <button id="try-button" type="button">Try</button> 27 - </div> 28 - <div class="control-row"> 29 - <input id="custom-client" type="url" placeholder="https://main.bsky.dev" aria-label="Custom client base URL" /> 30 - <button id="custom-open" type="button">Open</button> 31 - </div> 32 - </section> 33 - 34 - <p class="meta"> 35 - by <a href="?did=did:plc:l37td5yhxl2irrzrgvei4qay">@madebydanny.uk</a>, 36 - add a client <a href="https://tally.so/r/nrzQDl" target="_blank" rel="noopener">here</a> 37 - </p> 38 - <!-- Default Statcounter code for Made by Danny UK 39 - https://madebydanny.uk --> 40 - <script type="text/javascript"> 41 - var sc_project=13180172; 42 - var sc_invisible=0; 43 - var sc_security="a4ed014f"; 44 - var scJsHost = "https://"; 45 - document.write("<sc"+"ript type='text/javascript' src='" + 46 - scJsHost+ 47 - "statcounter.com/counter/counter.js'></"+"script>"); 48 - </script> 49 - <noscript><div class="statcounter"><a title="Web Analytics 50 - Made Easy - Statcounter" href="https://statcounter.com/" 51 - target="_blank"><img class="statcounter" 52 - src="https://c.statcounter.com/13180172/0/a4ed014f/0/" 53 - alt="Web Analytics Made Easy - Statcounter" 54 - referrerPolicy="no-referrer-when-downgrade"></a></div></noscript> 55 - <!-- End of Statcounter Code --> 56 - </main> 57 - <script src="/js/bsky.js"></script> 58 - </body> 59 - </html>
+182 -91
index.html
··· 3 3 <head> 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 - <title>Daniel Morrisey - Web Developer & Content Creator | madebydanny.uk</title> 7 - <meta http-equiv="onion-location" content="http://irgwdhat74pqcpkk7ynrphvohnnt574yvwmhredrfusemgu6wj2ik5id.onion/" /> 6 + <title>Daniel Morrisey — madebydanny.uk</title> 7 + <meta name="description" content="Web developer, musician, and Bluesky enthusiast. Explore my work, music, and creative projects."> 8 + <meta name="keywords" content="Daniel Morrisey, web developer, Bluesky, music, portfolio"> 9 + <meta name="author" content="Daniel Morrisey"> 10 + <meta name="robots" content="index, follow"> 11 + <meta name="language" content="English"> 12 + 13 + <!-- Open Graph --> 8 14 <meta property="og:url" content="https://madebydanny.uk/"> 15 + <meta property="og:type" content="website"> 16 + <meta property="og:title" content="Daniel Morrisey — madebydanny.uk"> 17 + <meta property="og:description" content="Web developer, musician, and Bluesky enthusiast"> 9 18 <meta property="og:image" content="https://cdn.madebydanny.uk/user-content/2026-03-20/07c24dfa-7037-4409-8774-a8235c350a0e.png"> 10 19 <meta property="og:site_name" content="madebydanny.uk"> 11 20 <meta property="og:locale" content="en_US"> 12 - 21 + 13 22 <!-- Twitter Card --> 14 23 <meta name="twitter:card" content="summary_large_image"> 15 - <meta name="twitter:title" content="Daniel Morrisey - Web Developer & Content Creator"> 24 + <meta name="twitter:title" content="Daniel Morrisey — madebydanny.uk"> 16 25 <meta name="twitter:description" content="Web developer, musician, and Bluesky enthusiast"> 17 26 <meta name="twitter:image" content="https://cdn.madebydanny.uk/user-content/2026-03-20/07c24dfa-7037-4409-8774-a8235c350a0e.png"> 18 - 19 - <!-- Canonical URL --> 27 + 20 28 <link rel="canonical" href="https://madebydanny.uk/"> 21 - 29 + 22 30 <!-- Favicons --> 23 - <script src="https://kit.fontawesome.com/0ca27f8db1.js" crossorigin="anonymous"></script> 24 - <link rel="icon" type="image/png" href="https://imrs.madebydanny.uk/?url=https://cloudflareisawesome.madebydanny.uk/madebydanny.uk/seo/favicon.webp"> 25 - <link rel="apple-touch-icon" href="https://imrs.madebydanny.uk/?url=https://cloudflareisawesome.madebydanny.uk/madebydanny.uk/seo/favicon.webp"> 26 - 27 - <!-- Performance: Preconnect & DNS Prefetch --> 31 + <link rel="icon" type="image/png" href="https://cdn.blueat.net/img/avatar/plain/did:plc:l37td5yhxl2irrzrgvei4qay/bafkreidfielr2nk5xr4v2odm5bth5yjk5y536ex5qpxsheq2ocl2qobwt4"> 32 + <link rel="apple-touch-icon" href="https://cdn.blueat.net/img/avatar/plain/did:plc:l37td5yhxl2irrzrgvei4qay/bafkreidfielr2nk5xr4v2odm5bth5yjk5y536ex5qpxsheq2ocl2qobwt4"> 33 + 34 + <!-- Performance --> 28 35 <link rel="preconnect" href="https://kit.fontawesome.com"> 29 36 <link rel="preconnect" href="https://cdn.madebydanny.uk"> 30 - <link rel="preconnect" href="https://public-cdn.madebydanny.uk"> 31 37 <link rel="dns-prefetch" href="https://api.bsky.app"> 32 - <link rel="dns-prefetch" href="https://selfhosted.social"> 33 - 38 + <link rel="preconnect" href="https://fonts.googleapis.com"> 39 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 40 + <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"> 41 + 42 + <!-- Font Awesome --> 43 + <script src="https://kit.fontawesome.com/0ca27f8db1.js" crossorigin="anonymous"></script> 44 + 45 + <!-- Stylesheet --> 34 46 <link rel="stylesheet" href="/css/index.css"> 35 - 36 - <!-- JSON-LD Structured Data --> 47 + 48 + <!-- JSON-LD --> 37 49 <script type="application/ld+json"> 38 50 { 39 51 "@context": "https://schema.org", ··· 47 59 "https://threads.net/@madebydanny.uk", 48 60 "https://mastodon.social/@danielmorrisey" 49 61 ], 50 - "jobTitle": "Web Developer", 51 - "knowsAbout": ["Web Development", "JavaScript", "Music Production", "Content Creation"] 62 + "jobTitle": "Web Developer" 52 63 } 53 64 </script> 54 - <meta name="description" content="Daniel Morrisey's personal website. Web developer, musician, and Bluesky enthusiast. Explore my projects, music, and more."> 55 - <meta name="keywords" content="Daniel Morrisey, developer, web development, Bluesky, music, portfolio"> 56 - <meta name="author" content="Daniel Morrisey"> 57 - <meta name="robots" content="index, follow"> 58 - <meta name="language" content="English"> 59 - <html lang="en"> 60 - <head> 61 - <meta charset="UTF-8"> 62 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 63 - <title>Daniel Morrisey  - madebydanny.uk</title> 64 - <script src="https://kit.fontawesome.com/0ca27f8db1.js" crossorigin="anonymous"></script> 65 - <link rel="icon" id="favicon" href="https://imrs.madebydanny.uk/?url=https://cloudflareisawesome.madebydanny.uk/madebydanny.uk/seo/favicon.webp"> 66 - <meta name="description" id="meta-description" content="Posting everything into the endless jet stream of posts"> 67 - <meta property="og:title" content="Daniel Morrisey  - madebydanny.uk"> 68 - <meta property="og:description" id="og-description" content="Posting everything into the endless jet stream of posts"> 69 - <meta property="og:type" content="website"> 65 + 70 66 <meta http-equiv="onion-location" content="http://irgwdhat74pqcpkk7ynrphvohnnt574yvwmhredrfusemgu6wj2ik5id.onion/" /> 71 - <meta property="og:url" content="https://madebydanny.uk/"> 72 - <meta property="og:image" id="og-image" content="https://cdn.madebydanny.uk/user-content/2026-03-20/07c24dfa-7037-4409-8774-a8235c350a0e.png"> 73 - <link rel="stylesheet" href="/css/index.css"> 67 + <script defer src="https://cloud.umami.is/script.js" data-website-id="87c0069b-6d7e-4e4b-85df-3608171d9562"></script> 74 68 </head> 75 69 76 70 <body> 77 - <h1>Hi, I'm Daniel Morrisey </h1> 78 - <div id="visitor-counter">Loading visitor info...</div> 79 - <div id="total-visitors" style="font-size: 0.8em; color: gray; text-align: center;"> 80 - Calculating total visits... 81 - </div> 82 - <p><a href="https://guestbook.madebydanny.uk/danny">Guestbook</a> ~ <a href="https://pdsls.dev/at://did:plc:l37td5yhxl2irrzrgvei4qay/fm.teal.alpha.feed.play">Recently played Music</a> ~ <a href="/about.html">About Me</a> ~ <a href="https://microblog.madebydanny.uk">Microblog</a> ~ <a href="/photos.html">Photos</a> ~ <a href="/cdn/index.html">CDN</a></p> 83 - <p>I like to listen to Music <i>(Mainly Tate McRae and Taylor Swift)</i>, and post on Bluesky<br>I'm also on <a href="https://threads.net/@madebydanny.uk" target="_blank">Threads</a> and <a href="ttps://mastodon.social/@danielmorrisey" target="_blank">Mastodon</a>, but active on <a href="https://bsky.app/profile/did:plc:l37td5yhxl2irrzrgvei4qay" target="_blank">Bluesky</a>, becuase it's the best social media platform</p> 84 - <div id="music-status-card"> 85 - <div class="bsky-header"> 86 - <span>Recently played</span> 87 - <span id="music-link-icon"></span> 88 - </div> 89 - <div id="music-ui-content" class="bsky-content-body"></div> 90 - <div class="bsky-footer"> 91 - <span>teal.fm via piper</span> 92 - </div> 93 - </div> 71 + 72 + <!-- Header --> 73 + <header class="site-header"> 74 + <nav class="nav-container"> 75 + <a href="/" class="nav-logo">Daniel Morrisey <i>.com</i></a> 76 + <ul class="nav-menu"> 77 + <li><a href="/about.html">About</a></li> 78 + <li><a href="/photos.html">Photos</a></li> 79 + <li><a href="/cdn.html">CDN</a></li> 80 + <li><a href="https://danielmorrisey.com">Blog</a></li> 81 + <li><a href="https://blueat.net" target="_blank">BlueAT Network</a></li> 82 + <li><a href="https://bsky.app/profile/danielmorrisey.com" target="_blank"><i class="fa-brands fa-bluesky" aria-hidden="true"></i></a></li> 83 + </ul> 84 + </nav> 85 + </header> 86 + 87 + <main class="main-content"> 88 + 89 + <!-- Hero --> 90 + <section class="hero"> 91 + <p class="hero-eyebrow">hi, I'm</p> 92 + <h1 class="hero-title">Daniel Morrisey</h1> 93 + <p class="hero-bio"> 94 + I build things for the AT Protocol, myself, and spend too much time on <a href="https://bsky.app/profile/danielmorrisey.com" target="_blank">Bluesky</a>. 95 + I run the <a href="https://blueat.net" target="_blank">BlueAT Network</a> — a simple Bluesky PDS made for the everyday user. When I'm not doing that, 96 + I'm probably listening to music. 97 + </p> 98 + <div id="visitor-counter" class="visitor-badge">Loading visitor info…</div> 99 + </section> 100 + 101 + <!-- Music --> 102 + <section class="content-section"> 103 + <h2 class="section-label">now playing</h2> 104 + <div id="music-card" class="card"> 105 + <div class="card-inner"> 106 + <span class="card-icon">♪</span> 107 + <div id="music-content" class="card-text"> 108 + <span class="loading-text">fetching last played…</span> 109 + </div> 110 + </div> 111 + <div class="card-meta">via teal.fm &amp; piper</div> 112 + </div> 113 + </section> 114 + 115 + <!-- Latest post --> 116 + <section class="content-section"> 117 + <h2 class="section-label">latest from bluesky</h2> 118 + <div id="post-card" class="card"> 119 + <div id="post-content" class="card-text"> 120 + <span class="loading-text">loading post…</span> 121 + </div> 122 + <div id="post-media"></div> 123 + <div class="card-meta" id="post-date"></div> 124 + </div> 125 + </section> 126 + 127 + <!-- Photos --> 128 + <section class="content-section"> 129 + <h2 class="section-label"> 130 + recent photos 131 + <a href="/photos.html" class="section-more">all photos →</a> 132 + </h2> 133 + <div id="home-photo-strip" class="photo-strip"></div> 134 + </section> 135 + 136 + <!-- Find me --> 137 + <section class="content-section"> 138 + <h2 class="section-label">find me</h2> 139 + <ul class="social-list" role="list"> 140 + <li> 141 + <a href="https://bsky.app/profile/did:plc:l37td5yhxl2irrzrgvei4qay" target="_blank" rel="noopener noreferrer" class="social-item"> 142 + <i class="fa-brands fa-bluesky" aria-hidden="true"></i> 143 + <span>Bluesky</span> 144 + <span class="social-handle">@danielmorrisey.com</span> 145 + </a> 146 + </li> 147 + <li> 148 + <a href="https://tangled.org/did:plc:l37td5yhxl2irrzrgvei4qay" target="_blank" rel="noopener noreferrer" class="social-item"> 149 + <i class="fa-brands fa-git-alt" aria-hidden="true"></i> 150 + <span>Tangled</span> 151 + <span class="social-handle">danielmorrisey.com</span> 152 + </a> 153 + </li> 154 + <li> 155 + <a href="https://threads.net/@madebydanny.uk" target="_blank" rel="noopener noreferrer" class="social-item"> 156 + <i class="fa-brands fa-threads" aria-hidden="true"></i> 157 + <span>Threads</span> 158 + <span class="social-handle">@madebydanny.uk</span> 159 + </a> 160 + </li> 161 + <li> 162 + <a href="https://mastodon.social/@danielmorrisey" target="_blank" rel="noopener noreferrer" class="social-item"> 163 + <i class="fa-brands fa-mastodon" aria-hidden="true"></i> 164 + <span>Mastodon</span> 165 + <span class="social-handle">@danielmorrisey@mastodon.social</span> 166 + </a> 167 + </li> 168 + </ul> 169 + </section> 94 170 95 - <div id="latest-post-card"> 96 - <div class="bsky-header"> 97 - <span>Current Status</span> 98 - <span id="post-link-icon"></span> 99 - </div> 100 - <div id="post-content" class="bsky-content-body"> 101 - Loading current status... 102 - </div> 103 - <div id="post-media"></div> 104 - <div class="bsky-footer" id="post-date"></div> 171 + <!-- Explore --> 172 + <section class="content-section"> 173 + <h2 class="section-label">explore</h2> 174 + <div class="link-grid"> 175 + <a href="https://blueat.net" target="_blank" class="grid-link"> 176 + <span class="grid-link-title">BlueAT Network</span> 177 + <span class="grid-link-desc">A simple ATProto PDS made for the everyday user</span> 178 + </a> 179 + <a href="/cdn/index.html" class="grid-link"> 180 + <span class="grid-link-title">MBD CDN</span> 181 + <span class="grid-link-desc">My simple, lightweight, and fast CDN anyone can use</span> 182 + </a> 183 + <a href="https://danielmorrisey.com" class="grid-link"> 184 + <span class="grid-link-title">Blog</span> 185 + <span class="grid-link-desc">My personal blog hosted on the AT Protocol</span> 186 + </a> 187 + <a href="https://blueat.net/tweet2bsky" class="grid-link"> 188 + <span class="grid-link-title">Tweets 2 Bsky</span> 189 + <span class="grid-link-desc">Mirror X accounts to Bluesky</span> 190 + </a> 191 + </div> 192 + </section> 193 + 194 + </main> 195 + 196 + <!-- Footer --> 197 + <footer class="site-footer"> 198 + <p>© 2024–26 Daniel Morrisey · <a href="https://aturi.to/did:plc:l37td5yhxl2irrzrgvei4qay" target="_blank" rel="noopener noreferrer">@madebydanny.uk</a> · hosted on <a href="https://wisp.place" target="_blank" rel="noopener noreferrer">wisp.place</a></p> 199 + <p class="footer-tor"><a href="http://irgwdhat74pqcpkk7ynrphvohnnt574yvwmhredrfusemgu6wj2ik5id.onion/" target="_blank" rel="noopener noreferrer">Open in Tor</a></p> 200 + </footer> 201 + 202 + <!-- Photo modal --> 203 + <div id="photo-modal" class="photo-modal" role="dialog" aria-modal="true" aria-label="Photo viewer"> 204 + <button id="modal-close" class="modal-close" aria-label="Close">✕</button> 205 + <img id="modal-photo" src="" alt="" class="modal-img"> 105 206 </div> 106 - <hr> 107 - <p><b>Social Links:</b></p> 108 - <div id="social-links"></div> 109 - <hr> 110 - <div id="site-footer"></div> 111 - <script src="/js/script.js"></script> 112 - <script src="/js/social-links.js"></script> 113 - <script src="https://visit-counter.madebydanny.uk"></script> 114 - <!-- Default Statcounter code for Made by Danny UK 115 - https://madebydanny.uk --> 116 - <script type="text/javascript"> 117 - var sc_project=13180172; 118 - var sc_invisible=0; 119 - var sc_security="a4ed014f"; 120 - var scJsHost = "https://"; 121 - document.write("<sc"+"ript type='text/javascript' src='" + 122 - scJsHost+ 123 - "statcounter.com/counter/counter.js'></"+"script>"); 124 - </script> 125 - <noscript><div class="statcounter"><a title="Web Analytics" 126 - href="https://statcounter.com/" target="_blank"><img 127 - class="statcounter" 128 - src="https://c.statcounter.com/13180172/0/a4ed014f/0/" 129 - alt="Web Analytics" 130 - referrerPolicy="no-referrer-when-downgrade"></a></div></noscript> 131 - <!-- End of Statcounter Code --> 207 + 208 + <script src="/js/script.js"></script> 209 + <script src="/js/photos.js"></script> 210 + <script src="https://visit-counter.madebydanny.uk"></script> 211 + 212 + <!-- Statcounter --> 213 + <script type="text/javascript"> 214 + var sc_project = 13180172; 215 + var sc_invisible = 1; 216 + var sc_security = "a4ed014f"; 217 + document.write("<sc" + "ript type='text/javascript' src='https://statcounter.com/counter/counter.js'></" + "script>"); 218 + </script> 219 + <noscript> 220 + <img src="https://c.statcounter.com/13180172/0/a4ed014f/1/" alt="" referrerpolicy="no-referrer-when-downgrade" style="display:none"> 221 + </noscript> 222 + 132 223 </body> 133 224 </html>
-147
js/music.js
··· 1 - (() => { 2 - const MUSIC_DID = 'did:plc:l37td5yhxl2irrzrgvei4qay'; 3 - const PDS_BASE = 'https://selfhosted.social'; 4 - 5 - const statusEl = document.getElementById('recent-status'); 6 - const listEl = document.getElementById('recent-list'); 7 - 8 - function setStatus(text) { 9 - if (statusEl) statusEl.textContent = text || ''; 10 - } 11 - 12 - function formatDate(iso) { 13 - if (!iso) return ''; 14 - try { 15 - const d = new Date(iso); 16 - return d.toLocaleString(undefined, { 17 - year: 'numeric', 18 - month: 'short', 19 - day: 'numeric', 20 - hour: '2-digit', 21 - minute: '2-digit' 22 - }); 23 - } catch { 24 - return iso; 25 - } 26 - } 27 - 28 - function buildArtworkUrl(play) { 29 - if (!play) return ''; 30 - // Prefer explicit artwork fields if they exist, else fall back to proxying the origin URL 31 - const fromField = 32 - play.artworkUrl || 33 - play.artworkUrl100 || 34 - (play.albumArt && play.albumArt.url); 35 - 36 - if (fromField) return fromField; 37 - 38 - if (play.originUrl) { 39 - return 'https://imrs.madebydanny.uk/?url=' + encodeURIComponent(play.originUrl); 40 - } 41 - return ''; 42 - } 43 - 44 - function renderPlays(plays) { 45 - if (!listEl) return; 46 - listEl.innerHTML = ''; 47 - 48 - if (!plays || !plays.length) { 49 - setStatus('No recent plays found.'); 50 - return; 51 - } 52 - 53 - setStatus(''); 54 - 55 - plays.forEach((rec) => { 56 - const value = rec.value || rec; // support both raw and listRecords-style 57 - const artistsArr = Array.isArray(value.artists) ? value.artists : []; 58 - const artists = artistsArr.map((a) => a.artistName || a.name).filter(Boolean).join(', '); 59 - const title = value.trackName || 'Unknown track'; 60 - const originUrl = value.originUrl || '#'; 61 - const when = formatDate(value.playedTime || rec.indexedAt); 62 - const artwork = buildArtworkUrl(value); 63 - 64 - const item = document.createElement('div'); 65 - item.className = 'recent-item'; 66 - 67 - if (artwork) { 68 - const img = document.createElement('img'); 69 - img.className = 'recent-artwork'; 70 - img.src = artwork; 71 - img.alt = `${title} artwork`; 72 - img.loading = 'lazy'; 73 - item.appendChild(img); 74 - } 75 - 76 - const meta = document.createElement('div'); 77 - meta.className = 'recent-meta'; 78 - 79 - const titleEl = document.createElement('div'); 80 - titleEl.className = 'recent-title'; 81 - if (originUrl && originUrl !== '#') { 82 - const link = document.createElement('a'); 83 - link.href = originUrl; 84 - link.target = '_blank'; 85 - link.rel = 'noopener'; 86 - link.textContent = title; 87 - titleEl.appendChild(link); 88 - } else { 89 - titleEl.textContent = title; 90 - } 91 - 92 - const artistEl = document.createElement('div'); 93 - artistEl.className = 'recent-artist'; 94 - artistEl.textContent = artists || 'Unknown artist'; 95 - 96 - const timeEl = document.createElement('div'); 97 - timeEl.className = 'recent-time'; 98 - timeEl.textContent = when; 99 - 100 - meta.appendChild(titleEl); 101 - meta.appendChild(artistEl); 102 - meta.appendChild(timeEl); 103 - 104 - item.appendChild(meta); 105 - listEl.appendChild(item); 106 - }); 107 - } 108 - 109 - async function fetchRecentPlays() { 110 - setStatus('Loading recent plays…'); 111 - 112 - try { 113 - const url = 114 - PDS_BASE + 115 - '/xrpc/com.atproto.repo.listRecords' + 116 - '?repo=' + 117 - encodeURIComponent(MUSIC_DID) + 118 - '&collection=' + 119 - encodeURIComponent('fm.teal.alpha.feed.play') + 120 - '&limit=50' + 121 - '&reverse=true'; 122 - 123 - const res = await fetch(url); 124 - if (!res.ok) { 125 - throw new Error('HTTP ' + res.status); 126 - } 127 - 128 - const data = await res.json(); 129 - const records = Array.isArray(data.records) ? data.records : []; 130 - 131 - // Sort by playedTime descending if present 132 - records.sort((a, b) => { 133 - const av = (a.value && a.value.playedTime) || a.indexedAt || ''; 134 - const bv = (b.value && b.value.playedTime) || b.indexedAt || ''; 135 - return (bv > av) ? 1 : (bv < av ? -1 : 0); 136 - }); 137 - 138 - renderPlays(records); 139 - } catch (err) { 140 - console.warn('Error loading recent plays', err); 141 - setStatus('Error loading recent plays. Please try again later.'); 142 - } 143 - } 144 - 145 - document.addEventListener('DOMContentLoaded', fetchRecentPlays); 146 - })(); 147 -
+105 -90
js/photos.js
··· 1 - // Photos Gallery Manager 1 + // ── Photo data ──────────────────────────────────────────────────────────────── 2 + 2 3 const PHOTOS = [ 3 - // Add your photos here in this format: 4 - // { 5 - // src: '/path/to/image.jpg', 6 - // title: 'Photo Title', 7 - // date: '2024-01-15' 8 - // } 9 4 { 10 5 src: 'https://cloudflareisawesome.madebydanny.uk/photos-upload-1/20240622_235041500_iOS.jpg', 11 6 title: 'DSC_3811.jpg', ··· 32 27 date: '2025-10-14' 33 28 }, 34 29 { 35 - src: `https://cloudflareisawesome.madebydanny.uk/photos-upload-1/DSC_3692.jpg`, 30 + src: 'https://cloudflareisawesome.madebydanny.uk/photos-upload-1/DSC_3692.jpg', 36 31 title: 'DSC_3692.jpg', 37 32 date: '2025-10-14' 38 33 }, ··· 43 38 } 44 39 ]; 45 40 46 - function renderPhotosGallery() { 47 - const gallery = document.getElementById('photos-gallery'); 48 - const emptyState = document.getElementById('empty-photos'); 41 + // ── Modal ───────────────────────────────────────────────────────────────────── 49 42 50 - if (!gallery) return; 43 + function openPhotoModal(src, alt) { 44 + const modal = document.getElementById('photo-modal'); 45 + const modalImg = document.getElementById('modal-photo'); 46 + if (!modal || !modalImg) return; 47 + modalImg.src = src; 48 + modalImg.alt = alt; 49 + modal.classList.add('active'); 50 + document.body.style.overflow = 'hidden'; 51 + } 51 52 52 - gallery.innerHTML = ''; 53 + function closePhotoModal() { 54 + const modal = document.getElementById('photo-modal'); 55 + if (!modal) return; 56 + modal.classList.remove('active'); 57 + document.body.style.overflow = ''; 58 + } 53 59 54 - if (PHOTOS.length === 0) { 55 - emptyState.style.display = 'block'; 56 - return; 57 - } 60 + // ── Home strip (4 most recent photos) ──────────────────────────────────────── 58 61 59 - emptyState.style.display = 'none'; 62 + function renderHomePhotos() { 63 + const strip = document.getElementById('home-photo-strip'); 64 + if (!strip) return; 60 65 61 - PHOTOS.forEach((photo, index) => { 62 - const photoItem = document.createElement('div'); 63 - photoItem.className = 'photo-item'; 64 - photoItem.role = 'button'; 65 - photoItem.tabIndex = 0; 66 - photoItem.setAttribute('aria-label', `View photo: ${photo.title}`); 66 + const recent = PHOTOS.slice(0, 4); 67 + strip.innerHTML = ''; 67 68 68 - const img = document.createElement('img'); 69 - img.src = photo.src; 70 - img.alt = photo.title; 71 - img.loading = 'lazy'; 69 + recent.forEach(photo => { 70 + const item = document.createElement('div'); 71 + item.className = 'photo-strip-item'; 72 + item.setAttribute('role', 'button'); 73 + item.setAttribute('tabindex', '0'); 74 + item.setAttribute('aria-label', `View photo: ${photo.title}`); 72 75 73 - const info = document.createElement('div'); 74 - info.className = 'photo-info'; 76 + const img = document.createElement('img'); 77 + img.src = photo.src; 78 + img.alt = photo.title; 79 + img.loading = 'lazy'; 75 80 76 - const title = document.createElement('h3'); 77 - title.className = 'photo-title'; 78 - title.textContent = photo.title; 81 + item.appendChild(img); 82 + item.addEventListener('click', () => openPhotoModal(photo.src, photo.title)); 83 + item.addEventListener('keydown', (e) => { 84 + if (e.key === 'Enter' || e.key === ' ') openPhotoModal(photo.src, photo.title); 85 + }); 79 86 80 - const date = document.createElement('p'); 81 - date.className = 'photo-date'; 82 - date.textContent = new Date(photo.date).toLocaleDateString('en-US', { 83 - year: 'numeric', 84 - month: 'short', 85 - day: 'numeric' 87 + strip.appendChild(item); 86 88 }); 89 + } 87 90 88 - info.appendChild(title); 89 - info.appendChild(date); 90 - photoItem.appendChild(img); 91 - photoItem.appendChild(info); 91 + // ── Full gallery (photos.html) ──────────────────────────────────────────────── 92 + 93 + function renderPhotosGallery() { 94 + const gallery = document.getElementById('photos-gallery'); 95 + const emptyState = document.getElementById('empty-photos'); 96 + if (!gallery) return; 97 + 98 + gallery.innerHTML = ''; 99 + 100 + if (PHOTOS.length === 0) { 101 + if (emptyState) emptyState.style.display = 'block'; 102 + return; 103 + } 104 + if (emptyState) emptyState.style.display = 'none'; 105 + 106 + PHOTOS.forEach(photo => { 107 + const item = document.createElement('div'); 108 + item.className = 'photo-item'; 109 + item.setAttribute('role', 'button'); 110 + item.setAttribute('tabindex', '0'); 111 + item.setAttribute('aria-label', `View photo: ${photo.title}`); 112 + 113 + const img = document.createElement('img'); 114 + img.src = photo.src; 115 + img.alt = photo.title; 116 + img.loading = 'lazy'; 117 + 118 + const info = document.createElement('div'); 119 + info.className = 'photo-info'; 120 + 121 + const title = document.createElement('h3'); 122 + title.className = 'photo-title'; 123 + title.textContent = photo.title; 92 124 93 - photoItem.addEventListener('click', () => openPhotoModal(photo.src, photo.title)); 94 - photoItem.addEventListener('keydown', (e) => { 95 - if (e.key === 'Enter' || e.key === ' ') { 96 - openPhotoModal(photo.src, photo.title); 97 - } 98 - }); 125 + const date = document.createElement('p'); 126 + date.className = 'photo-date'; 127 + date.textContent = new Date(photo.date).toLocaleDateString('en-GB', { 128 + year: 'numeric', month: 'short', day: 'numeric' 129 + }); 99 130 100 - gallery.appendChild(photoItem); 101 - }); 102 - } 131 + info.appendChild(title); 132 + info.appendChild(date); 133 + item.appendChild(img); 134 + item.appendChild(info); 103 135 104 - function openPhotoModal(src, alt) { 105 - const modal = document.getElementById('photo-modal'); 106 - const modalImg = document.getElementById('modal-photo'); 136 + item.addEventListener('click', () => openPhotoModal(photo.src, photo.title)); 137 + item.addEventListener('keydown', (e) => { 138 + if (e.key === 'Enter' || e.key === ' ') openPhotoModal(photo.src, photo.title); 139 + }); 107 140 108 - if (modal && modalImg) { 109 - modalImg.src = src; 110 - modalImg.alt = alt; 111 - modal.classList.add('active'); 112 - document.body.style.overflow = 'hidden'; 113 - } 141 + gallery.appendChild(item); 142 + }); 114 143 } 115 144 116 - function closePhotoModal() { 117 - const modal = document.getElementById('photo-modal'); 118 - if (modal) { 119 - modal.classList.remove('active'); 120 - document.body.style.overflow = 'auto'; 121 - } 122 - } 145 + // ── Init ────────────────────────────────────────────────────────────────────── 123 146 124 - // Initialize on page load 125 147 document.addEventListener('DOMContentLoaded', () => { 126 - renderPhotosGallery(); 148 + // Render whichever surface is present on this page 149 + renderHomePhotos(); 150 + renderPhotosGallery(); 127 151 128 - // Modal close button 129 - const closeBtn = document.getElementById('modal-close'); 130 - if (closeBtn) { 131 - closeBtn.addEventListener('click', closePhotoModal); 132 - } 152 + // Modal wiring (used on both pages) 153 + const modal = document.getElementById('photo-modal'); 154 + const closeBtn = document.getElementById('modal-close'); 133 155 134 - // Close modal when clicking outside image 135 - const modal = document.getElementById('photo-modal'); 136 - if (modal) { 137 - modal.addEventListener('click', (e) => { 138 - if (e.target === modal) { 139 - closePhotoModal(); 140 - } 141 - }); 156 + if (closeBtn) closeBtn.addEventListener('click', closePhotoModal); 142 157 143 - // Close on Escape key 144 - document.addEventListener('keydown', (e) => { 145 - if (e.key === 'Escape') { 146 - closePhotoModal(); 147 - } 158 + if (modal) { 159 + modal.addEventListener('click', e => { if (e.target === modal) closePhotoModal(); }); 160 + } 161 + 162 + document.addEventListener('keydown', e => { 163 + if (e.key === 'Escape') closePhotoModal(); 148 164 }); 149 - } 150 - }); 165 + });
+78 -78
js/script.js
··· 1 1 document.addEventListener('DOMContentLoaded', () => { 2 - const BLUESKY_DID = 'did:plc:4qjpcixqrc3b2qmbce76sm7k'; 3 - const MUSIC_DID = 'did:plc:l37td5yhxl2irrzrgvei4qay'; 2 + const AT_DID = 'did:plc:l37td5yhxl2irrzrgvei4qay'; 3 + 4 + // ── Music ────────────────────────────────────────────────────────────── 4 5 5 6 async function fetchMusicStatus() { 6 - const url = `https://rose.madebydanny.uk/xrpc/com.atproto.repo.getRecord?repo=${MUSIC_DID}&collection=fm.teal.alpha.actor.status&rkey=self`; 7 + const url = `https://rose.madebydanny.uk/xrpc/com.atproto.repo.getRecord?repo=${AT_DID}&collection=fm.teal.alpha.actor.status&rkey=self`; 8 + const el = document.getElementById('music-content'); 7 9 try { 8 - const response = await fetch(url); 9 - if (!response.ok) { 10 - console.error('Music API error:', response.status, response.statusText); 11 - throw new Error('Network response was not ok'); 12 - } 13 - const data = await response.json(); 14 - console.log('Music data:', data); 15 - 16 - if (data && data.value && data.value.item) { 17 - const item = data.value.item; 18 - const artists = item.artists.map(a => a.artistName).join(', '); 19 - const trackUrl = item.originUrl || '#'; 20 - 21 - document.getElementById('music-link-icon').innerHTML = `<a href="${trackUrl}" target="_blank">↗</a>`; 22 - document.getElementById('music-ui-content').innerHTML = ` 23 - <a href="${trackUrl}" target="_blank" class="bsky-trackname">${item.trackName}</a> 24 - <div class="bsky-artist">by ${artists}</div> 25 - `; 26 - document.getElementById('music-status-card').style.display = 'block'; 27 - } else { 28 - console.warn('No music data found'); 29 - } 30 - } catch (error) { 31 - console.error('Error fetching music:', error); 32 - // Show card anyway with error message 33 - document.getElementById('music-ui-content').innerHTML = '<span class="error-msg">No recent music activity</span>'; 34 - document.getElementById('music-status-card').style.display = 'block'; 10 + const res = await fetch(url); 11 + if (!res.ok) throw new Error(res.status); 12 + const data = await res.json(); 13 + const item = data?.value?.item; 14 + if (!item) throw new Error('no item'); 15 + 16 + const artists = item.artists.map(a => a.artistName).join(', '); 17 + const trackUrl = item.originUrl || '#'; 18 + 19 + el.innerHTML = ` 20 + <a href="${trackUrl}" target="_blank" rel="noopener noreferrer" class="track-name">${escapeHtml(item.trackName)}</a> 21 + <span class="track-artist">by ${escapeHtml(artists)}</span> 22 + `; 23 + } catch { 24 + el.innerHTML = '<span class="loading-text">no recent activity</span>'; 35 25 } 36 26 } 37 27 28 + // ── Latest original post ─────────────────────────────────────────────── 29 + // filter=posts_no_replies strips replies from the API response. 30 + // We then skip any reposts (reason.$type === reasonRepost) client-side, 31 + // since the filter param doesn't exclude those. 32 + 38 33 async function fetchLatestPost() { 39 - const url = `https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=${BLUESKY_DID}&limit=1`; 34 + // Fetch a small batch so we can skip any leading reposts 35 + const url = `https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=${AT_DID}&limit=10&filter=posts_no_replies`; 36 + const contentEl = document.getElementById('post-content'); 37 + const dateEl = document.getElementById('post-date'); 38 + const mediaEl = document.getElementById('post-media'); 40 39 try { 41 - console.log('Fetching post from:', url); 42 - const response = await fetch(url); 43 - 44 - if (!response.ok) { 45 - console.error('Post API error:', response.status, response.statusText); 46 - const errorText = await response.text(); 47 - console.error('Error response:', errorText); 48 - throw new Error(`HTTP ${response.status}`); 49 - } 50 - 51 - const data = await response.json(); 52 - console.log('Post data:', data); 53 - 54 - if (data.feed && data.feed.length > 0) { 55 - const latest = data.feed[0].post; 56 - const record = latest.record; 57 - const rkey = latest.uri.split('/').pop(); 58 - const postUrl = `https://bsky.app/profile/madebydanny.uk/post/${rkey}`; 40 + const res = await fetch(url); 41 + if (!res.ok) throw new Error(res.status); 42 + const data = await res.json(); 59 43 60 - // Set Text and Date 61 - document.getElementById('post-content').innerText = record.text || ""; 62 - document.getElementById('post-date').innerText = new Date(record.createdAt).toLocaleString(); 63 - document.getElementById('post-link-icon').innerHTML = `<a href="${postUrl}" target="_blank">↗</a>`; 44 + // Find first item that is neither a reply nor a repost 45 + const item = (data.feed || []).find(entry => 46 + !entry.reason && // no repost reason 47 + !entry.post.record.reply // no reply parent 48 + ); 64 49 65 - // Handle Images (Embeds) 66 - const mediaContainer = document.getElementById('post-media'); 67 - mediaContainer.innerHTML = ''; // Clear previous 50 + if (!item) throw new Error('no original posts found'); 68 51 69 - if (record.embed && record.embed.$type === 'app.bsky.embed.images') { 70 - record.embed.images.forEach(img => { 71 - const imgRef = img.image.ref.$link; 72 - const imgSrc = `https://cdn.bsky.app/img/feed_fullsize/plain/${BLUESKY_DID}/${imgRef}@jpeg`; 73 - 74 - const imgElement = document.createElement('img'); 75 - imgElement.src = imgSrc; 76 - imgElement.alt = img.alt || "Bluesky post image"; 77 - imgElement.className = 'post-image'; 78 - mediaContainer.appendChild(imgElement); 79 - }); 80 - } 52 + const post = item.post; 53 + const record = post.record; 54 + const rkey = post.uri.split('/').pop(); 55 + const postUrl = `https://bsky.app/profile/danielmorrisey.com/post/${rkey}`; 81 56 82 - document.getElementById('latest-post-card').style.display = 'block'; 83 - } else { 84 - console.warn('No posts found in feed'); 85 - document.getElementById('post-content').innerHTML = '<span class="error-msg">No posts found</span>'; 86 - document.getElementById('latest-post-card').style.display = 'block'; 57 + contentEl.innerHTML = ` 58 + <p>${escapeHtml(record.text || '')}</p> 59 + <a href="${postUrl}" target="_blank" rel="noopener noreferrer" style="font-size:0.8125rem;opacity:0.6;">view on Bluesky ↗</a> 60 + `; 61 + 62 + if (record.createdAt) { 63 + dateEl.textContent = new Date(record.createdAt).toLocaleDateString('en-GB', { 64 + day: 'numeric', month: 'long', year: 'numeric' 65 + }); 87 66 } 88 - } catch (error) { 89 - console.error('Error fetching post:', error); 90 - document.getElementById('post-content').innerHTML = `<span class="error-msg">Error loading post: ${error.message}</span>`; 91 - document.getElementById('latest-post-card').style.display = 'block'; 67 + 68 + // Images 69 + if (record.embed?.$type === 'app.bsky.embed.images') { 70 + record.embed.images.forEach(img => { 71 + const ref = img.image.ref.$link; 72 + const src = `https://cdn.blueat.net/img/feed_fullsize/plain/${AT_DID}/${ref}@jpeg`; 73 + const el = document.createElement('img'); 74 + el.src = src; 75 + el.alt = img.alt || 'Bluesky post image'; 76 + el.loading = 'lazy'; 77 + mediaEl.appendChild(el); 78 + }); 79 + } 80 + } catch { 81 + contentEl.innerHTML = '<span class="loading-text">couldn\'t load post</span>'; 92 82 } 93 83 } 94 84 85 + // ── Helpers ──────────────────────────────────────────────────────────── 86 + 87 + function escapeHtml(str) { 88 + return str 89 + .replace(/&/g, '&amp;') 90 + .replace(/</g, '&lt;') 91 + .replace(/>/g, '&gt;') 92 + .replace(/"/g, '&quot;'); 93 + } 94 + 95 95 fetchMusicStatus(); 96 96 fetchLatestPost(); 97 - }); 97 + });
-118
js/social-links.js
··· 1 - // Social Links Component 2 - const SOCIAL_LINKS = [ 3 - { 4 - id: 'bluesky', 5 - label: 'Bluesky', 6 - icon: 'fa-bluesky', 7 - href: 'https://bsky.app/profile/did:plc:l37td5yhxl2irrzrgvei4qay', 8 - target: '_blank' 9 - }, 10 - { 11 - id: 'tangled', 12 - label: 'Tangled', 13 - icon: 'fa-git-alt', 14 - href: 'https://tangled.org/did:plc:l37td5yhxl2irrzrgvei4qay', 15 - target: '_blank' 16 - }, 17 - { 18 - id: 'threads', 19 - label: 'Threads', 20 - icon: 'fa-threads', 21 - href: 'https://threads.net/@madebydanny.uk', 22 - target: '_blank' 23 - }, 24 - { 25 - id: 'mastodon', 26 - label: 'Mastodon', 27 - icon: 'fa-mastodon', 28 - href: 'https://mastodon.social/@danielmorrisey', 29 - target: '_blank' 30 - } 31 - ]; 32 - 33 - const FOOTER_CONFIG = { 34 - copyright: '2024-26 Daniel Morrisey', 35 - createdBy: '@madebydanny.uk', 36 - createdByUrl: 'https://aturi.to/did:plc:l37td5yhxl2irrzrgvei4qay', 37 - hostedOn: 'wisp.place', 38 - hostedOnUrl: 'https://wisp.place', 39 - homeUrl: '/', 40 - torUrl: 'http://irgwdhat74pqcpkk7ynrphvohnnt574yvwmhredrfusemgu6wj2ik5id.onion/' 41 - }; 42 - 43 - function renderSocialLinks(containerId) { 44 - const container = document.getElementById(containerId); 45 - if (!container) { 46 - console.warn(`Social links container with id "${containerId}" not found`); 47 - return; 48 - } 49 - 50 - container.innerHTML = ''; 51 - container.setAttribute('role', 'list'); 52 - container.classList.add('social-row'); 53 - 54 - SOCIAL_LINKS.forEach(link => { 55 - const a = document.createElement('a'); 56 - a.className = `social-btn btn-${link.id}`; 57 - a.href = link.href; 58 - a.setAttribute('role', 'listitem'); 59 - a.setAttribute('aria-label', link.label); 60 - 61 - if (link.target === '_blank') { 62 - a.target = '_blank'; 63 - a.rel = 'noopener noreferrer'; 64 - } 65 - 66 - const icon = document.createElement('i'); 67 - icon.className = `fa-brands ${link.icon}`; 68 - 69 - const label = document.createElement('span'); 70 - label.className = 'label'; 71 - label.textContent = link.label; 72 - 73 - a.appendChild(icon); 74 - a.appendChild(label); 75 - container.appendChild(a); 76 - }); 77 - } 78 - 79 - function renderFooter(containerId) { 80 - const container = document.getElementById(containerId); 81 - if (!container) { 82 - console.warn(`Footer container with id "${containerId}" not found`); 83 - return; 84 - } 85 - 86 - container.innerHTML = ''; 87 - 88 - // Back to home button (only show if not on home page) 89 - if (window.location.pathname !== '/') { 90 - const backLink = document.createElement('p'); 91 - backLink.style.marginBottom = '12px'; 92 - backLink.innerHTML = `← <a href="${FOOTER_CONFIG.homeUrl}">Back to home</a>`; 93 - container.appendChild(backLink); 94 - } 95 - 96 - const p = document.createElement('p'); 97 - p.innerHTML = `© ${FOOTER_CONFIG.copyright} by <a href="${FOOTER_CONFIG.createdByUrl}" target="_blank" rel="noopener noreferrer">${FOOTER_CONFIG.createdBy}</a> - hosted on <a href="${FOOTER_CONFIG.hostedOnUrl}" target="_blank" rel="noopener noreferrer">${FOOTER_CONFIG.hostedOn}</a>`; 98 - container.appendChild(p); 99 - 100 - // Tor link 101 - const torLink = document.createElement('p'); 102 - torLink.style.marginTop = '8px'; 103 - torLink.innerHTML = `<a href="${FOOTER_CONFIG.torUrl}" target="_blank" rel="noopener noreferrer">Open in Tor</a>`; 104 - container.appendChild(torLink); 105 - } 106 - 107 - // Auto-render if element with id "social-links" exists 108 - document.addEventListener('DOMContentLoaded', () => { 109 - const socialLinksContainer = document.getElementById('social-links'); 110 - if (socialLinksContainer) { 111 - renderSocialLinks('social-links'); 112 - } 113 - 114 - const footerContainer = document.getElementById('site-footer'); 115 - if (footerContainer) { 116 - renderFooter('site-footer'); 117 - } 118 - });
test.html

This is a binary file and will not be displayed.

test.txt

This is a binary file and will not be displayed.