Podcast-Episodes-form-with-rss

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

Raw Paste

Comments 0
Login to post a comment.
  • No comments yet. Be the first.
Login to post a comment. Login or Register
We use cookies. To comply with GDPR in the EU and the UK we have to show you these.

We use cookies and similar technologies to keep this website functional (including spam protection via Google reCAPTCHA or Cloudflare Turnstile), and — with your consent — to measure usage and show ads. See Privacy.