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