Nipun Claude Opus 4.5 commited on
Commit
28a953c
·
1 Parent(s): 1b9bcd8

Add form data and file upload support with UI controls

Browse files

- Add form endpoints: /form/login, /form/contact for URL-encoded data
- Add file upload endpoints: /upload/file, /upload/files, /upload/file-with-data
- Add file management: GET /upload/files, GET/DELETE /upload/file/{id}
- Add Form tab with dynamic field management in UI
- Add File tab with drag-and-drop upload area
- Add code snippet panel to display generated cURL/Python/fetch/HTTPie code
- Update examples grid with form and file upload examples

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +601 -8
app.py CHANGED
@@ -10,12 +10,16 @@ Endpoints cover:
10
  - Headers, Status Codes, and more
11
  """
12
 
13
- from fastapi import FastAPI, Query, Path, Body, Header, Response, HTTPException, Request
14
  from fastapi.responses import PlainTextResponse, HTMLResponse, JSONResponse
15
  from pydantic import BaseModel, Field
16
  from typing import Optional, List
17
  from datetime import datetime
18
  import json
 
 
 
 
19
 
20
  app = FastAPI(
21
  title="API Teaching Tool",
@@ -69,6 +73,7 @@ def api_info():
69
  "PUT examples": ["/items/{id}"],
70
  "DELETE examples": ["/items/{id}"],
71
  "PATCH examples": ["/items/{id}"],
 
72
  "Response formats": ["/format/json", "/format/text", "/format/html", "/format/xml"],
73
  "Educational": ["/echo", "/headers", "/status/{code}"],
74
  }
@@ -381,6 +386,130 @@ def ui():
381
  .json-boolean { color: #aa0d91; }
382
  .json-null { color: #86868b; }
383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  /* Preview */
385
  .preview-container {
386
  padding: 16px;
@@ -481,12 +610,56 @@ def ui():
481
  <div class="panel">
482
  <div class="tabs" id="request-tabs">
483
  <button class="tab active" data-tab="req-body">Body</button>
 
 
484
  <button class="tab" data-tab="req-headers">Headers</button>
485
  </div>
486
  <div class="panel-body">
487
  <div class="tab-content active" id="req-body-tab">
488
  <textarea id="body" placeholder='{"name": "Laptop", "price": 999.99, "quantity": 5}'></textarea>
489
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  <div class="tab-content" id="req-headers-tab">
491
  <textarea id="headers" placeholder="Content-Type: application/json">Content-Type: application/json</textarea>
492
  </div>
@@ -530,6 +703,18 @@ def ui():
530
  <h3>Examples</h3>
531
  <div class="examples-grid" id="examples"></div>
532
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
533
  </div>
534
 
535
  <script>
@@ -545,18 +730,21 @@ def ui():
545
  { method: 'PUT', url: '/items/1', title: 'Replace Item', body: '{"name": "Green Apple", "price": 2.50, "quantity": 200}' },
546
  { method: 'PATCH', url: '/items/1', title: 'Partial Update', body: '{"price": 1.99}' },
547
  { method: 'DELETE', url: '/items/3', title: 'Delete Item' },
 
 
 
 
548
  { method: 'GET', url: '/format/json', title: 'JSON Format' },
549
  { method: 'GET', url: '/format/xml', title: 'XML Format' },
550
  { method: 'GET', url: '/format/html', title: 'HTML Format' },
551
  { method: 'GET', url: '/format/csv', title: 'CSV Format' },
552
- { method: 'GET', url: '/format/yaml', title: 'YAML Format' },
553
- { method: 'GET', url: '/format/markdown', title: 'Markdown' },
554
- { method: 'GET', url: '/format/image', title: 'PNG Image' },
555
  { method: 'GET', url: '/headers', title: 'View Headers' },
556
  { method: 'GET', url: '/status/404', title: 'Error 404' },
557
- { method: 'GET', url: '/status/201', title: 'Status 201' },
558
  ];
559
 
 
 
 
560
  // Render examples
561
  const examplesContainer = document.getElementById('examples');
562
  examples.forEach(ex => {
@@ -573,10 +761,52 @@ def ui():
573
  document.getElementById('method').value = ex.method;
574
  document.getElementById('url').value = ex.url;
575
  document.getElementById('body').value = ex.body || '';
 
 
 
 
 
 
 
 
 
 
576
  };
577
  examplesContainer.appendChild(btn);
578
  });
579
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
  // Tab switching
581
  function setupTabs(containerId) {
582
  const container = document.getElementById(containerId);
@@ -604,12 +834,35 @@ def ui():
604
  .replace(/: (null)/g, ': <span class="json-null">$1</span>');
605
  }
606
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
  // Send request
608
  async function sendRequest() {
609
  const method = document.getElementById('method').value;
610
  const url = document.getElementById('url').value;
611
  const body = document.getElementById('body').value;
612
  const headersText = document.getElementById('headers').value;
 
613
 
614
  const headers = {};
615
  headersText.split('\\n').forEach(line => {
@@ -618,8 +871,32 @@ def ui():
618
  });
619
 
620
  const opts = { method, headers };
621
- if (['POST', 'PUT', 'PATCH'].includes(method) && body) {
622
- opts.body = body;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
  }
624
 
625
  document.getElementById('send').disabled = true;
@@ -928,8 +1205,13 @@ def ui():
928
  e.stopPropagation();
929
  const format = btn.dataset.format;
930
  const code = generateCode(format);
 
 
 
 
 
931
  try {
932
- await navigator.clipboard.writeText(code.replace(/\\\\n/g, '\\n'));
933
  showToast(`Copied as ${format.toUpperCase()}!`);
934
  } catch {
935
  showToast('Failed to copy');
@@ -937,6 +1219,114 @@ def ui():
937
  copyMenu.classList.remove('show');
938
  };
939
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
940
  </script>
941
  </body>
942
  </html>
@@ -1086,6 +1476,209 @@ async def echo_text(request: Request):
1086
  "content_type": request.headers.get("content-type", "not specified")
1087
  }
1088
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1089
  # ============================================================================
1090
  # DIFFERENT RESPONSE FORMATS
1091
  # ============================================================================
 
10
  - Headers, Status Codes, and more
11
  """
