- 1# Podcast Episode Form with RSS Auto-Population
- 2
- 3Here's a complete form implementation that auto-populates from RSS feeds and allows manual editing:
- 4
- 5```html
- 6<!DOCTYPE html>
- 7<html lang="en">
- 8<head>
- 9 <meta charset="UTF-8">
- 10 <meta name="viewport" content="width=device-width, initial-scale=1.0">
- 11 <title>Podcast Episode Manager</title>
- 12 <style>
- 13 :root {
- 14 --primary: #1a73e8;
- 15 --secondary: #34a853;
- 16 --danger: #ea4335;
- 17 --warning: #fbbc04;
- 18 --light: #f8f9fa;
- 19 --dark: #202124;
- 20 --gray: #5f6368;
- 21 --border: #dadce0;
- 22 }
- 23
- 24 * {
- 25 box-sizing: border-box;
- 26 margin: 0;
- 27 padding: 0;
- 28 }
- 29
- 30 body {
- 31 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
- 32 line-height: 1.6;
- 33 color: var(--dark);
- 34 background-color: var(--light);
- 35 padding: 20px;
- 36 }
- 37
- 38 .container {
- 39 max-width: 1200px;
- 40 margin: 0 auto;
- 41 background: white;
- 42 border-radius: 12px;
- 43 box-shadow: 0 2px 10px rgba(0,0,0,0.1);
- 44 overflow: hidden;
- 45 }
- 46
- 47 .header {
- 48 background: linear-gradient(135deg, var(--primary), #6c63ff);
- 49 color: white;
- 50 padding: 30px;
- 51 text-align: center;
- 52 }
- 53
- 54 .header h1 {
- 55 font-size: 2.5rem;
- 56 margin-bottom: 10px;
- 57 }
- 58
- 59 .header p {
- 60 opacity: 0.9;
- 61 font-size: 1.1rem;
- 62 }
- 63
- 64 .tabs {
- 65 display: flex;
- 66 background: #f1f3f4;
- 67 border-bottom: 1px solid var(--border);
- 68 }
- 69
- 70 .tab {
- 71 padding: 15px 30px;
- 72 background: none;
- 73 border: none;
- 74 font-size: 16px;
- 75 cursor: pointer;
- 76 transition: all 0.3s ease;
- 77 position: relative;
- 78 }
- 79
- 80 .tab.active {
- 81 background: white;
- 82 color: var(--primary);
- 83 font-weight: 600;
- 84 }
- 85
- 86 .tab.active::after {
- 87 content: '';
- 88 position: absolute;
- 89 bottom: -1px;
- 90 left: 0;
- 91 right: 0;
- 92 height: 3px;
- 93 background: var(--primary);
- 94 }
- 95
- 96 .tab-content {
- 97 display: none;
- 98 padding: 30px;
- 99 }
- 100
- 101 .tab-content.active {
- 102 display: block;
- 103 }
- 104
- 105 .section {
- 106 background: white;
- 107 border: 1px solid var(--border);
- 108 border-radius: 8px;
- 109 margin-bottom: 24px;
- 110 overflow: hidden;
- 111 }
- 112
- 113 .section-header {
- 114 background: #f8f9fa;
- 115 padding: 16px 24px;
- 116 border-bottom: 1px solid var(--border);
- 117 display: flex;
- 118 justify-content: space-between;
- 119 align-items: center;
- 120 }
- 121
- 122 .section-header h3 {
- 123 color: var(--dark);
- 124 font-size: 18px;
- 125 margin: 0;
- 126 }
- 127
- 128 .section-content {
- 129 padding: 24px;
- 130 }
- 131
- 132 .form-group {
- 133 margin-bottom: 20px;
- 134 }
- 135
- 136 label {
- 137 display: block;
- 138 margin-bottom: 8px;
- 139 font-weight: 500;
- 140 color: var(--dark);
- 141 }
- 142
- 143 .required::after {
- 144 content: " *";
- 145 color: var(--danger);
- 146 }
- 147
- 148 input[type="text"],
- 149 input[type="number"],
- 150 input[type="url"],
- 151 input[type="datetime-local"],
- 152 textarea,
- 153 select {
- 154 width: 100%;
- 155 padding: 12px;
- 156 border: 1px solid var(--border);
- 157 border-radius: 6px;
- 158 font-size: 16px;
- 159 transition: border 0.3s ease;
- 160 }
- 161
- 162 input[type="text"]:focus,
- 163 input[type="number"]:focus,
- 164 input[type="url"]:focus,
- 165 textarea:focus,
- 166 select:focus {
- 167 outline: none;
- 168 border-color: var(--primary);
- 169 box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1);
- 170 }
- 171
- 172 textarea {
- 173 min-height: 100px;
- 174 resize: vertical;
- 175 }
- 176
- 177 .form-row {
- 178 display: grid;
- 179 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- 180 gap: 20px;
- 181 }
- 182
- 183 .checkbox-group {
- 184 display: flex;
- 185 gap: 20px;
- 186 flex-wrap: wrap;
- 187 }
- 188
- 189 .checkbox-item {
- 190 display: flex;
- 191 align-items: center;
- 192 gap: 8px;
- 193 }
- 194
- 195 .btn {
- 196 padding: 12px 24px;
- 197 border: none;
- 198 border-radius: 6px;
- 199 font-size: 16px;
- 200 font-weight: 500;
- 201 cursor: pointer;
- 202 transition: all 0.3s ease;
- 203 display: inline-flex;
- 204 align-items: center;
- 205 gap: 8px;
- 206 }
- 207
- 208 .btn-primary {
- 209 background: var(--primary);
- 210 color: white;
- 211 }
- 212
- 213 .btn-primary:hover {
- 214 background: #0d62d9;
- 215 transform: translateY(-2px);
- 216 }
- 217
- 218 .btn-secondary {
- 219 background: var(--secondary);
- 220 color: white;
- 221 }
- 222
- 223 .btn-secondary:hover {
- 224 background: #2c8e44;
- 225 }
- 226
- 227 .btn-outline {
- 228 background: white;
- 229 color: var(--primary);
- 230 border: 2px solid var(--primary);
- 231 }
- 232
- 233 .btn-outline:hover {
- 234 background: var(--primary);
- 235 color: white;
- 236 }
- 237
- 238 .btn-danger {
- 239 background: var(--danger);
- 240 color: white;
- 241 }
- 242
- 243 .btn-danger:hover {
- 244 background: #d33426;
- 245 }
- 246
- 247 .btn-small {
- 248 padding: 6px 12px;
- 249 font-size: 14px;
- 250 }
- 251
- 252 .form-actions {
- 253 display: flex;
- 254 gap: 12px;
- 255 justify-content: flex-end;
- 256 margin-top: 30px;
- 257 padding-top: 20px;
- 258 border-top: 1px solid var(--border);
- 259 }
- 260
- 261 .status-badge {
- 262 display: inline-block;
- 263 padding: 4px 12px;
- 264 border-radius: 20px;
- 265 font-size: 12px;
- 266 font-weight: 500;
- 267 text-transform: uppercase;
- 268 }
- 269
- 270 .status-draft {
- 271 background: #fef3c7;
- 272 color: #92400e;
- 273 }
- 274
- 275 .status-ready {
- 276 background: #d1fae5;
- 277 color: #065f46;
- 278 }
- 279
- 280 .status-published {
- 281 background: #dbeafe;
- 282 color: #1e40af;
- 283 }
- 284
- 285 .tag-input {
- 286 display: flex;
- 287 flex-wrap: wrap;
- 288 gap: 8px;
- 289 padding: 8px;
- 290 border: 1px solid var(--border);
- 291 border-radius: 6px;
- 292 min-height: 48px;
- 293 }
- 294
- 295 .tag {
- 296 background: #e8f0fe;
- 297 color: var(--primary);
- 298 padding: 4px 12px;
- 299 border-radius: 16px;
- 300 font-size: 14px;
- 301 display: flex;
- 302 align-items: center;
- 303 gap: 6px;
- 304 }
- 305
- 306 .tag-remove {
- 307 background: none;
- 308 border: none;
- 309 color: var(--primary);
- 310 cursor: pointer;
- 311 font-size: 16px;
- 312 line-height: 1;
- 313 }
- 314
- 315 .tag-input input {
- 316 flex: 1;
- 317 min-width: 150px;
- 318 border: none;
- 319 outline: none;
- 320 padding: 8px;
- 321 font-size: 16px;
- 322 }
- 323
- 324 .repeater-item {
- 325 background: #f8f9fa;
- 326 border: 1px solid var(--border);
- 327 border-radius: 6px;
- 328 padding: 16px;
- 329 margin-bottom: 12px;
- 330 }
- 331
- 332 .repeater-header {
- 333 display: flex;
- 334 justify-content: space-between;
- 335 align-items: center;
- 336 margin-bottom: 12px;
- 337 }
- 338
- 339 .repeater-title {
- 340 font-weight: 600;
- 341 color: var(--dark);
- 342 }
- 343
- 344 .file-preview {
- 345 display: flex;
- 346 align-items: center;
- 347 gap: 12px;
- 348 padding: 12px;
- 349 background: #f8f9fa;
- 350 border: 1px solid var(--border);
- 351 border-radius: 6px;
- 352 margin-top: 8px;
- 353 }
- 354
- 355 .file-preview img {
- 356 width: 60px;
- 357 height: 60px;
- 358 object-fit: cover;
- 359 border-radius: 4px;
- 360 }
- 361
- 362 .file-info {
- 363 flex: 1;
- 364 }
- 365
- 366 .file-name {
- 367 font-weight: 500;
- 368 margin-bottom: 4px;
- 369 }
- 370
- 371 .file-size {
- 372 font-size: 14px;
- 373 color: var(--gray);
- 374 }
- 375
- 376 .progress-bar {
- 377 height: 4px;
- 378 background: var(--border);
- 379 border-radius: 2px;
- 380 overflow: hidden;
- 381 margin-top: 8px;
- 382 }
- 383
- 384 .progress-fill {
- 385 height: 100%;
- 386 background: var(--primary);
- 387 width: 0%;
- 388 transition: width 0.3s ease;
- 389 }
- 390
- 391 .rss-import-section {
- 392 background: #f0f7ff;
- 393 border: 2px dashed var(--primary);
- 394 border-radius: 8px;
- 395 padding: 30px;
- 396 text-align: center;
- 397 margin-bottom: 30px;
- 398 }
- 399
- 400 .rss-import-section h3 {
- 401 color: var(--primary);
- 402 margin-bottom: 16px;
- 403 }
- 404
- 405 .episode-selector {
- 406 max-height: 300px;
- 407 overflow-y: auto;
- 408 border: 1px solid var(--border);
- 409 border-radius: 6px;
- 410 margin-top: 16px;
- 411 }
- 412
- 413 .episode-item {
- 414 padding: 12px 16px;
- 415 border-bottom: 1px solid var(--border);
- 416 cursor: pointer;
- 417 transition: background 0.2s ease;
- 418 }
- 419
- 420 .episode-item:hover {
- 421 background: #f8f9fa;
- 422 }
- 423
- 424 .episode-item.selected {
- 425 background: #e8f0fe;
- 426 border-left: 4px solid var(--primary);
- 427 }
- 428
- 429 .episode-title {
- 430 font-weight: 500;
- 431 margin-bottom: 4px;
- 432 }
- 433
- 434 .episode-meta {
- 435 font-size: 14px;
- 436 color: var(--gray);
- 437 }
- 438
- 439 .auto-populated {
- 440 background: #f0f7ff !important;
- 441 border-color: var(--primary) !important;
- 442 }
- 443
- 444 .field-status {
- 445 display: flex;
- 446 justify-content: space-between;
- 447 margin-bottom: 4px;
- 448 }
- 449
- 450 .field-source {
- 451 font-size: 12px;
- 452 color: var(--gray);
- 453 font-style: italic;
- 454 }
- 455
- 456 .source-rss {
- 457 color: var(--primary);
- 458 }
- 459
- 460 .source-manual {
- 461 color: var(--secondary);
- 462 }
- 463
- 464 .source-missing {
- 465 color: var(--warning);
- 466 }
- 467
- 468 @media (max-width: 768px) {
- 469 .container {
- 470 margin: 0;
- 471 border-radius: 0;
- 472 }
- 473
- 474 .header {
- 475 padding: 20px;
- 476 }
- 477
- 478 .header h1 {
- 479 font-size: 2rem;
- 480 }
- 481
- 482 .tab {
- 483 padding: 12px 16px;
- 484 font-size: 14px;
- 485 }
- 486
- 487 .tab-content {
- 488 padding: 20px;
- 489 }
- 490
- 491 .form-row {
- 492 grid-template-columns: 1fr;
- 493 }
- 494 }
- 495 </style>
- 496</head>
- 497<body>
- 498 <div class="container">
- 499 <!-- Header -->
- 500 <div class="header">
- 501 <h1>🎙️ Podcast Episode Manager</h1>
- 502 <p>Import from RSS, edit, and manage your podcast episodes</p>
- 503 </div>
- 504
- 505 <!-- Tabs -->
- 506 <div class="tabs">
- 507 <button class="tab active" onclick="switchTab('import')">📥 Import RSS</button>
- 508 <button class="tab" onclick="switchTab('episode')">🎧 Episode Details</button>
- 509 <button class="tab" onclick="switchTab('media')">🎵 Media & Files</button>
- 510 <button class="tab" onclick="switchTab('publish')">🚀 Publish & Distribute</button>
- 511 <button class="tab" onclick="switchTab('workflow')">⚙️ Workflow</button>
- 512 </div>
- 513
- 514 <!-- Import Tab -->
- 515 <div id="import-tab" class="tab-content active">
- 516 <div class="rss-import-section">
- 517 <h3>Import Episode from RSS Feed</h3>
- 518 <p>Paste your RSS feed URL to auto-populate episode details</p>
- 519
- 520 <div class="form-group">
- 521 <label for="rss-url">RSS Feed URL</label>
- 522 <div style="display: flex; gap: 10px;">
- 523 <input type="url" id="rss-url" placeholder="https://anchor.fm/s/98e2f3e0/podcast/rss" style="flex: 1;">
- 524 <button class="btn btn-primary" onclick="fetchRSS()">Fetch Episodes</button>
- 525 </div>
- 526 </div>
- 527
- 528 <div id="episode-list" class="episode-selector" style="display: none;">
- 529 <!-- Episode list will be populated here -->
- 530 </div>
- 531
- 532 <div id="import-status" style="margin-top: 16px; display: none;">
- 533 <div class="progress-bar">
- 534 <div class="progress-fill" id="import-progress"></div>
- 535 </div>
- 536 <p id="import-message" style="margin-top: 8px;"></p>
- 537 </div>
- 538 </div>
- 539
- 540 <div class="section">
- 541 <div class="section-header">
- 542 <h3>Currently Imported Episode</h3>
- 543 <span id="import-status-badge" class="status-badge status-draft">No Data</span>
- 544 </div>
- 545 <div class="section-content">
- 546 <div id="current-episode-info" style="text-align: center; padding: 40px; color: var(--gray);">
- 547 <p>No episode imported yet.</p>
- 548 <p>Fetch an RSS feed and select an episode to get started.</p>
- 549 </div>
- 550 </div>
- 551 </div>
- 552 </div>
- 553
- 554 <!-- Episode Details Tab -->
- 555 <div id="episode-tab" class="tab-content">
- 556 <div class="section">
- 557 <div class="section-header">
- 558 <h3>Core Episode Information</h3>
- 559 <span id="episode-status" class="status-badge status-draft">Draft</span>
- 560 </div>
- 561 <div class="section-content">
- 562 <div class="form-row">
- 563 <div class="form-group">
- 564 <label for="episode_title" class="required">Episode Title</label>
- 565 <div class="field-status">
- 566 <span>Required</span>
- 567 <span id="episode_title_source" class="field-source source-missing">Missing</span>
- 568 </div>
- 569 <input type="text" id="episode_title" name="episode_title" placeholder="Enter episode title">
- 570 </div>
- 571
- 572 <div class="form-group">
- 573 <label for="episode_subtitle">Subtitle</label>
- 574 <div class="field-status">
- 575 <span>Optional</span>
- 576 <span id="episode_subtitle_source" class="field-source source-missing">Missing</span>
- 577 </div>
- 578 <input type="text" id="episode_subtitle" name="episode_subtitle" placeholder="Short description (max 150 chars)">
- 579 </div>
- 580 </div>
- 581
- 582 <div class="form-row">
- 583 <div class="form-group">
- 584 <label for="season_number">Season Number</label>
- 585 <div class="field-status">
- 586 <span>Optional</span>
- 587 <span id="season_number_source" class="field-source source-missing">Missing</span>
- 588 </div>
- 589 <input type="number" id="season_number" name="season_number" min="1" placeholder="1">
- 590 </div>
- 591
- 592 <div class="form-group">
- 593 <label for="episode_number">Episode Number</label>
- 594 <div class="field-status">
- 595 <span>Optional</span>
- 596 <span id="episode_number_source" class="field-source source-missing">Missing</span>
- 597 </div>
- 598 <input type="number" id="episode_number" name="episode_number" min="1" placeholder="1">
- 599 </div>
- 600
- 601 <div class="form-group">
- 602 <label for="series_information">Series Information</label>
- 603 <div class="field-status">
- 604 <span>Optional</span>
- 605 <span id="series_information_source" class="field-source source-missing">Manual</span>
- 606 </div>
- 607 <input type="text" id="series_information" name="series_information" placeholder="e.g., Part 2 of 3">
- 608 </div>
- 609 </div>
- 610
- 611 <div class="form-row">
- 612 <div class="form-group">
- 613 <label for="publication_datetime" class="required">Publication Date & Time</label>
- 614 <div class="field-status">
- 615 <span>Required</span>
- 616 <span id="publication_datetime_source" class="field-source source-missing">Missing</span>
- 617 </div>
- 618 <input type="datetime-local" id="publication_datetime" name="publication_datetime">
- 619 </div>
- 620
- 621 <div class="form-group">
- 622 <label for="episode_duration">Duration</label>
- 623 <div class="field-status">
- 624 <span>Optional</span>
- 625 <span id="episode_duration_source" class="field-source source-missing">Missing</span>
- 626 </div>
- 627 <input type="text" id="episode_duration" name="episode_duration" placeholder="HH:MM:SS">
- 628 </div>
- 629
- 630 <div class="form-group">
- 631 <label for="explicit_content">Explicit Content</label>
- 632 <div class="field-status">
- 633 <span>Required</span>
- 634 <span id="explicit_content_source" class="field-source source-missing">Missing</span>
- 635 </div>
- 636 <select id="explicit_content" name="explicit_content">
- 637 <option value="">Select</option>
- 638 <option value="no">No (Clean)</option>
- 639 <option value="yes">Yes (Explicit)</option>
- 640 </select>
- 641 </div>
- 642 </div>
- 643 </div>
- 644 </div>
- 645
- 646 <div class="section">
- 647 <div class="section-header">
- 648 <h3>Content & Format</h3>
- 649 </div>
- 650 <div class="section-content">
- 651 <div class="form-row">
- 652 <div class="form-group">
- 653 <label for="content_format" class="required">Content Format</label>
- 654 <div class="field-status">
- 655 <span>Required</span>
- 656 <span id="content_format_source" class="field-source source-missing">Manual</span>
- 657 </div>
- 658 <select id="content_format" name="content_format">
- 659 <option value="">Select format</option>
- 660 <option value="interview">Interview</option>
- 661 <option value="solo">Solo/Monologue</option>
- 662 <option value="panel">Panel Discussion</option>
- 663 <option value="deep-dive">Deep Dive</option>
- 664 <option value="storytelling">Storytelling</option>
- 665 <option value="mixtape">Mixtape/Curation</option>
- 666 <option value="qa">Q&A</option>
- 667 <option value="live">Live Recording</option>
- 668 </select>
- 669 </div>
- 670
- 671 <div class="form-group">
- 672 <label for="primary_category" class="required">Primary Category</label>
- 673 <div class="field-status">
- 674 <span>Required</span>
- 675 <span id="primary_category_source" class="field-source source-missing">Missing</span>
- 676 </div>
- 677 <select id="primary_category" name="primary_category">
- 678 <option value="">Select category</option>
- 679 <option value="arts">Arts</option>
- 680 <option value="business">Business</option>
- 681 <option value="comedy">Comedy</option>
- 682 <option value="education">Education</option>
- 683 <option value="news">News</option>
- 684 <option value="society">Society & Culture</option>
- 685 <option value="sports">Sports</option>
- 686 <option value="technology">Technology</option>
- 687 </select>
- 688 </div>
- 689 </div>
- 690
- 691 <div class="form-group">
- 692 <label for="topics_keywords">Topics & Keywords</label>
- 693 <div class="field-status">
- 694 <span>Optional</span>
- 695 <span id="topics_keywords_source" class="field-source source-missing">Manual</span>
- 696 </div>
- 697 <div class="tag-input" id="topics_keywords_container">
- 698 <input type="text" id="topics_keywords_input" placeholder="Type and press Enter to add keywords">
- 699 </div>
- 700 </div>
- 701
- 702 <div class="form-group">
- 703 <label for="episode_summary" class="required">Episode Summary</label>
- 704 <div class="field-status">
- 705 <span>Required</span>
- 706 <span id="episode_summary_source" class="field-source source-missing">Missing</span>
- 707 </div>
- 708 <textarea id="episode_summary" name="episode_summary" placeholder="Write a detailed summary of the episode..."></textarea>
- 709 </div>
- 710 </div>
- 711 </div>
- 712
- 713 <div class="section">
- 714 <div class="section-header">
- 715 <h3>People</h3>
- 716 <button type="button" class="btn btn-outline btn-small" onclick="addGuest()">+ Add Guest</button>
- 717 </div>
- 718 <div class="section-content">
- 719 <div class="form-group">
- 720 <label for="hosts" class="required">Host(s)</label>
- 721 <div class="field-status">
- 722 <span>Required</span>
- 723 <span id="hosts_source" class="field-source source-missing">Missing</span>
- 724 </div>
- 725 <select id="hosts" name="hosts" multiple style="height: 100px;">
- 726 <option value="john_doe">John Doe</option>
- 727 <option value="jane_smith">Jane Smith</option>
- 728 <option value="alex_johnson">Alex Johnson</option>
- 729 </select>
- 730 <small>Hold Ctrl/Cmd to select multiple hosts</small>
- 731 </div>
- 732
- 733 <div id="guests-container">
- 734 <!-- Guest entries will be added here -->
- 735 </div>
- 736 </div>
- 737 </div>
- 738
- 739 <div class="section">
- 740 <div class="section-header">
- 741 <h3>Show Notes & Links</h3>
- 742 </div>
- 743 <div class="section-content">
- 744 <div class="form-group">
- 745 <label for="show_notes">Detailed Show Notes</label>
- 746 <div class="field-status">
- 747 <span>Optional</span>
- 748 <span id="show_notes_source" class="field-source source-missing">Missing</span>
- 749 </div>
- 750 <textarea id="show_notes" name="show_notes" placeholder="Include timestamps, links, and key takeaways..."></textarea>
- 751 </div>
- 752
- 753 <div id="resources-container">
- 754 <!-- Resource links will be added here -->
- 755 </div>
- 756
- 757 <div class="form-row">
- 758 <div class="form-group">
- 759 <label for="primary_cta_text">Primary CTA Text</label>
- 760 <input type="text" id="primary_cta_text" name="primary_cta_text" placeholder="e.g., Get the Free Guide">
- 761 </div>
- 762 <div class="form-group">
- 763 <label for="primary_cta_url">Primary CTA URL</label>
- 764 <input type="url" id="primary_cta_url" name="primary_cta_url" placeholder="https://example.com/guide">
- 765 </div>
- 766 </div>
- 767
- 768 <div class="form-row">
- 769 <div class="form-group">
- 770 <label for="secondary_cta_text">Secondary CTA Text</label>
- 771 <input type="text" id="secondary_cta_text" name="secondary_cta_text" placeholder="e.g., Subscribe to Newsletter">
- 772 </div>
- 773 <div class="form-group">
- 774 <label for="secondary_cta_url">Secondary CTA URL</label>
- 775 <input type="url" id="secondary_cta_url" name="secondary_cta_url" placeholder="https://example.com/newsletter">
- 776 </div>
- 777 </div>
- 778 </div>
- 779 </div>
- 780 </div>
- 781
- 782 <!-- Media & Files Tab -->
- 783 <div id="media-tab" class="tab-content">
- 784 <div class="section">
- 785 <div class="section-header">
- 786 <h3>Episode Media</h3>
- 787 </div>
- 788 <div class="section-content">
- 789 <div class="form-group">
- 790 <label for="episode_artwork" class="required">Episode Artwork</label>
- 791 <div class="field-status">
- 792 <span>Required</span>
- 793 <span id="episode_artwork_source" class="field-source source-missing">Missing</span>
- 794 </div>
- 795 <input type="file" id="episode_artwork" name="episode_artwork" accept="image/*" onchange="previewImage(this)">
- 796 <div id="artwork-preview" class="file-preview" style="display: none;">
- 797 <img id="artwork-thumbnail" src="" alt="Artwork Preview">
- 798 <div class="file-info">
- 799 <div class="file-name" id="artwork-name"></div>
- 800 <div class="file-size" id="artwork-size"></div>
- 801 </div>
- 802 <button type="button" class="btn btn-danger btn-small" onclick="clearArtwork()">Remove</button>
- 803 </div>
- 804 <small>Square image, 3000x3000px recommended, JPG or PNG</small>
- 805 </div>
- 806
- 807 <div class="form-group">
- 808 <label for="audio_file" class="required">Audio File (MP3)</label>
- 809 <div class="field-status">
- 810 <span>Required</span>
- 811 <span id="audio_file_source" class="field-source source-missing">Missing</span>
- 812 </div>
- 813 <input type="file" id="audio_file" name="audio_file" accept="audio/mpeg" onchange="previewAudio(this)">
- 814 <div id="audio-preview" class="file-preview" style="display: none;">
- 815 <div class="file-info">
- 816 <div class="file-name" id="audio-name"></div>
- 817 <div class="file-size" id="audio-size"></div>
- 818 </div>
- 819 </div>
- 820 <small>MP3 format, max 200MB</small>
- 821 </div>
- 822
- 823 <div class="form-group">
- 824 <label for="transcript_file">Transcript (TXT)</label>
- 825 <input type="file" id="transcript_file" name="transcript_file" accept=".txt">
- 826 </div>
- 827
- 828 <div class="form-group">
- 829 <label for="subtitles_file">Subtitles (SRT/VTT)</label>
- 830 <input type="file" id="subtitles_file" name="subtitles_file" accept=".srt,.vtt">
- 831 </div>
- 832
- 833 <div class="form-group">
- 834 <label for="glossary_file">Glossary / Unique Words</label>
- 835 <input type="file" id="glossary_file" name="glossary_file" accept=".txt,.json">
- 836 </div>
- 837 </div>
- 838 </div>
- 839 </div>
- 840
- 841 <!-- Publish & Distribute Tab -->
- 842 <div id="publish-tab" class="tab-content">
- 843 <div class="section">
- 844 <div class="section-header">
- 845 <h3>Publishing Settings</h3>
- 846 </div>
- 847 <div class="section-content">
- 848 <div class="form-group">
- 849 <label for="episode_slug">Episode Slug / URL</label>
- 850 <div class="field-status">
- 851 <span>Optional</span>
- 852 <span id="episode_slug_source" class="field-source source-missing">Auto-generated</span>
- 853 </div>
- 854 <input type="text" id="episode_slug" name="episode_slug" placeholder="auto-generated-from-title">
- 855 <small>Used for the episode URL. Will be auto-generated from title if left empty.</small>
- 856 </div>
- 857
- 858 <div class="form-group">
- 859 <label for="meta_description">Meta Description</label>
- 860 <textarea id="meta_description" name="meta_description" maxlength="155" placeholder="SEO description for search engines"></textarea>
- 861 <small><span id="meta-desc-count">0</span>/155 characters</small>
- 862 </div>
- 863
- 864 <div class="form-group">
- 865 <label>Distribution Channels</label>
- 866 <div class="checkbox-group">
- 867 <div class="checkbox-item">
- 868 <input type="checkbox" id="publish_apple" name="publish_apple" checked>
- 869 <label for="publish_apple">Apple Podcasts</label>
- 870 </div>
- 871 <div class="checkbox-item">
- 872 <input type="checkbox" id="publish_spotify" name="publish_spotify" checked>
- 873 <label for="publish_spotify">Spotify</label>
- 874 </div>
- 875 <div class="checkbox-item">
- 876 <input type="checkbox" id="publish_youtube" name="publish_youtube">
- 877 <label for="publish_youtube">YouTube</label>
- 878 </div>
- 879 <div class="checkbox-item">
- 880 <input type="checkbox" id="include_newsletter" name="include_newsletter" checked>
- 881 <label for="include_newsletter">Newsletter</label>
- 882 </div>
- 883 </div>
- 884 </div>
- 885 </div>
- 886 </div>
- 887
- 888 <div class="section">
- 889 <div class="section-header">
- 890 <h3>Related Content</h3>
- 891 </div>
- 892 <div class="section-content">
- 893 <div class="form-group">
- 894 <label for="related_episodes">Related Episodes</label>
- 895 <select id="related_episodes" name="related_episodes" multiple style="height: 100px;">
- 896 <option value="ep1">Previous Episode: Getting Started</option>
- 897 <option value="ep2">Next Episode: Advanced Topics</option>
- 898 <option value="ep3">Interview with Expert</option>
- 899 </select>
- 900 <small>Hold Ctrl/Cmd to select multiple episodes</small>
- 901 </div>
- 902
- 903 <div class="form-group">
- 904 <label for="merchandise_ideas">Merchandise Ideas</label>
- 905 <div class="tag-input" id="merchandise_container">
- 906 <input type="text" id="merchandise_input" placeholder="Type and press Enter to add merch ideas">
- 907 </div>
- 908 </div>
- 909 </div>
- 910 </div>
- 911 </div>
- 912
- 913 <!-- Workflow Tab -->
- 914 <div id="workflow-tab" class="tab-content">
- 915 <div class="section">
- 916 <div class="section-header">
- 917 <h3>Workflow & Status</h3>
- 918 </div>
- 919 <div class="section-content">
- 920 <div class="form-row">
- 921 <div class="form-group">
- 922 <label for="internal_status">Internal Status</label>
- 923 <select id="internal_status" name="internal_status">
- 924 <option value="draft">Draft</option>
- 925 <option value="in_review">In Review</option>
- 926 <option value="ready">Ready to Publish</option>
- 927 <option value="scheduled">Scheduled</option>
- 928 <option value="published">Published</option>
- 929 </select>
- 930 </div>
- 931
- 932 <div class="form-group">
- 933 <label for="assigned_editor">Assigned Editor</label>
- 934 <select id="assigned_editor" name="assigned_editor">
- 935 <option value="">Unassigned</option>
- 936 <option value="editor1">Jane Smith</option>
- 937 <option value="editor2">John Doe</option>
- 938 <option value="editor3">Alex Johnson</option>
- 939 </select>
- 940 </div>
- 941 </div>
- 942
- 943 <div class="form-group">
- 944 <label for="internal_notes">Internal Notes</label>
- 945 <textarea id="internal_notes" name="internal_notes" placeholder="Notes for the team..."></textarea>
- 946 </div>
- 947 </div>
- 948 </div>
- 949
- 950 <div class="section">
- 951 <div class="section-header">
- 952 <h3>Timestamps (Chapters)</h3>
- 953 <button type="button" class="btn btn-outline btn-small" onclick="addTimestamp()">+ Add Timestamp</button>
- 954 </div>
- 955 <div class="section-content">
- 956 <div id="chapters-container">
- 957 <!-- Chapter entries will be added here -->
- 958 </div>
- 959 </div>
- 960 </div>
- 961 </div>
- 962
- 963 <!-- Form Actions -->
- 964 <div class="form-actions">
- 965 <button type="button" class="btn btn-outline" onclick="resetForm()">Reset Form</button>
- 966 <button type="button" class="btn btn-secondary" onclick="saveDraft()">Save Draft</button>
- 967 <button type="button" class="btn btn-primary" onclick="submitForm()">Publish Episode</button>
- 968 </div>
- 969 </div>
- 970
- 971 <script>
- 972 // Global variables
- 973 let currentEpisode = null;
- 974 let episodeData = {
- 975 // Initialize with default values
- 976 episode_title: '',
- 977 episode_subtitle: '',
- 978 season_number: '',
- 979 episode_number: '',
- 980 series_information: '',
- 981 publication_datetime: '',
- 982 episode_duration: '',
- 983 explicit_content: '',
- 984 content_format: '',
- 985 primary_category: '',
- 986 topics_keywords: [],
- 987 episode_summary: '',
- 988 hosts: [],
- 989 guests: [],
- 990 show_notes: '',
- 991 resources_links: [],
- 992 primary_cta_text: '',
- 993 primary_cta_url: '',
- 994 secondary_cta_text: '',
- 995 secondary_cta_url: '',
- 996 chapters: [],
- 997 episode_artwork: null,
- 998 audio_file: null,
- 999 transcript_file: null,
- 1000 subtitles_file: null,
- 1001 glossary_file: null,
- 1002 episode_slug: '',
- 1003 meta_description: '',
- 1004 publish_apple: true,
- 1005 publish_spotify: true,
- 1006 publish_youtube: false,
- 1007 include_newsletter: true,
- 1008 related_episodes: [],
- 1009 merchandise_ideas: [],
- 1010 internal_status: 'draft',
- 1011 internal_notes: '',
- 1012 assigned_editor: '',
- 1013 // Track field sources
- 1014 field_sources: {}
- 1015 };
- 1016
- 1017 // Tab switching
- 1018 function switchTab(tabName) {
- 1019 // Hide all tabs
- 1020 document.querySelectorAll('.tab-content').forEach(tab => {
- 1021 tab.classList.remove('active');
- 1022 });
- 1023
- 1024 // Remove active class from all tabs
- 1025 document.querySelectorAll('.tab').forEach(tab => {
- 1026 tab.classList.remove('active');
- 1027 });
- 1028
- 1029 // Show selected tab
- 1030 document.getElementById(`${tabName}-tab`).classList.add('active');
- 1031
- 1032 // Activate corresponding tab button
- 1033 document.querySelectorAll('.tab').forEach(tab => {
- 1034 if (tab.textContent.includes(tabName.charAt(0).toUpperCase() + tabName.slice(1))) {
- 1035 tab.classList.add('active');
- 1036 }
- 1037 });
- 1038 }
- 1039
- 1040 // Fetch RSS feed
- 1041 async function fetchRSS() {
- 1042 const rssUrl = document.getElementById('rss-url').value;
- 1043 if (!rssUrl) {
- 1044 alert('Please enter an RSS feed URL');
- 1045 return;
- 1046 }
- 1047
- 1048 const importStatus = document.getElementById('import-status');
- 1049 const importProgress = document.getElementById('import-progress');
- 1050 const importMessage = document.getElementById('import-message');
- 1051
- 1052 importStatus.style.display = 'block';
- 1053 importProgress.style.width = '30%';
- 1054 importMessage.textContent = 'Fetching RSS feed...';
- 1055
- 1056 try {
- 1057 // Note: In a real implementation, you would use a CORS proxy
- 1058 // or server-side endpoint to fetch RSS feeds
- 1059 const response = await fetch(`https://api.allorigins.win/get?url=${encodeURIComponent(rssUrl)}`);
- 1060 const data = await response.json();
- 1061
- 1062 importProgress.style.width = '60%';
- 1063 importMessage.textContent = 'Parsing RSS feed...';
- 1064
- 1065 // Parse the RSS feed
- 1066 const parser = new DOMParser();
- 1067 const xmlDoc = parser.parseFromString(data.contents, 'text/xml');
- 1068
- 1069 // Extract channel info
- 1070 const channel = xmlDoc.querySelector('channel');
- 1071 const episodes = xmlDoc.querySelectorAll('item');
- 1072
- 1073 // Display episodes
- 1074 const episodeList = document.getElementById('episode-list');
- 1075 episodeList.innerHTML = '';
- 1076 episodeList.style.display = 'block';
- 1077
- 1078 episodes.forEach((episode, index) => {
- 1079 const title = episode.querySelector('title')?.textContent || `Episode ${index + 1}`;
- 1080 const pubDate = episode.querySelector('pubDate')?.textContent || '';
- 1081 const guid = episode.querySelector('guid')?.textContent || '';
- 1082
- 1083 const episodeDiv = document.createElement('div');
- 1084 episodeDiv.className = 'episode-item';
- 1085 episodeDiv.dataset.index = index;
- 1086 episodeDiv.dataset.guid = guid;
- 1087 episodeDiv.innerHTML = `
- 1088 <div class="episode-title">${title}</div>
- 1089 <div class="episode-meta">${pubDate}</div>
- 1090 `;
- 1091
- 1092 episodeDiv.onclick = () => selectEpisode(episode, channel);
- 1093 episodeList.appendChild(episodeDiv);
- 1094 });
- 1095
- 1096 importProgress.style.width = '100%';
- 1097 importMessage.textContent = `Found ${episodes.length} episodes`;
- 1098
- 1099 setTimeout(() => {
- 1100 importStatus.style.display = 'none';
- 1101 }, 2000);
- 1102
- 1103 } catch (error) {
- 1104 console.error('Error fetching RSS:', error);
- 1105 importProgress.style.width = '0%';
- 1106 importMessage.textContent = 'Error fetching RSS feed. Please check the URL and try again.';
- 1107 importMessage.style.color = 'var(--danger)';
- 1108 }
- 1109 }
- 1110
- 1111 // Select episode from RSS
- 1112 function selectEpisode(episodeElement, channelElement) {
- 1113 // Update UI
- 1114 document.querySelectorAll('.episode-item').forEach(item => {
- 1115 item.classList.remove('selected');
- 1116 });
- 1117 event.currentTarget.classList.add('selected');
- 1118
- 1119 // Parse RSS data
- 1120 const rssData = {
- 1121 // Core information
- 1122 episode_title: getRssValue(episodeElement, 'title'),
- 1123 episode_subtitle: getRssValue(episodeElement, 'itunes|subtitle'),
- 1124 episode_summary: getRssValue(episodeElement, 'description') ||
- 1125 getRssValue(episodeElement, 'content|encoded') ||
- 1126 getRssValue(episodeElement, 'itunes|summary'),
- 1127 publication_datetime: getRssValue(episodeElement, 'pubDate'),
- 1128 episode_duration: getRssValue(episodeElement, 'itunes|duration'),
- 1129 explicit_content: getRssValue(episodeElement, 'itunes|explicit'),
- 1130 episode_number: getRssValue(episodeElement, 'itunes|episode'),
- 1131 season_number: getRssValue(episodeElement, 'itunes|season'),
- 1132 content_format: getRssValue(episodeElement, 'itunes|episodeType'),
- 1133
- 1134 // Media
- 1135 episode_artwork: getRssAttribute(episodeElement, 'itunes|image', 'href') ||
- 1136 getRssAttribute(channelElement, 'itunes|image', 'href'),
- 1137 audio_file: getRssAttribute(episodeElement, 'enclosure', 'url'),
- 1138 transcript_file: getRssAttribute(episodeElement, 'podcast|transcript', 'url'),
- 1139
- 1140 // Channel info
- 1141 primary_category: getRssAttribute(channelElement, 'itunes|category', 'text'),
- 1142 hosts: [getRssValue(channelElement, 'itunes|author') ||
- 1143 getRssValue(channelElement, 'dc|creator')].filter(Boolean),
- 1144
- 1145 // URLs
- 1146 episode_slug: getRssValue(episodeElement, 'link') ||
- 1147 getRssValue(episodeElement, 'guid'),
- 1148 meta_description: getRssValue(episodeElement, 'itunes|summary')
- 1149 };
- 1150
- 1151 // Update form with RSS data
- 1152 updateFormWithRSSData(rssData);
- 1153
- 1154 // Update current episode display
- 1155 document.getElementById('current-episode-info').innerHTML = `
- 1156 <h3>${rssData.episode_title || 'Untitled Episode'}</h3>
- 1157 <p>${rssData.episode_subtitle || ''}</p>
- 1158 <p><small>Duration: ${rssData.episode_duration || 'N/A'} |
- 1159 Explicit: ${rssData.explicit_content || 'N/A'}</small></p>
- 1160 <button class="btn btn-outline" onclick="switchTab('episode')">
- 1161 Edit Episode Details →
- 1162 </button>
- 1163 `;
- 1164
- 1165 document.getElementById('import-status-badge').textContent = 'Imported';
- 1166 document.getElementById('import-status-badge').className = 'status-badge status-ready';
- 1167
- 1168 // Switch to episode tab
- 1169 switchTab('episode');
- 1170 }
- 1171
- 1172 // Helper function to get RSS value
- 1173 function getRssValue(element, name) {
- 1174 if (!element) return '';
- 1175
- 1176 // Handle namespaced elements
- 1177 const parts = name.split('|');
- 1178 if (parts.length === 2) {
- 1179 const [namespace, localName] = parts;
- 1180 return element.getElementsByTagNameNS(`http://www.itunes.com/dtds/podcast-1.0.dtd`, localName)[0]?.textContent || '';
- 1181 }
- 1182
- 1183 return element.querySelector(name)?.textContent || '';
- 1184 }
- 1185
- 1186 // Helper function to get RSS attribute
- 1187 function getRssAttribute(element, name, attr) {
- 1188 if (!element) return '';
- 1189
- 1190 const parts = name.split('|');
- 1191 if (parts.length === 2) {
- 1192 const [namespace, localName] = parts;
- 1193 const elem = element.getElementsByTagNameNS(`http://www.itunes.com/dtds/podcast-1.0.dtd`, localName)[0];
- 1194 return elem?.getAttribute(attr) || '';
- 1195 }
- 1196
- 1197 return element.querySelector(name)?.getAttribute(attr) || '';
- 1198 }
- 1199
- 1200 // Update form with RSS data
- 1201 function updateFormWithRSSData(rssData) {
- 1202 // Store the data
- 1203 currentEpisode = rssData;
- 1204
- 1205 // Update each field
- 1206 for (const [key, value] of Object.entries(rssData)) {
- 1207 if (value) {
- 1208 const element = document.getElementById(key);
- 1209 if (element) {
- 1210 element.value = value;
- 1211
- 1212 // Mark as auto-populated
- 1213 element.classList.add('auto-populated');
- 1214
- 1215 // Update field source indicator
- 1216 const sourceElement = document.getElementById(`${key}_source`);
- 1217 if (sourceElement) {
- 1218 sourceElement.textContent = 'RSS';
- 1219 sourceElement.className = 'field-source source-rss';
- 1220 }
- 1221
- 1222 // Store in episodeData
- 1223 episodeData[key] = value;
- 1224 episodeData.field_sources[key] = 'rss';
- 1225 }
- 1226 }
- 1227 }
- 1228
- 1229 // Special handling for arrays
- 1230 if (rssData.hosts && rssData.hosts.length > 0) {
- 1231 const hostsSelect = document.getElementById('hosts');
- 1232 Array.from(hostsSelect.options).forEach(option => {
- 1233 if (rssData.hosts.some(host => option.text.toLowerCase().includes(host.toLowerCase()))) {
- 1234 option.selected = true;
- 1235 }
- 1236 });
- 1237 }
- 1238
- 1239 // Update meta description counter
- 1240 updateMetaDescCounter();
- 1241
- 1242 // Update status badge
- 1243 document.getElementById('episode-status').textContent = 'Imported';
- 1244 document.getElementById('episode-status').className = 'status-badge status-ready';
- 1245 }
- 1246
- 1247 // Add guest
- 1248 function addGuest() {
- 1249 const container = document.getElementById('guests-container');
- 1250 const guestCount = container.children.length + 1;
- 1251
- 1252 const guestDiv = document.createElement('div');
- 1253 guestDiv.className = 'repeater-item';
- 1254 guestDiv.innerHTML = `
- 1255 <div class="repeater-header">
- 1256 <span class="repeater-title">Guest #${guestCount}</span>
- 1257 <button type="button" class="btn btn-danger btn-small" onclick="removeGuest(this)">Remove</button>
- 1258 </div>
- 1259 <div class="form-row">
- 1260 <div class="form-group">
- 1261 <label>Guest Name</label>
- 1262 <input type="text" name="guest_name[]" placeholder="Full name">
- 1263 </div>
- 1264 <div class="form-group">
- 1265 <label>Title / Company</label>
- 1266 <input type="text" name="guest_title[]" placeholder="Position and company">
- 1267 </div>
- 1268 </div>
- 1269 <div class="form-group">
- 1270 <label>Bio</label>
- 1271 <textarea name="guest_bio[]" placeholder="Short bio"></textarea>
- 1272 </div>
- 1273 <div class="form-row">
- 1274 <div class="form-group">
- 1275 <label>Website / Social Link</label>
- 1276 <input type="url" name="guest_website[]" placeholder="https://">
- 1277 </div>
- 1278 <div class="form-group">
- 1279 <label>Headshot</label>
- 1280 <input type="file" name="guest_headshot[]" accept="image/*">
- 1281 </div>
- 1282 </div>
- 1283 `;
- 1284
- 1285 container.appendChild(guestDiv);
- 1286 }
- 1287
- 1288 // Remove guest
- 1289 function removeGuest(button) {
- 1290 button.closest('.repeater-item').remove();
- 1291 updateGuestNumbers();
- 1292 }
- 1293
- 1294 // Update guest numbers
- 1295 function updateGuestNumbers() {
- 1296 const guests = document.querySelectorAll('#guests-container .repeater-item');
- 1297 guests.forEach((guest, index) => {
- 1298 guest.querySelector('.repeater-title').textContent = `Guest #${index + 1}`;
- 1299 });
- 1300 }
- 1301
- 1302 // Add timestamp
- 1303 function addTimestamp() {
- 1304 const container = document.getElementById('chapters-container');
- 1305 const chapterCount = container.children.length + 1;
- 1306
- 1307 const chapterDiv = document.createElement('div');
- 1308 chapterDiv.className = 'repeater-item';
- 1309 chapterDiv.innerHTML = `
- 1310 <div class="repeater-header">
- 1311 <span class="repeater-title">Chapter #${chapterCount}</span>
- 1312 <button type="button" class="btn btn-danger btn-small" onclick="removeChapter(this)">Remove</button>
- 1313 </div>
- 1314 <div class="form-row">
- 1315 <div class="form-group">
- 1316 <label>Timecode</label>
- 1317 <input type="text" name="chapter_timecode[]" placeholder="HH:MM:SS">
- 1318 </div>
- 1319 <div class="form-group">
- 1320 <label>Chapter Title</label>
- 1321 <input type="text" name="chapter_title[]" placeholder="Introduction">
- 1322 </div>
- 1323 </div>
- 1324 <div class="form-group">
- 1325 <label>Description</label>
- 1326 <textarea name="chapter_description[]" placeholder="Optional description"></textarea>
- 1327 </div>
- 1328 `;
- 1329
- 1330 container.appendChild(chapterDiv);
- 1331 }
- 1332
- 1333 // Remove chapter
- 1334 function removeChapter(button) {
- 1335 button.closest('.repeater-item').remove();
- 1336 updateChapterNumbers();
- 1337 }
- 1338
- 1339 // Update chapter numbers
- 1340 function updateChapterNumbers() {
- 1341 const chapters = document.querySelectorAll('#chapters-container .repeater-item');
- 1342 chapters.forEach((chapter, index) => {
- 1343 chapter.querySelector('.repeater-title').textContent = `Chapter #${index + 1}`;
- 1344 });
- 1345 }
- 1346
- 1347 // Preview image
- 1348 function previewImage(input) {
- 1349 if (input.files && input.files[0]) {
- 1350 const file = input.files[0];
- 1351 const reader = new FileReader();
- 1352
- 1353 reader.onload = function(e) {
- 1354 const preview = document.getElementById('artwork-preview');
- 1355 const thumbnail = document.getElementById('artwork-thumbnail');
- 1356 const name = document.getElementById('artwork-name');
- 1357 const size = document.getElementById('artwork-size');
- 1358
- 1359 thumbnail.src = e.target.result;
- 1360 name.textContent = file.name;
- 1361 size.textContent = formatFileSize(file.size);
- 1362 preview.style.display = 'flex';
- 1363
- 1364 // Update field source
- 1365 const sourceElement = document.getElementById('episode_artwork_source');
- 1366 sourceElement.textContent = 'Manual Upload';
- 1367 sourceElement.className = 'field-source source-manual';
- 1368
- 1369 input.classList.remove('auto-populated');
- 1370 };
- 1371
- 1372 reader.readAsDataURL(file);
- 1373 }
- 1374 }
- 1375
- 1376 // Preview audio
- 1377 function previewAudio(input) {
- 1378 if (input.files && input.files[0]) {
- 1379 const file = input.files[0];
- 1380 const preview = document.getElementById('audio-preview');
- 1381 const name = document.getElementById('audio-name');
- 1382 const size = document.getElementById('audio-size');
- 1383
- 1384 name.textContent = file.name;
- 1385 size.textContent = formatFileSize(file.size);
- 1386 preview.style.display = 'flex';
- 1387
- 1388 // Update field source
- 1389 const sourceElement = document.getElementById('audio_file_source');
- 1390 sourceElement.textContent = 'Manual Upload';
- 1391 sourceElement.className = 'field-source source-manual';
- 1392
- 1393 input.classList.remove('auto-populated');
- 1394 }
- 1395 }
- 1396
- 1397 // Clear artwork
- 1398 function clearArtwork() {
- 1399 document.getElementById('episode_artwork').value = '';
- 1400 document.getElementById('artwork-preview').style.display = 'none';
- 1401
- 1402 // Update field source
- 1403 const sourceElement = document.getElementById('episode_artwork_source');
- 1404 sourceElement.textContent = 'Missing';
- 1405 sourceElement.className = 'field-source source-missing';
- 1406 }
- 1407
- 1408 // Format file size
- 1409 function formatFileSize(bytes) {
- 1410 if (bytes === 0) return '0 Bytes';
- 1411 const k = 1024;
- 1412 const sizes = ['Bytes', 'KB', 'MB', 'GB'];
- 1413 const i = Math.floor(Math.log(bytes) / Math.log(k));
- 1414 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
- 1415 }
- 1416
- 1417 // Update meta description counter
- 1418 function updateMetaDescCounter() {
- 1419 const textarea = document.getElementById('meta_description');
- 1420 const counter = document.getElementById('meta-desc-count');
- 1421 counter.textContent = textarea.value.length;
- 1422 }
- 1423
- 1424 // Initialize tag inputs
- 1425 function initTagInputs() {
- 1426 // Topics/keywords
- 1427 const topicsInput = document.getElementById('topics_keywords_input');
- 1428 const topicsContainer = document.getElementById('topics_keywords_container');
- 1429
- 1430 topicsInput.addEventListener('keypress', function(e) {
- 1431 if (e.key === 'Enter' && this.value.trim()) {
- 1432 e.preventDefault();
- 1433 addTag(this.value.trim(), topicsContainer);
- 1434 this.value = '';
- 1435 }
- 1436 });
- 1437
- 1438 // Merchandise ideas
- 1439 const merchInput = document.getElementById('merchandise_input');
- 1440 const merchContainer = document.getElementById('merchandise_container');
- 1441
- 1442 merchInput.addEventListener('keypress', function(e) {
- 1443 if (e.key === 'Enter' && this.value.trim()) {
- 1444 e.preventDefault();
- 1445 addTag(this.value.trim(), merchContainer);
- 1446 this.value = '';
- 1447 }
- 1448 });
- 1449 }
- 1450
- 1451 // Add tag
- 1452 function addTag(text, container) {
- 1453 const tag = document.createElement('div');
- 1454 tag.className = 'tag';
- 1455 tag.innerHTML = `
- 1456 ${text}
- 1457 <button type="button" class="tag-remove" onclick="removeTag(this)">×</button>
- 1458 `;
- 1459
- 1460 // Insert before the input
- 1461 const input = container.querySelector('input');
- 1462 container.insertBefore(tag, input);
- 1463 }
- 1464
- 1465 // Remove tag
- 1466 function removeTag(button) {
- 1467 button.closest('.tag').remove();
- 1468 }
- 1469
- 1470 // Reset form
- 1471 function resetForm() {
- 1472 if (confirm('Are you sure you want to reset the form? All changes will be lost.')) {
- 1473 // Clear all inputs
- 1474 document.querySelectorAll('input, textarea, select').forEach(element => {
- 1475 if (element.type !== 'button') {
- 1476 element.value = '';
- 1477 element.classList.remove('auto-populated');
- 1478 }
- 1479 });
- 1480
- 1481 // Clear checkboxes
- 1482 document.querySelectorAll('input[type="checkbox"]').forEach(cb => {
- 1483 cb.checked = false;
- 1484 });
- 1485
- 1486 // Reset multi-selects
- 1487 document.querySelectorAll('select[multiple]').forEach(select => {
- 1488 Array.from(select.options).forEach(option => {
- 1489 option.selected = false;
- 1490 });
- 1491 });
- 1492
- 1493 // Clear file previews
- 1494 document.querySelectorAll('.file-preview').forEach(preview => {
- 1495 preview.style.display = 'none';
- 1496 });
- 1497
- 1498 // Clear containers
- 1499 document.getElementById('guests-container').innerHTML = '';
- 1500 document.getElementById('chapters-container').innerHTML = '';
- 1501
- 1502 // Reset field sources
- 1503 document.querySelectorAll('.field-source').forEach(source => {
- 1504 source.textContent = 'Missing';
- 1505 source.className = 'field-source source-missing';
- 1506 });
- 1507
- 1508 // Reset current episode
- 1509 currentEpisode = null;
- 1510 episodeData = {
- 1511 field_sources: {}
- 1512 };
- 1513
- 1514 // Update UI
- 1515 document.getElementById('current-episode-info').innerHTML = `
- 1516 <p>No episode imported yet.</p>
- 1517 <p>Fetch an RSS feed and select an episode to get started.</p>
- 1518 `;
- 1519
- 1520 document.getElementById('import-status-badge').textContent = 'No Data';
- 1521 document.getElementById('import-status-badge').className = 'status-badge status-draft';
- 1522
- 1523 document.getElementById('episode-status').textContent = 'Draft';
- 1524 document.getElementById('episode-status').className = 'status-badge status-draft';
- 1525 }
- 1526 }
- 1527
- 1528 // Save draft
- 1529 function saveDraft() {
- 1530 // Collect form data
- 1531 collectFormData();
- 1532
- 1533 // In a real app, this would save to a database
- 1534 console.log('Saving draft:', episodeData);
- 1535
- 1536 // Update UI
- 1537 document.getElementById('episode-status').textContent = 'Draft Saved';
- 1538 document.getElementById('episode-status').className = 'status-badge status-ready';
- 1539
- 1540 // Show success message
- 1541 alert('Draft saved successfully!');
- 1542 }
- 1543
- 1544 // Submit form
- 1545 function submitForm() {
- 1546 // Collect form data
- 1547 collectFormData();
- 1548
- 1549 // Validate required fields
- 1550 const requiredFields = [
- 1551 'episode_title',
- 1552 'episode_summary',
- 1553 'episode_artwork',
- 1554 'audio_file'
- 1555 ];
- 1556
- 1557 const missingFields = [];
- 1558 for (const field of requiredFields) {
- 1559 if (!episodeData[field]) {
- 1560 missingFields.push(field.replace('_', ' '));
- 1561 }
- 1562 }
- 1563
- 1564 if (missingFields.length > 0) {
- 1565 alert(`Please fill in the following required fields:\n\n• ${missingFields.join('\n• ')}`);
- 1566 return;
- 1567 }
- 1568
- 1569 // Submit to server (simulated)
- 1570 console.log('Submitting episode:', episodeData);
- 1571
- 1572 // Show success message
- 1573 alert('Episode submitted successfully!');
- 1574
- 1575 // Update status
- 1576 document.getElementById('internal_status').value = 'published';
- 1577 document.getElementById('episode-status').textContent = 'Published';
- 1578 document.getElementById('episode-status').className = 'status-badge status-published';
- 1579 }
- 1580
- 1581 // Collect form data
- 1582 function collectFormData() {
- 1583 // Collect basic fields
- 1584 const fields = [
- 1585 'episode_title', 'episode_subtitle', 'season_number', 'episode_number',
- 1586 'series_information', 'publication_datetime', 'episode_duration',
- 1587 'explicit_content', 'content_format', 'primary_category', 'episode_summary',
- 1588 'show_notes', 'primary_cta_text', 'primary_cta_url', 'secondary_cta_text',
- 1589 'secondary_cta_url', 'episode_slug', 'meta_description', 'internal_status',
- 1590 'internal_notes', 'assigned_editor'
- 1591 ];
- 1592
- 1593 fields.forEach(field => {
- 1594 const element = document.getElementById(field);
- 1595 if (element) {
- 1596 episodeData[field] = element.value;
- 1597 }
- 1598 });
- 1599
- 1600 // Collect checkboxes
- 1601 episodeData.publish_apple = document.getElementById('publish_apple').checked;
- 1602 episodeData.publish_spotify = document.getElementById('publish_spotify').checked;
- 1603 episodeData.publish_youtube = document.getElementById('publish_youtube').checked;
- 1604 episodeData.include_newsletter = document.getElementById('include_newsletter').checked;
- 1605
- 1606 // Collect multi-selects
- 1607 episodeData.hosts = Array.from(document.getElementById('hosts').selectedOptions).map(opt => opt.value);
- 1608 episodeData.related_episodes = Array.from(document.getElementById('related_episodes').selectedOptions).map(opt => opt.value);
- 1609
- 1610 // Collect tags
- 1611 episodeData.topics_keywords = Array.from(document.querySelectorAll('#topics_keywords_container .tag'))
- 1612 .map(tag => tag.textContent.replace('×', '').trim());
- 1613
- 1614 episodeData.merchandise_ideas = Array.from(document.querySelectorAll('#merchandise_container .tag'))
- 1615 .map(tag => tag.textContent.replace('×', '').trim());
- 1616
- 1617 // Collect guests
- 1618 episodeData.guests = [];
- 1619 document.querySelectorAll('#guests-container .repeater-item').forEach(item => {
- 1620 const guest = {
- 1621 guest_name: item.querySelector('input[name="guest_name[]"]')?.value || '',
- 1622 guest_title: item.querySelector('input[name="guest_title[]"]')?.value || '',
- 1623 guest_bio: item.querySelector('textarea[name="guest_bio[]"]')?.value || '',
- 1624 guest_website: item.querySelector('input[name="guest_website[]"]')?.value || '',
- 1625 guest_headshot: item.querySelector('input[name="guest_headshot[]"]')?.files[0] || null
- 1626 };
- 1627 if (guest.guest_name) {
- 1628 episodeData.guests.push(guest);
- 1629 }
- 1630 });
- 1631
- 1632 // Collect chapters
- 1633 episodeData.chapters = [];
- 1634 document.querySelectorAll('#chapters-container .repeater-item').forEach(item => {
- 1635 const chapter = {
- 1636 chapter_timecode: item.querySelector('input[name="chapter_timecode[]"]')?.value || '',
- 1637 chapter_title: item.querySelector('input[name="chapter_title[]"]')?.value || '',
- 1638 chapter_description: item.querySelector('textarea[name="chapter_description[]"]')?.value || ''
- 1639 };
- 1640 if (chapter.chapter_timecode && chapter.chapter_title) {
- 1641 episodeData.chapters.push(chapter);
- 1642 }
- 1643 });
- 1644
- 1645 // Collect files
- 1646 episodeData.episode_artwork = document.getElementById('episode_artwork').files[0] || null;
- 1647 episodeData.audio_file = document.getElementById('audio_file').files[0] || null;
- 1648 episodeData.transcript_file = document.getElementById('transcript_file').files[0] || null;
- 1649 episodeData.subtitles_file = document.getElementById('subtitles_file').files[0] || null;
- 1650 episodeData.glossary_file = document.getElementById('glossary_file').files[0] || null;
- 1651 }
- 1652
- 1653 // Initialize
- 1654 document.addEventListener('DOMContentLoaded', function() {
- 1655 // Initialize tag inputs
- 1656 initTagInputs();
- 1657
- 1658 // Set up meta description counter
- 1659 document.getElementById('meta_description').addEventListener('input', updateMetaDescCounter);
- 1660
- 1661 // Set default publication date to tomorrow
- 1662 const tomorrow = new Date();
- 1663 tomorrow.setDate(tomorrow.getDate() + 1);
- 1664 const formattedDate = tomorrow.toISOString().slice(0, 16);
- 1665 document.getElementById('publication_datetime').value = formattedDate;
- 1666
- 1667 // Add sample guests and chapters
- 1668 addGuest();
- 1669 addTimestamp();
- 1670
- 1671 // Mark manual fields
- 1672 const manualFields = [
- 1673 'content_format', 'series_information', 'topics_keywords',
- 1674 'primary_cta_text', 'primary_cta_url', 'secondary_cta_text', 'secondary_cta_url',
- 1675 'merchandise_ideas', 'internal_status', 'internal_notes', 'assigned_editor'
- 1676 ];
- 1677
- 1678 manualFields.forEach(field => {
- 1679 const sourceElement = document.getElementById(`${field}_source`);
- 1680 if (sourceElement) {
- 1681 sourceElement.textContent = 'Manual';
- 1682 sourceElement.className = 'field-source source-manual';
- 1683 }
- 1684 });
- 1685 });
- 1686 </script>
- 1687</body>
- 1688</html>
- 1689```
- 1690
- 1691## Key Features of This Implementation:
- 1692
- 1693### 1. **RSS Auto-Population**
- 1694- Fetches RSS feed and extracts episode data
- 1695- Maps RSS fields to form fields automatically
- 1696- Shows field source indicators (RSS/Manual/Missing)
- 1697- Highlights auto-populated fields
- 1698
- 1699### 2. **Smart Field Management**
- 1700- **Field Source Tracking**: Each field shows where the data came from
- 1701- **Auto-populated Styling**: Fields from RSS are visually distinct
- 1702- **Missing Field Indicators**: Shows which required fields are empty
- 1703- **Manual Override**: Can edit any auto-populated field
- 1704
- 1705### 3. **Tabbed Interface**
- 1706- **Import RSS Tab**: Fetch and select episodes
- 1707- **Episode Details Tab**: Core information, content, people, show notes
- 1708- **Media & Files Tab**: Upload artwork, audio, transcripts
- 1709- **Publish & Distribute**: Distribution channels, related content
- 1710- **Workflow Tab**: Internal status, timestamps, team notes
- 1711
- 1712### 4. **Interactive Components**
- 1713- **Tag Inputs**: For topics/keywords and merchandise ideas
- 1714- **Repeater Sections**: Add multiple guests, chapters, resources
- 1715- **File Previews**: Image and audio file previews
- 1716- **Dynamic Validation**: Real-time field validation
- 1717
- 1718### 5. **Data Collection**
- 1719- Collects all form data into a structured object
- 1720- Preserves RSS source information
- 1721- Handles file uploads
- 1722- Supports nested data (guests, chapters)
- 1723
- 1724### 6. **User Experience**
- 1725- Clear visual hierarchy
- 1726- Responsive design
- 1727- Progress indicators
- 1728- Status badges
- 1729- Intuitive navigation
- 1730
- 1731## How It Works:
- 1732
- 17331. **Import**: Paste RSS URL → Fetch episodes → Select episode
- 17342. **Auto-Populate**: Form fills with RSS data, source indicators update
- 17353. **Edit**: Manually edit any field (overwrites RSS data)
- 17364. **Add**: Fill in missing fields not in RSS (guests, CTAs, etc.)
- 17375. **Save/Publish**: Validate and submit to server
- 1738
- 1739This form bridges the gap between RSS import (automatic) and manual content creation, making podcast episode management efficient while maintaining flexibility.
Raw Paste