12
 
13
+ from fastapi import FastAPI, Query, Path, Body, Header, Response, HTTPException, Request, File, UploadFile, Form
14
  from fastapi.responses import PlainTextResponse, HTMLResponse, JSONResponse
15
  from pydantic import BaseModel, Field
16
  from typing import Optional, List
17
  from datetime import datetime
18
  import json
19
+ import base64
20
+
21
+ # In-memory storage for uploaded files
22
+ uploaded_files_db = {}
23
 
24
  app = FastAPI(
25
  title="API Teaching Tool",
 
73
  "PUT examples": ["/items/{id}"],
74
  "DELETE examples": ["/items/{id}"],
75
  "PATCH examples": ["/items/{id}"],
76
+ "Form & File Upload": ["/form/login", "/form/contact", "/upload/file", "/upload/files", "/upload/file-with-data"],
77
  "Response formats": ["/format/json", "/format/text", "/format/html", "/format/xml"],
78
  "Educational": ["/echo", "/headers", "/status/{code}"],
79
  }
 
386
  .json-boolean { color: #aa0d91; }
387
  .json-null { color: #86868b; }
388
 
389
+ /* Form fields */
390
+ .form-group { margin-bottom: 12px; }
391
+ .form-label {
392
+ display: block;
393
+ font-size: 12px;
394
+ font-weight: 500;
395
+ color: #86868b;
396
+ margin-bottom: 4px;
397
+ }
398
+ .form-input {
399
+ width: 100%;
400
+ padding: 8px 12px;
401
+ background: #f5f5f7;
402
+ border: 1px solid transparent;
403
+ border-radius: 6px;
404
+ font-size: 14px;
405
+ transition: border-color 0.2s;
406
+ }
407
+ .form-input:focus { border-color: #007aff; outline: none; }
408
+ .form-row { display: flex; gap: 12px; }
409
+ .form-row .form-group { flex: 1; }
410
+ .checkbox-group {
411
+ display: flex;
412
+ align-items: center;
413
+ gap: 8px;
414
+ }
415
+ .checkbox-group input { width: 16px; height: 16px; }
416
+
417
+ /* File upload */
418
+ .file-upload-area {
419
+ border: 2px dashed #d2d2d7;
420
+ border-radius: 8px;
421
+ padding: 24px;
422
+ text-align: center;
423
+ transition: all 0.2s;
424
+ cursor: pointer;
425
+ background: #fafafa;
426
+ }
427
+ .file-upload-area:hover { border-color: #007aff; background: #f0f7ff; }
428
+ .file-upload-area.dragover { border-color: #007aff; background: #e8f4ff; }
429
+ .file-upload-icon { font-size: 32px; margin-bottom: 8px; }
430
+ .file-upload-text { font-size: 13px; color: #86868b; }
431
+ .file-upload-text strong { color: #007aff; }
432
+ .file-input { display: none; }
433
+ .file-list { margin-top: 12px; }
434
+ .file-item {
435
+ display: flex;
436
+ align-items: center;
437
+ justify-content: space-between;
438
+ padding: 8px 12px;
439
+ background: #f5f5f7;
440
+ border-radius: 6px;
441
+ margin-bottom: 6px;
442
+ font-size: 13px;
443
+ }
444
+ .file-item-name { font-weight: 500; }
445
+ .file-item-size { color: #86868b; font-size: 11px; }
446
+ .file-item-remove {
447
+ background: none;
448
+ border: none;
449
+ color: #ff3b30;
450
+ cursor: pointer;
451
+ font-size: 16px;
452
+ padding: 0 4px;
453
+ }
454
+
455
+ /* Code snippet panel */
456
+ .code-panel {
457
+ margin-top: 20px;
458
+ background: #fff;
459
+ border-radius: 10px;
460
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
461
+ overflow: hidden;
462
+ display: none;
463
+ }
464
+ .code-panel.show { display: block; }
465
+ .code-panel-header {
466
+ display: flex;
467
+ align-items: center;
468
+ justify-content: space-between;
469
+ padding: 12px 16px;
470
+ background: #fafafa;
471
+ border-bottom: 1px solid #d2d2d7;
472
+ }
473
+ .code-panel-title { font-weight: 600; font-size: 13px; }
474
+ .code-panel-close {
475
+ background: none;
476
+ border: none;
477
+ font-size: 18px;
478
+ color: #86868b;
479
+ cursor: pointer;
480
+ padding: 0 4px;
481
+ }
482
+ .code-panel-close:hover { color: #1d1d1f; }
483
+ .code-panel-content {
484
+ padding: 16px;
485
+ font-family: 'SF Mono', Menlo, monospace;
486
+ font-size: 13px;
487
+ line-height: 1.6;
488
+ white-space: pre-wrap;
489
+ word-break: break-all;
490
+ background: #1d1d1f;
491
+ color: #f5f5f7;
492
+ max-height: 300px;
493
+ overflow: auto;
494
+ }
495
+ .code-panel-actions {
496
+ padding: 12px 16px;
497
+ background: #fafafa;
498
+ border-top: 1px solid #d2d2d7;
499
+ display: flex;
500
+ gap: 8px;
501
+ }
502
+ .code-copy-btn {
503
+ padding: 6px 12px;
504
+ background: #007aff;
505
+ color: #fff;
506
+ border: none;
507
+ border-radius: 6px;
508
+ font-size: 12px;
509
+ cursor: pointer;
510
+ }
511
+ .code-copy-btn:hover { background: #0056b3; }
512
+
513
  /* Preview */
514
  .preview-container {
515
  padding: 16px;
 
610
  <div class="panel">
611
  <div class="tabs" id="request-tabs">
612
  <button class="tab active" data-tab="req-body">Body</button>
613
+ <button class="tab" data-tab="req-form">Form</button>
614
+ <button class="tab" data-tab="req-file">File</button>
615
  <button class="tab" data-tab="req-headers">Headers</button>
616
  </div>
617
  <div class="panel-body">
618
  <div class="tab-content active" id="req-body-tab">
619
  <textarea id="body" placeholder='{"name": "Laptop", "price": 999.99, "quantity": 5}'></textarea>
620
  </div>
621
+ <div class="tab-content" id="req-form-tab">
622
+ <div id="form-fields">
623
+ <div class="form-row">
624
+ <div class="form-group">
625
+ <label class="form-label">Field Name</label>
626
+ <input type="text" class="form-input form-field-name" placeholder="username">
627
+ </div>
628
+ <div class="form-group">
629
+ <label class="form-label">Value</label>
630
+ <input type="text" class="form-input form-field-value" placeholder="john_doe">
631
+ </div>
632
+ </div>
633
+ </div>
634
+ <button type="button" id="add-form-field" style="margin-top:8px;padding:6px 12px;background:#f5f5f7;border:1px solid #d2d2d7;border-radius:6px;font-size:12px;cursor:pointer;">+ Add Field</button>
635
+ <div style="margin-top:12px;font-size:11px;color:#86868b;">
636
+ Content-Type: application/x-www-form-urlencoded
637
+ </div>
638
+ </div>
639
+ <div class="tab-content" id="req-file-tab">
640
+ <div class="file-upload-area" id="file-drop-area">
641
+ <div class="file-upload-icon">📁</div>
642
+ <div class="file-upload-text">
643
+ <strong>Click to upload</strong> or drag and drop<br>
644
+ <span style="font-size:11px;">Any file type supported</span>
645
+ </div>
646
+ <input type="file" id="file-input" class="file-input" multiple>
647
+ </div>
648
+ <div class="file-list" id="file-list"></div>
649
+ <div id="file-metadata" style="margin-top:12px;display:none;">
650
+ <div class="form-group">
651
+ <label class="form-label">Title (optional)</label>
652
+ <input type="text" class="form-input" id="file-title" placeholder="My Document">
653
+ </div>
654
+ <div class="form-group">
655
+ <label class="form-label">Description (optional)</label>
656
+ <input type="text" class="form-input" id="file-description" placeholder="A description of the file">
657
+ </div>
658
+ </div>
659
+ <div style="margin-top:12px;font-size:11px;color:#86868b;">
660
+ Content-Type: multipart/form-data
661
+ </div>
662
+ </div>
663
  <div class="tab-content" id="req-headers-tab">
664
  <textarea id="headers" placeholder="Content-Type: application/json">Content-Type: application/json</textarea>
665
  </div>
 
703
  <h3>Examples</h3>
704
  <div class="examples-grid" id="examples"></div>
705
  </div>
706
+
707
+ <!-- Code Snippet Panel -->
708
+ <div class="code-panel" id="code-panel">
709
+ <div class="code-panel-header">
710
+ <span class="code-panel-title" id="code-panel-title">cURL</span>
711
+ <button class="code-panel-close" id="code-panel-close">&times;</button>
712
+ </div>
713
+ <div class="code-panel-content" id="code-panel-content"></div>
714
+ <div class="code-panel-actions">
715
+ <button class="code-copy-btn" id="code-copy-btn">Copy to Clipboard</button>
716
+ </div>
717
+ </div>
718
  </div>
719
 
720
  <script>
 
730
  { method: 'PUT', url: '/items/1', title: 'Replace Item', body: '{"name": "Green Apple", "price": 2.50, "quantity": 200}' },
731
  { method: 'PATCH', url: '/items/1', title: 'Partial Update', body: '{"price": 1.99}' },
732
  { method: 'DELETE', url: '/items/3', title: 'Delete Item' },
733
+ { method: 'POST', url: '/form/login', title: 'Form Login', formData: {username: 'john_doe', password: 'secret123', remember_me: 'true'} },
734
+ { method: 'POST', url: '/form/contact', title: 'Contact Form', formData: {name: 'Alice', email: 'alice@example.com', subject: 'Hello', message: 'Nice API!'} },
735
+ { method: 'POST', url: '/upload/file', title: 'File Upload', isFileUpload: true },
736
+ { method: 'GET', url: '/upload/files', title: 'List Uploads' },
737
  { method: 'GET', url: '/format/json', title: 'JSON Format' },
738
  { method: 'GET', url: '/format/xml', title: 'XML Format' },
739
  { method: 'GET', url: '/format/html', title: 'HTML Format' },
740
  { method: 'GET', url: '/format/csv', title: 'CSV Format' },
 
 
 
741
  { method: 'GET', url: '/headers', title: 'View Headers' },
742
  { method: 'GET', url: '/status/404', title: 'Error 404' },
 
743
  ];
744
 
745
+ // Track selected files
746
+ let selectedFiles = [];
747
+
748
  // Render examples
749
  const examplesContainer = document.getElementById('examples');
750
  examples.forEach(ex => {
 
761
  document.getElementById('method').value = ex.method;
762
  document.getElementById('url').value = ex.url;
763
  document.getElementById('body').value = ex.body || '';
764
+
765
+ // Handle form data examples
766
+ if (ex.formData) {
767
+ switchToTab('request-tabs', 'req-form');
768
+ populateFormFields(ex.formData);
769
+ } else if (ex.isFileUpload) {
770
+ switchToTab('request-tabs', 'req-file');
771
+ } else {
772
+ switchToTab('request-tabs', 'req-body');
773
+ }
774
  };
775
  examplesContainer.appendChild(btn);
776
  });
777
 
778
+ function switchToTab(tabsId, tabName) {
779
+ const container = document.getElementById(tabsId);
780
+ container.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
781
+ const tab = container.querySelector(`[data-tab="${tabName}"]`);
782
+ if (tab) {
783
+ tab.classList.add('active');
784
+ const panel = container.closest('.panel') || document;
785
+ panel.querySelectorAll(':scope > .tab-content, :scope > .panel-body > .tab-content').forEach(c => c.classList.remove('active'));
786
+ document.getElementById(tabName + '-tab').classList.add('active');
787
+ }
788
+ }
789
+
790
+ function populateFormFields(data) {
791
+ const container = document.getElementById('form-fields');
792
+ container.innerHTML = '';
793
+ Object.entries(data).forEach(([key, value], i) => {
794
+ const row = document.createElement('div');
795
+ row.className = 'form-row';
796
+ row.innerHTML = `
797
+ <div class="form-group">
798
+ <label class="form-label">${i === 0 ? 'Field Name' : ''}</label>
799
+ <input type="text" class="form-input form-field-name" value="${key}" placeholder="field_name">
800
+ </div>
801
+ <div class="form-group">
802
+ <label class="form-label">${i === 0 ? 'Value' : ''}</label>
803
+ <input type="text" class="form-input form-field-value" value="${value}" placeholder="value">
804
+ </div>
805
+ `;
806
+ container.appendChild(row);
807
+ });
808
+ }
809
+
810
  // Tab switching
811
  function setupTabs(containerId) {
812
  const container = document.getElementById(containerId);
 
834
  .replace(/: (null)/g, ': <span class="json-null">$1</span>');
835
  }
836
 
837
+ // Get active request type
838
+ function getActiveRequestType() {
839
+ const formTab = document.getElementById('req-form-tab');
840
+ const fileTab = document.getElementById('req-file-tab');
841
+ if (formTab.classList.contains('active')) return 'form';
842
+ if (fileTab.classList.contains('active')) return 'file';
843
+ return 'body';
844
+ }
845
+
846
+ // Get form data from the form fields
847
+ function getFormFieldsData() {
848
+ const data = {};
849
+ const names = document.querySelectorAll('.form-field-name');
850
+ const values = document.querySelectorAll('.form-field-value');
851
+ names.forEach((nameInput, i) => {
852
+ const name = nameInput.value.trim();
853
+ const value = values[i]?.value || '';
854
+ if (name) data[name] = value;
855
+ });
856
+ return data;
857
+ }
858
+
859
  // Send request
860
  async function sendRequest() {
861
  const method = document.getElementById('method').value;
862
  const url = document.getElementById('url').value;
863
  const body = document.getElementById('body').value;
864
  const headersText = document.getElementById('headers').value;
865
+ const requestType = getActiveRequestType();
866
 
867
  const headers = {};
868
  headersText.split('\\n').forEach(line => {
 
871
  });
872
 
873
  const opts = { method, headers };
874
+
875
+ if (['POST', 'PUT', 'PATCH'].includes(method)) {
876
+ if (requestType === 'form') {
877
+ // Form URL-encoded data
878
+ const formData = getFormFieldsData();
879
+ opts.body = new URLSearchParams(formData).toString();
880
+ opts.headers['Content-Type'] = 'application/x-www-form-urlencoded';
881
+ } else if (requestType === 'file' && selectedFiles.length > 0) {
882
+ // Multipart form data for file upload
883
+ const formData = new FormData();
884
+ if (selectedFiles.length === 1) {
885
+ formData.append('file', selectedFiles[0]);
886
+ } else {
887
+ selectedFiles.forEach(f => formData.append('files', f));
888
+ }
889
+ // Add optional metadata
890
+ const title = document.getElementById('file-title')?.value;
891
+ const description = document.getElementById('file-description')?.value;
892
+ if (title) formData.append('title', title);
893
+ if (description) formData.append('description', description);
894
+ opts.body = formData;
895
+ // Don't set Content-Type for FormData - browser sets it with boundary
896
+ delete opts.headers['Content-Type'];
897
+ } else if (body) {
898
+ opts.body = body;
899
+ }
900
  }
901
 
902
  document.getElementById('send').disabled = true;
 
1205
  e.stopPropagation();
1206
  const format = btn.dataset.format;
1207
  const code = generateCode(format);
1208
+ const formattedCode = code.replace(/\\\\n/g, '\\n');
1209
+
1210
+ // Show in code panel
1211
+ showCodePanel(format, formattedCode);
1212
+
1213
  try {
1214
+ await navigator.clipboard.writeText(formattedCode);
1215
  showToast(`Copied as ${format.toUpperCase()}!`);
1216
  } catch {
1217
  showToast('Failed to copy');
 
1219
  copyMenu.classList.remove('show');
1220
  };
1221
  });
1222
+
1223
+ // Code panel functionality
1224
+ const codePanel = document.getElementById('code-panel');
1225
+ const codePanelTitle = document.getElementById('code-panel-title');
1226
+ const codePanelContent = document.getElementById('code-panel-content');
1227
+ const codePanelClose = document.getElementById('code-panel-close');
1228
+ const codeCopyBtn = document.getElementById('code-copy-btn');
1229
+
1230
+ function showCodePanel(format, code) {
1231
+ const titles = {
1232
+ curl: 'cURL Command',
1233
+ python: 'Python (requests)',
1234
+ fetch: 'JavaScript (fetch)',
1235
+ httpie: 'HTTPie Command'
1236
+ };
1237
+ codePanelTitle.textContent = titles[format] || format.toUpperCase();
1238
+ codePanelContent.textContent = code;
1239
+ codePanel.classList.add('show');
1240
+ }
1241
+
1242
+ codePanelClose.onclick = () => codePanel.classList.remove('show');
1243
+ codeCopyBtn.onclick = async () => {
1244
+ try {
1245
+ await navigator.clipboard.writeText(codePanelContent.textContent);
1246
+ showToast('Copied to clipboard!');
1247
+ } catch {
1248
+ showToast('Failed to copy');
1249
+ }
1250
+ };
1251
+
1252
+ // File upload handling
1253
+ const fileDropArea = document.getElementById('file-drop-area');
1254
+ const fileInput = document.getElementById('file-input');
1255
+ const fileList = document.getElementById('file-list');
1256
+ const fileMetadata = document.getElementById('file-metadata');
1257
+
1258
+ fileDropArea.onclick = () => fileInput.click();
1259
+
1260
+ fileDropArea.ondragover = (e) => {
1261
+ e.preventDefault();
1262
+ fileDropArea.classList.add('dragover');
1263
+ };
1264
+
1265
+ fileDropArea.ondragleave = () => {
1266
+ fileDropArea.classList.remove('dragover');
1267
+ };
1268
+
1269
+ fileDropArea.ondrop = (e) => {
1270
+ e.preventDefault();
1271
+ fileDropArea.classList.remove('dragover');
1272
+ handleFiles(e.dataTransfer.files);
1273
+ };
1274
+
1275
+ fileInput.onchange = (e) => {
1276
+ handleFiles(e.target.files);
1277
+ };
1278
+
1279
+ function handleFiles(files) {
1280
+ selectedFiles = Array.from(files);
1281
+ renderFileList();
1282
+ }
1283
+
1284
+ function renderFileList() {
1285
+ if (selectedFiles.length === 0) {
1286
+ fileList.innerHTML = '';
1287
+ fileMetadata.style.display = 'none';
1288
+ return;
1289
+ }
1290
+
1291
+ fileList.innerHTML = selectedFiles.map((file, i) => `
1292
+ <div class="file-item">
1293
+ <div>
1294
+ <span class="file-item-name">${file.name}</span>
1295
+ <span class="file-item-size">${formatFileSize(file.size)}</span>
1296
+ </div>
1297
+ <button class="file-item-remove" onclick="removeFile(${i})">&times;</button>
1298
+ </div>
1299
+ `).join('');
1300
+
1301
+ fileMetadata.style.display = 'block';
1302
+ }
1303
+
1304
+ function formatFileSize(bytes) {
1305
+ if (bytes < 1024) return bytes + ' B';
1306
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
1307
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
1308
+ }
1309
+
1310
+ window.removeFile = function(index) {
1311
+ selectedFiles.splice(index, 1);
1312
+ renderFileList();
1313
+ };
1314
+
1315
+ // Add form field button
1316
+ document.getElementById('add-form-field').onclick = () => {
1317
+ const container = document.getElementById('form-fields');
1318
+ const row = document.createElement('div');
1319
+ row.className = 'form-row';
1320
+ row.innerHTML = `
1321
+ <div class="form-group">
1322
+ <input type="text" class="form-input form-field-name" placeholder="field_name">
1323
+ </div>
1324
+ <div class="form-group">
1325
+ <input type="text" class="form-input form-field-value" placeholder="value">
1326
+ </div>
1327
+ `;
1328
+ container.appendChild(row);
1329
+ };
1330
  </script>
1331
  </body>
1332
  </html>
 
1476
  "content_type": request.headers.get("content-type", "not specified")
1477
  }
1478
 
1479
+ # ============================================================================
1480
+ # FORM & FILE UPLOAD
1481
+ # ============================================================================
1482
+
1483
+ next_file_id = 1
1484
+
1485
+ @app.post("/form/login", tags=["Form & File Upload"])
1486
+ async def form_login(
1487
+ username: str = Form(..., description="Username"),
1488
+ password: str = Form(..., description="Password"),
1489
+ remember_me: bool = Form(default=False, description="Remember me checkbox")
1490
+ ):
1491
+ """POST - Handle form data (like a login form)"""
1492
+ return {
1493
+ "message": "Form data received successfully",
1494
+ "data": {
1495
+ "username": username,
1496
+ "password": "***hidden***",
1497
+ "password_length": len(password),
1498
+ "remember_me": remember_me
1499
+ },
1500
+ "note": "This demonstrates form-urlencoded data (Content-Type: application/x-www-form-urlencoded)"
1501
+ }
1502
+
1503
+ @app.post("/form/contact", tags=["Form & File Upload"])
1504
+ async def form_contact(
1505
+ name: str = Form(..., description="Your name"),
1506
+ email: str = Form(..., description="Your email"),
1507
+ subject: str = Form(default="General Inquiry", description="Subject"),
1508
+ message: str = Form(..., description="Your message")
1509
+ ):
1510
+ """POST - Contact form submission"""
1511
+ return {
1512
+ "message": "Contact form received",
1513
+ "data": {
1514
+ "name": name,
1515
+ "email": email,
1516
+ "subject": subject,
1517
+ "message": message,
1518
+ "message_length": len(message)
1519
+ },
1520
+ "timestamp": datetime.now().isoformat()
1521
+ }
1522
+
1523
+ @app.post("/upload/file", tags=["Form & File Upload"])
1524
+ async def upload_single_file(
1525
+ file: UploadFile = File(..., description="File to upload")
1526
+ ):
1527
+ """POST - Upload a single file"""
1528
+ global next_file_id
1529
+
1530
+ contents = await file.read()
1531
+ file_size = len(contents)
1532
+
1533
+ # Store file metadata and content
1534
+ file_id = next_file_id
1535
+ uploaded_files_db[file_id] = {
1536
+ "id": file_id,
1537
+ "filename": file.filename,
1538
+ "content_type": file.content_type,
1539
+ "size": file_size,
1540
+ "content": base64.b64encode(contents).decode("utf-8")
1541
+ }
1542
+ next_file_id += 1
1543
+
1544
+ return {
1545
+ "message": "File uploaded successfully",
1546
+ "file": {
1547
+ "id": file_id,
1548
+ "filename": file.filename,
1549
+ "content_type": file.content_type,
1550
+ "size": file_size,
1551
+ "size_formatted": f"{file_size / 1024:.2f} KB" if file_size > 1024 else f"{file_size} bytes"
1552
+ },
1553
+ "download_url": f"/upload/file/{file_id}"
1554
+ }
1555
+
1556
+ @app.post("/upload/files", tags=["Form & File Upload"])
1557
+ async def upload_multiple_files(
1558
+ files: List[UploadFile] = File(..., description="Multiple files to upload")
1559
+ ):
1560
+ """POST - Upload multiple files at once"""
1561
+ global next_file_id
1562
+
1563
+ uploaded = []
1564
+ for file in files:
1565
+ contents = await file.read()
1566
+ file_size = len(contents)
1567
+
1568
+ file_id = next_file_id
1569
+ uploaded_files_db[file_id] = {
1570
+ "id": file_id,
1571
+ "filename": file.filename,
1572
+ "content_type": file.content_type,
1573
+ "size": file_size,
1574
+ "content": base64.b64encode(contents).decode("utf-8")
1575
+ }
1576
+ next_file_id += 1
1577
+
1578
+ uploaded.append({
1579
+ "id": file_id,
1580
+ "filename": file.filename,
1581
+ "content_type": file.content_type,
1582
+ "size": file_size
1583
+ })
1584
+
1585
+ return {
1586
+ "message": f"Uploaded {len(uploaded)} files successfully",
1587
+ "files": uploaded,
1588
+ "total_size": sum(f["size"] for f in uploaded)
1589
+ }
1590
+
1591
+ @app.post("/upload/file-with-data", tags=["Form & File Upload"])
1592
+ async def upload_file_with_form_data(
1593
+ file: UploadFile = File(..., description="File to upload"),
1594
+ title: str = Form(..., description="Title for the file"),
1595
+ description: str = Form(default="", description="Optional description"),
1596
+ category: str = Form(default="general", description="Category")
1597
+ ):
1598
+ """POST - Upload file with additional form fields (multipart/form-data)"""
1599
+ global next_file_id
1600
+
1601
+ contents = await file.read()
1602
+ file_size = len(contents)
1603
+
1604
+ file_id = next_file_id
1605
+ uploaded_files_db[file_id] = {
1606
+ "id": file_id,
1607
+ "filename": file.filename,
1608
+ "content_type": file.content_type,
1609
+ "size": file_size,
1610
+ "content": base64.b64encode(contents).decode("utf-8"),
1611
+ "metadata": {
1612
+ "title": title,
1613
+ "description": description,
1614
+ "category": category
1615
+ }
1616
+ }
1617
+ next_file_id += 1
1618
+
1619
+ return {
1620
+ "message": "File uploaded with metadata",
1621
+ "file": {
1622
+ "id": file_id,
1623
+ "filename": file.filename,
1624
+ "content_type": file.content_type,
1625
+ "size": file_size
1626
+ },
1627
+ "metadata": {
1628
+ "title": title,
1629
+ "description": description,
1630
+ "category": category
1631
+ },
1632
+ "download_url": f"/upload/file/{file_id}"
1633
+ }
1634
+
1635
+ @app.get("/upload/files", tags=["Form & File Upload"])
1636
+ def list_uploaded_files():
1637
+ """GET - List all uploaded files"""
1638
+ files = [
1639
+ {
1640
+ "id": f["id"],
1641
+ "filename": f["filename"],
1642
+ "content_type": f["content_type"],
1643
+ "size": f["size"],
1644
+ "metadata": f.get("metadata")
1645
+ }
1646
+ for f in uploaded_files_db.values()
1647
+ ]
1648
+ return {"files": files, "count": len(files)}
1649
+
1650
+ @app.get("/upload/file/{file_id}", tags=["Form & File Upload"])
1651
+ def download_file(file_id: int = Path(..., description="File ID to download")):
1652
+ """GET - Download an uploaded file by ID"""
1653
+ if file_id not in uploaded_files_db:
1654
+ raise HTTPException(status_code=404, detail=f"File with id {file_id} not found")
1655
+
1656
+ file_data = uploaded_files_db[file_id]
1657
+ content = base64.b64decode(file_data["content"])
1658
+
1659
+ return Response(
1660
+ content=content,
1661
+ media_type=file_data["content_type"],
1662
+ headers={
1663
+ "Content-Disposition": f'attachment; filename="{file_data["filename"]}"'
1664
+ }
1665
+ )
1666
+
1667
+ @app.delete("/upload/file/{file_id}", tags=["Form & File Upload"])
1668
+ def delete_uploaded_file(file_id: int = Path(..., description="File ID to delete")):
1669
+ """DELETE - Delete an uploaded file"""
1670
+ if file_id not in uploaded_files_db:
1671
+ raise HTTPException(status_code=404, detail=f"File with id {file_id} not found")
1672
+
1673
+ deleted = uploaded_files_db.pop(file_id)
1674
+ return {
1675
+ "message": "File deleted successfully",
1676
+ "deleted_file": {
1677
+ "id": deleted["id"],
1678
+ "filename": deleted["filename"]
1679
+ }
1680
+ }
1681
+
1682
  # ============================================================================
1683
  # DIFFERENT RESPONSE FORMATS
1684
  # ============================================================================