Here's a complete form implementation that auto-populates from RSS feeds and allows manual editing:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Podcast Episode Manager</title>
<style>
:root {
--primary: #1a73e8;
--secondary: #34a853;
--danger: #ea4335;
--warning: #fbbc04;
--light: #f8f9fa;
--dark: #202124;
--gray: #5f6368;
--border: #dadce0;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
line-height: 1.6;
color: var(--dark);
background-color: var(--light);
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, var(--primary), #6c63ff);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 1.1rem;
}
.tabs {
display: flex;
background: #f1f3f4;
border-bottom: 1px solid var(--border);
}
.tab {
padding: 15px 30px;
background: none;
border: none;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tab.active {
background: white;
color: var(--primary);
font-weight: 600;
}
.tab.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 3px;
background: var(--primary);
}
.tab-content {
display: none;
padding: 30px;
}
.tab-content.active {
display: block;
}
.section {
background: white;
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 24px;
overflow: hidden;
}
.section-header {
background: #f8f9fa;
padding: 16px 24px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.section-header h3 {
color: var(--dark);
font-size: 18px;
margin: 0;
}
.section-content {
padding: 24px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--dark);
}
.required::after {
content: " *";
color: var(--danger);
}
input[type="text"],
input[type="number"],
input[type="url"],
input[type="datetime-local"],
textarea,
select {
width: 100%;
padding: 12px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 16px;
transition: border 0.3s ease;
}
input[type="text"]:focus,
input[type="number"]:focus,
input[type="url"]:focus,
textarea:focus,
select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1);
}
textarea {
min-height: 100px;
resize: vertical;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.checkbox-group {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: #0d62d9;
transform: translateY(-2px);
}
.btn-secondary {
background: var(--secondary);
color: white;
}
.btn-secondary:hover {
background: #2c8e44;
}
.btn-outline {
background: white;
color: var(--primary);
border: 2px solid var(--primary);
}
.btn-outline:hover {
background: var(--primary);
color: white;
}
.btn-danger {
background: var(--danger);
color: white;
}
.btn-danger:hover {
background: #d33426;
}
.btn-small {
padding: 6px 12px;
font-size: 14px;
}
.form-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid var(--border);
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
.status-draft {
background: #fef3c7;
color: #92400e;
}
.status-ready {
background: #d1fae5;
color: #065f46;
}
.status-published {
background: #dbeafe;
color: #1e40af;
}
.tag-input {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 8px;
border: 1px solid var(--border);
border-radius: 6px;
min-height: 48px;
}
.tag {
background: #e8f0fe;
color: var(--primary);
padding: 4px 12px;
border-radius: 16px;
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
}
.tag-remove {
background: none;
border: none;
color: var(--primary);
cursor: pointer;
font-size: 16px;
line-height: 1;
}
.tag-input input {
flex: 1;
min-width: 150px;
border: none;
outline: none;
padding: 8px;
font-size: 16px;
}
.repeater-item {
background: #f8f9fa;
border: 1px solid var(--border);
border-radius: 6px;
padding: 16px;
margin-bottom: 12px;
}
.repeater-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.repeater-title {
font-weight: 600;
color: var(--dark);
}
.file-preview {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #f8f9fa;
border: 1px solid var(--border);
border-radius: 6px;
margin-top: 8px;
}
.file-preview img {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
}
.file-info {
flex: 1;
}
.file-name {
font-weight: 500;
margin-bottom: 4px;
}
.file-size {
font-size: 14px;
color: var(--gray);
}
.progress-bar {
height: 4px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
margin-top: 8px;
}
.progress-fill {
height: 100%;
background: var(--primary);
width: 0%;
transition: width 0.3s ease;
}
.rss-import-section {
background: #f0f7ff;
border: 2px dashed var(--primary);
border-radius: 8px;
padding: 30px;
text-align: center;
margin-bottom: 30px;
}
.rss-import-section h3 {
color: var(--primary);
margin-bottom: 16px;
}
.episode-selector {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 6px;
margin-top: 16px;
}
.episode-item {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
cursor: pointer;
transition: background 0.2s ease;
}
.episode-item:hover {
background: #f8f9fa;
}
.episode-item.selected {
background: #e8f0fe;
border-left: 4px solid var(--primary);
}
.episode-title {
font-weight: 500;
margin-bottom: 4px;
}
.episode-meta {
font-size: 14px;
color: var(--gray);
}
.auto-populated {
background: #f0f7ff !important;
border-color: var(--primary) !important;
}
.field-status {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.field-source {
font-size: 12px;
color: var(--gray);
font-style: italic;
}
.source-rss {
color: var(--primary);
}
.source-manual {
color: var(--secondary);
}
.source-missing {
color: var(--warning);
}
@media (max-width: 768px) {
.container {
margin: 0;
border-radius: 0;
}
.header {
padding: 20px;
}
.header h1 {
font-size: 2rem;
}
.tab {
padding: 12px 16px;
font-size: 14px;
}
.tab-content {
padding: 20px;
}
.form-row {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<h1>🎙️ Podcast Episode Manager</h1>
<p>Import from RSS, edit, and manage your podcast episodes</p>
</div>
<!-- Tabs -->
<div class="tabs">
<button class="tab active" onclick="switchTab('import')">📥 Import RSS</button>
<button class="tab" onclick="switchTab('episode')">🎧 Episode Details</button>
<button class="tab" onclick="switchTab('media')">🎵 Media & Files</button>
<button class="tab" onclick="switchTab('publish')">🚀 Publish & Distribute</button>
<button class="tab" onclick="switchTab('workflow')">⚙️ Workflow</button>
</div>
<!-- Import Tab -->
<div id="import-tab" class="tab-content active">
<div class="rss-import-section">
<h3>Import Episode from RSS Feed</h3>
<p>Paste your RSS feed URL to auto-populate episode details</p>
<div class="form-group">
<label for="rss-url">RSS Feed URL</label>
<div style="display: flex; gap: 10px;">
<input type="url" id="rss-url" placeholder="https://anchor.fm/s/98e2f3e0/podcast/rss" style="flex: 1;">
<button class="btn btn-primary" onclick="fetchRSS()">Fetch Episodes</button>
</div>
</div>
<div id="episode-list" class="episode-selector" style="display: none;">
<!-- Episode list will be populated here -->
</div>
<div id="import-status" style="margin-top: 16px; display: none;">
<div class="progress-bar">
<div class="progress-fill" id="import-progress"></div>
</div>
<p id="import-message" style="margin-top: 8px;"></p>
</div>
</div>
<div class="section">
<div class="section-header">
<h3>Currently Imported Episode</h3>
<span id="import-status-badge" class="status-badge status-draft">No Data</span>
</div>
<div class="section-content">
<div id="current-episode-info" style="text-align: center; padding: 40px; color: var(--gray);">
<p>No episode imported yet.</p>
<p>Fetch an RSS feed and select an episode to get started.</p>
</div>
</div>
</div>
</div>
<!-- Episode Details Tab -->
<div id="episode-tab" class="tab-content">
<div class="section">
<div class="section-header">
<h3>Core Episode Information</h3>
<span id="episode-status" class="status-badge status-draft">Draft</span>
</div>
<div class="section-content">
<div class="form-row">
<div class="form-group">
<label for="episode_title" class="required">Episode Title</label>
<div class="field-status">
<span>Required</span>
<span id="episode_title_source" class="field-source source-missing">Missing</span>
</div>
<input type="text" id="episode_title" name="episode_title" placeholder="Enter episode title">
</div>
<div class="form-group">
<label for="episode_subtitle">Subtitle</label>
<div class="field-status">
<span>Optional</span>
<span id="episode_subtitle_source" class="field-source source-missing">Missing</span>
</div>
<input type="text" id="episode_subtitle" name="episode_subtitle" placeholder="Short description (max 150 chars)">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="season_number">Season Number</label>
<div class="field-status">
<span>Optional</span>
<span id="season_number_source" class="field-source source-missing">Missing</span>
</div>
<input type="number" id="season_number" name="season_number" min="1" placeholder="1">
</div>
<div class="form-group">
<label for="episode_number">Episode Number</label>
<div class="field-status">
<span>Optional</span>
<span id="episode_number_source" class="field-source source-missing">Missing</span>
</div>
<input type="number" id="episode_number" name="episode_number" min="1" placeholder="1">
</div>
<div class="form-group">
<label for="series_information">Series Information</label>
<div class="field-status">
<span>Optional</span>
<span id="series_information_source" class="field-source source-missing">Manual</span>
</div>
<input type="text" id="series_information" name="series_information" placeholder="e.g., Part 2 of 3">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="publication_datetime" class="required">Publication Date & Time</label>
<div class="field-status">
<span>Required</span>
<span id="publication_datetime_source" class="field-source source-missing">Missing</span>
</div>
<input type="datetime-local" id="publication_datetime" name="publication_datetime">
</div>
<div class="form-group">
<label for="episode_duration">Duration</label>
<div class="field-status">
<span>Optional</span>
<span id="episode_duration_source" class="field-source source-missing">Missing</span>
</div>
<input type="text" id="episode_duration" name="episode_duration" placeholder="HH:MM:SS">
</div>
<div class="form-group">
<label for="explicit_content">Explicit Content</label>
<div class="field-status">
<span>Required</span>
<span id="explicit_content_source" class="field-source source-missing">Missing</span>
</div>
<select id="explicit_content" name="explicit_content">
<option value="">Select</option>
<option value="no">No (Clean)</option>
<option value="yes">Yes (Explicit)</option>
</select>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-header">
<h3>Content & Format</h3>
</div>
<div class="section-content">
<div class="form-row">
<div class="form-group">
<label for="content_format" class="required">Content Format</label>
<div class="field-status">
<span>Required</span>
<span id="content_format_source" class="field-source source-missing">Manual</span>
</div>
<select id="content_format" name="content_format">
<option value="">Select format</option>
<option value="interview">Interview</option>
<option value="solo">Solo/Monologue</option>
<option value="panel">Panel Discussion</option>
<option value="deep-dive">Deep Dive</option>
<option value="storytelling">Storytelling</option>
<option value="mixtape">Mixtape/Curation</option>
<option value="qa">Q&A</option>
<option value="live">Live Recording</option>
</select>
</div>
<div class="form-group">
<label for="primary_category" class="required">Primary Category</label>
<div class="field-status">
<span>Required</span>
<span id="primary_category_source" class="field-source source-missing">Missing</span>
</div>
<select id="primary_category" name="primary_category">
<option value="">Select category</option>
<option value="arts">Arts</option>
<option value="business">Business</option>
<option value="comedy">Comedy</option>
<option value="education">Education</option>
<option value="news">News</option>
<option value="society">Society & Culture</option>
<option value="sports">Sports</option>
<option value="technology">Technology</option>
</select>
</div>
</div>
<div class="form-group">
<label for="topics_keywords">Topics & Keywords</label>
<div class="field-status">
<span>Optional</span>
<span id="topics_keywords_source" class="field-source source-missing">Manual</span>
</div>
<div class="tag-input" id="topics_keywords_container">
<input type="text" id="topics_keywords_input" placeholder="Type and press Enter to add keywords">
</div>
</div>
<div class="form-group">
<label for="episode_summary" class="required">Episode Summary</label>
<div class="field-status">
<span>Required</span>
<span id="episode_summary_source" class="field-source source-missing">Missing</span>
</div>
<textarea id="episode_summary" name="episode_summary" placeholder="Write a detailed summary of the episode..."></textarea>
</div>
</div>
</div>
<div class="section">
<div class="section-header">
<h3>People</h3>
<button type="button" class="btn btn-outline btn-small" onclick="addGuest()">+ Add Guest</button>
</div>
<div class="section-content">
<div class="form-group">
<label for="hosts" class="required">Host(s)</label>
<div class="field-status">
<span>Required</span>
<span id="hosts_source" class="field-source source-missing">Missing</span>
</div>
<select id="hosts" name="hosts" multiple style="height: 100px;">
<option value="john_doe">John Doe</option>
<option value="jane_smith">Jane Smith</option>
<option value="alex_johnson">Alex Johnson</option>
</select>
<small>Hold Ctrl/Cmd to select multiple hosts</small>
</div>
<div id="guests-container">
<!-- Guest entries will be added here -->
</div>
</div>
</div>
<div class="section">
<div class="section-header">
<h3>Show Notes & Links</h3>
</div>
<div class="section-content">
<div class="form-group">
<label for="show_notes">Detailed Show Notes</label>
<div class="field-status">
<span>Optional</span>
<span id="show_notes_source" class="field-source source-missing">Missing</span>
</div>
<textarea id="show_notes" name="show_notes" placeholder="Include timestamps, links, and key takeaways..."></textarea>
</div>
<div id="resources-container">
<!-- Resource links will be added here -->
</div>
<div class="form-row">
<div class="form-group">
<label for="primary_cta_text">Primary CTA Text</label>
<input type="text" id="primary_cta_text" name="primary_cta_text" placeholder="e.g., Get the Free Guide">
</div>
<div class="form-group">
<label for="primary_cta_url">Primary CTA URL</label>
<input type="url" id="primary_cta_url" name="primary_cta_url" placeholder="https://example.com/guide">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="secondary_cta_text">Secondary CTA Text</label>
<input type="text" id="secondary_cta_text" name="secondary_cta_text" placeholder="e.g., Subscribe to Newsletter">
</div>
<div class="form-group">
<label for="secondary_cta_url">Secondary CTA URL</label>
<input type="url" id="secondary_cta_url" name="secondary_cta_url" placeholder="https://example.com/newsletter">
</div>
</div>
</div>
</div>
</div>
<!-- Media & Files Tab -->
<div id="media-tab" class="tab-content">
<div class="section">
<div class="section-header">
<h3>Episode Media</h3>
</div>
<div class="section-content">
<div class="form-group">
<label for="episode_artwork" class="required">Episode Artwork</label>
<div class="field-status">
<span>Required</span>
<span id="episode_artwork_source" class="field-source source-missing">Missing</span>
</div>
<input type="file" id="episode_artwork" name="episode_artwork" accept="image/*" onchange="previewImage(this)">
<div id="artwork-preview" class="file-preview" style="display: none;">
<img id="artwork-thumbnail" src="" alt="Artwork Preview">
<div class="file-info">
<div class="file-name" id="artwork-name"></div>
<div class="file-size" id="artwork-size"></div>
</div>
<button type="button" class="btn btn-danger btn-small" onclick="clearArtwork()">Remove</button>
</div>
<small>Square image, 3000x3000px recommended, JPG or PNG</small>
</div>
<div class="form-group">
<label for="audio_file" class="required">Audio File (MP3)</label>
<div class="field-status">
<span>Required</span>
<span id="audio_file_source" class="field-source source-missing">Missing</span>
</div>
<input type="file" id="audio_file" name="audio_file" accept="audio/mpeg" onchange="previewAudio(this)">
<div id="audio-preview" class="file-preview" style="display: none;">
<div class="file-info">
<div class="file-name" id="audio-name"></div>
<div class="file-size" id="audio-size"></div>
</div>
</div>
<small>MP3 format, max 200MB</small>
</div>
<div class="form-group">
<label for="transcript_file">Transcript (TXT)</label>
<input type="file" id="transcript_file" name="transcript_file" accept=".txt">
</div>
<div class="form-group">
<label for="subtitles_file">Subtitles (SRT/VTT)</label>
<input type="file" id="subtitles_file" name="subtitles_file" accept=".srt,.vtt">
</div>
<div class="form-group">
<label for="glossary_file">Glossary / Unique Words</label>
<input type="file" id="glossary_file" name="glossary_file" accept=".txt,.json">
</div>
</div>
</div>
</div>
<!-- Publish & Distribute Tab -->
<div id="publish-tab" class="tab-content">
<div class="section">
<div class="section-header">
<h3>Publishing Settings</h3>
</div>
<div class="section-content">
<div class="form-group">
<label for="episode_slug">Episode Slug / URL</label>
<div class="field-status">
<span>Optional</span>
<span id="episode_slug_source" class="field-source source-missing">Auto-generated</span>
</div>
<input type="text" id="episode_slug" name="episode_slug" placeholder="auto-generated-from-title">
<small>Used for the episode URL. Will be auto-generated from title if left empty.</small>
</div>
<div class="form-group">
<label for="meta_description">Meta Description</label>
<textarea id="meta_description" name="meta_description" maxlength="155" placeholder="SEO description for search engines"></textarea>
<small><span id="meta-desc-count">0</span>/155 characters</small>
</div>
<div class="form-group">
<label>Distribution Channels</label>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="publish_apple" name="publish_apple" checked>
<label for="publish_apple">Apple Podcasts</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="publish_spotify" name="publish_spotify" checked>
<label for="publish_spotify">Spotify</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="publish_youtube" name="publish_youtube">
<label for="publish_youtube">YouTube</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="include_newsletter" name="include_newsletter" checked>
<label for="include_newsletter">Newsletter</label>
</div>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-header">
<h3>Related Content</h3>
</div>
<div class="section-content">
<div class="form-group">
<label for="related_episodes">Related Episodes</label>
<select id="related_episodes" name="related_episodes" multiple style="height: 100px;">
<option value="ep1">Previous Episode: Getting Started</option>
<option value="ep2">Next Episode: Advanced Topics</option>
<option value="ep3">Interview with Expert</option>
</select>
<small>Hold Ctrl/Cmd to select multiple episodes</small>
</div>
<div class="form-group">
<label for="merchandise_ideas">Merchandise Ideas</label>
<div class="tag-input" id="merchandise_container">
<input type="text" id="merchandise_input" placeholder="Type and press Enter to add merch ideas">
</div>
</div>
</div>
</div>
</div>
<!-- Workflow Tab -->
<div id="workflow-tab" class="tab-content">
<div class="section">
<div class="section-header">
<h3>Workflow & Status</h3>
</div>
<div class="section-content">
<div class="form-row">
<div class="form-group">
<label for="internal_status">Internal Status</label>
<select id="internal_status" name="internal_status">
<option value="draft">Draft</option>
<option value="in_review">In Review</option>
<option value="ready">Ready to Publish</option>
<option value="scheduled">Scheduled</option>
<option value="published">Published</option>
</select>
</div>
<div class="form-group">
<label for="assigned_editor">Assigned Editor</label>
<select id="assigned_editor" name="assigned_editor">
<option value="">Unassigned</option>
<option value="editor1">Jane Smith</option>
<option value="editor2">John Doe</option>
<option value="editor3">Alex Johnson</option>
</select>
</div>
</div>
<div class="form-group">
<label for="internal_notes">Internal Notes</label>
<textarea id="internal_notes" name="internal_notes" placeholder="Notes for the team..."></textarea>
</div>
</div>
</div>
<div class="section">
<div class="section-header">
<h3>Timestamps (Chapters)</h3>
<button type="button" class="btn btn-outline btn-small" onclick="addTimestamp()">+ Add Timestamp</button>
</div>
<div class="section-content">
<div id="chapters-container">
<!-- Chapter entries will be added here -->
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="form-actions">
<button type="button" class="btn btn-outline" onclick="resetForm()">Reset Form</button>
<button type="button" class="btn btn-secondary" onclick="saveDraft()">Save Draft</button>
<button type="button" class="btn btn-primary" onclick="submitForm()">Publish Episode</button>
</div>
</div>
<script>
// Global variables
let currentEpisode = null;
let episodeData = {
// Initialize with default values
episode_title: '',
episode_subtitle: '',
season_number: '',
episode_number: '',
series_information: '',
publication_datetime: '',
episode_duration: '',
explicit_content: '',
content_format: '',
primary_category: '',
topics_keywords: [],
episode_summary: '',
hosts: [],
guests: [],
show_notes: '',
resources_links: [],
primary_cta_text: '',
primary_cta_url: '',
secondary_cta_text: '',
secondary_cta_url: '',
chapters: [],
episode_artwork: null,
audio_file: null,
transcript_file: null,
subtitles_file: null,
glossary_file: null,
episode_slug: '',
meta_description: '',
publish_apple: true,
publish_spotify: true,
publish_youtube: false,
include_newsletter: true,
related_episodes: [],
merchandise_ideas: [],
internal_status: 'draft',
internal_notes: '',
assigned_editor: '',
// Track field sources
field_sources: {}
};
// Tab switching
function switchTab(tabName) {
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
// Remove active class from all tabs
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
// Show selected tab
document.getElementById(`${tabName}-tab`).classList.add('active');
// Activate corresponding tab button
document.querySelectorAll('.tab').forEach(tab => {
if (tab.textContent.includes(tabName.charAt(0).toUpperCase() + tabName.slice(1))) {
tab.classList.add('active');
}
});
}
// Fetch RSS feed
async function fetchRSS() {
const rssUrl = document.getElementById('rss-url').value;
if (!rssUrl) {
alert('Please enter an RSS feed URL');
return;
}
const importStatus = document.getElementById('import-status');
const importProgress = document.getElementById('import-progress');
const importMessage = document.getElementById('import-message');
importStatus.style.display = 'block';
importProgress.style.width = '30%';
importMessage.textContent = 'Fetching RSS feed...';
try {
// Note: In a real implementation, you would use a CORS proxy
// or server-side endpoint to fetch RSS feeds
const response = await fetch(`https://api.allorigins.win/get?url=${encodeURIComponent(rssUrl)}`);
const data = await response.json();
importProgress.style.width = '60%';
importMessage.textContent = 'Parsing RSS feed...';
// Parse the RSS feed
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(data.contents, 'text/xml');
// Extract channel info
const channel = xmlDoc.querySelector('channel');
const episodes = xmlDoc.querySelectorAll('item');
// Display episodes
const episodeList = document.getElementById('episode-list');
episodeList.innerHTML = '';
episodeList.style.display = 'block';
episodes.forEach((episode, index) => {
const title = episode.querySelector('title')?.textContent || `Episode ${index + 1}`;
const pubDate = episode.querySelector('pubDate')?.textContent || '';
const guid = episode.querySelector('guid')?.textContent || '';
const episodeDiv = document.createElement('div');
episodeDiv.className = 'episode-item';
episodeDiv.dataset.index = index;
episodeDiv.dataset.guid = guid;
episodeDiv.innerHTML = `
<div class="episode-title">${title}</div>
<div class="episode-meta">${pubDate}</div>
`;
episodeDiv.onclick = () => selectEpisode(episode, channel);
episodeList.appendChild(episodeDiv);
});
importProgress.style.width = '100%';
importMessage.textContent = `Found ${episodes.length} episodes`;
setTimeout(() => {
importStatus.style.display = 'none';
}, 2000);
} catch (error) {
console.error('Error fetching RSS:', error);
importProgress.style.width = '0%';
importMessage.textContent = 'Error fetching RSS feed. Please check the URL and try again.';
importMessage.style.color = 'var(--danger)';
}
}
// Select episode from RSS
function selectEpisode(episodeElement, channelElement) {
// Update UI
document.querySelectorAll('.episode-item').forEach(item => {
item.classList.remove('selected');
});
event.currentTarget.classList.add('selected');
// Parse RSS data
const rssData = {
// Core information
episode_title: getRssValue(episodeElement, 'title'),
episode_subtitle: getRssValue(episodeElement, 'itunes|subtitle'),
episode_summary: getRssValue(episodeElement, 'description') ||
getRssValue(episodeElement, 'content|encoded') ||
getRssValue(episodeElement, 'itunes|summary'),
publication_datetime: getRssValue(episodeElement, 'pubDate'),
episode_duration: getRssValue(episodeElement, 'itunes|duration'),
explicit_content: getRssValue(episodeElement, 'itunes|explicit'),
episode_number: getRssValue(episodeElement, 'itunes|episode'),
season_number: getRssValue(episodeElement, 'itunes|season'),
content_format: getRssValue(episodeElement, 'itunes|episodeType'),
// Media
episode_artwork: getRssAttribute(episodeElement, 'itunes|image', 'href') ||
getRssAttribute(channelElement, 'itunes|image', 'href'),
audio_file: getRssAttribute(episodeElement, 'enclosure', 'url'),
transcript_file: getRssAttribute(episodeElement, 'podcast|transcript', 'url'),
// Channel info
primary_category: getRssAttribute(channelElement, 'itunes|category', 'text'),
hosts: [getRssValue(channelElement, 'itunes|author') ||
getRssValue(channelElement, 'dc|creator')].filter(Boolean),
// URLs
episode_slug: getRssValue(episodeElement, 'link') ||
getRssValue(episodeElement, 'guid'),
meta_description: getRssValue(episodeElement, 'itunes|summary')
};
// Update form with RSS data
updateFormWithRSSData(rssData);
// Update current episode display
document.getElementById('current-episode-info').innerHTML = `
<h3>${rssData.episode_title || 'Untitled Episode'}</h3>
<p>${rssData.episode_subtitle || ''}</p>
<p><small>Duration: ${rssData.episode_duration || 'N/A'} |
Explicit: ${rssData.explicit_content || 'N/A'}</small></p>
<button class="btn btn-outline" onclick="switchTab('episode')">
Edit Episode Details →
</button>
`;
document.getElementById('import-status-badge').textContent = 'Imported';
document.getElementById('import-status-badge').className = 'status-badge status-ready';
// Switch to episode tab
switchTab('episode');
}
// Helper function to get RSS value
function getRssValue(element, name) {
if (!element) return '';
// Handle namespaced elements
const parts = name.split('|');
if (parts.length === 2) {
const [namespace, localName] = parts;
return element.getElementsByTagNameNS(`http://www.itunes.com/dtds/podcast-1.0.dtd`, localName)[0]?.textContent || '';
}
return element.querySelector(name)?.textContent || '';
}
// Helper function to get RSS attribute
function getRssAttribute(element, name, attr) {
if (!element) return '';
const parts = name.split('|');
if (parts.length === 2) {
const [namespace, localName] = parts;
const elem = element.getElementsByTagNameNS(`http://www.itunes.com/dtds/podcast-1.0.dtd`, localName)[0];
return elem?.getAttribute(attr) || '';
}
return element.querySelector(name)?.getAttribute(attr) || '';
}
// Update form with RSS data
function updateFormWithRSSData(rssData) {
// Store the data
currentEpisode = rssData;
// Update each field
for (const [key, value] of Object.entries(rssData)) {
if (value) {
const element = document.getElementById(key);
if (element) {
element.value = value;
// Mark as auto-populated
element.classList.add('auto-populated');
// Update field source indicator
const sourceElement = document.getElementById(`${key}_source`);
if (sourceElement) {
sourceElement.textContent = 'RSS';
sourceElement.className = 'field-source source-rss';
}
// Store in episodeData
episodeData[key] = value;
episodeData.field_sources[key] = 'rss';
}
}
}
// Special handling for arrays
if (rssData.hosts && rssData.hosts.length > 0) {
const hostsSelect = document.getElementById('hosts');
Array.from(hostsSelect.options).forEach(option => {
if (rssData.hosts.some(host => option.text.toLowerCase().includes(host.toLowerCase()))) {
option.selected = true;
}
});
}
// Update meta description counter
updateMetaDescCounter();
// Update status badge
document.getElementById('episode-status').textContent = 'Imported';
document.getElementById('episode-status').className = 'status-badge status-ready';
}
// Add guest
function addGuest() {
const container = document.getElementById('guests-container');
const guestCount = container.children.length + 1;
const guestDiv = document.createElement('div');
guestDiv.className = 'repeater-item';
guestDiv.innerHTML = `
<div class="repeater-header">
<span class="repeater-title">Guest #${guestCount}</span>
<button type="button" class="btn btn-danger btn-small" onclick="removeGuest(this)">Remove</button>
</div>
<div class="form-row">
<div class="form-group">
<label>Guest Name</label>
<input type="text" name="guest_name[]" placeholder="Full name">
</div>
<div class="form-group">
<label>Title / Company</label>
<input type="text" name="guest_title[]" placeholder="Position and company">
</div>
</div>
<div class="form-group">
<label>Bio</label>
<textarea name="guest_bio[]" placeholder="Short bio"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>Website / Social Link</label>
<input type="url" name="guest_website[]" placeholder="https://">
</div>
<div class="form-group">
<label>Headshot</label>
<input type="file" name="guest_headshot[]" accept="image/*">
</div>
</div>
`;
container.appendChild(guestDiv);
}
// Remove guest
function removeGuest(button) {
button.closest('.repeater-item').remove();
updateGuestNumbers();
}
// Update guest numbers
function updateGuestNumbers() {
const guests = document.querySelectorAll('#guests-container .repeater-item');
guests.forEach((guest, index) => {
guest.querySelector('.repeater-title').textContent = `Guest #${index + 1}`;
});
}
// Add timestamp
function addTimestamp() {
const container = document.getElementById('chapters-container');
const chapterCount = container.children.length + 1;
const chapterDiv = document.createElement('div');
chapterDiv.className = 'repeater-item';
chapterDiv.innerHTML = `
<div class="repeater-header">
<span class="repeater-title">Chapter #${chapterCount}</span>
<button type="button" class="btn btn-danger btn-small" onclick="removeChapter(this)">Remove</button>
</div>
<div class="form-row">
<div class="form-group">
<label>Timecode</label>
<input type="text" name="chapter_timecode[]" placeholder="HH:MM:SS">
</div>
<div class="form-group">
<label>Chapter Title</label>
<input type="text" name="chapter_title[]" placeholder="Introduction">
</div>
</div>
<div class="form-group">
<label>Description</label>
<textarea name="chapter_description[]" placeholder="Optional description"></textarea>
</div>
`;
container.appendChild(chapterDiv);
}
// Remove chapter
function removeChapter(button) {
button.closest('.repeater-item').remove();
updateChapterNumbers();
}
// Update chapter numbers
function updateChapterNumbers() {
const chapters = document.querySelectorAll('#chapters-container .repeater-item');
chapters.forEach((chapter, index) => {
chapter.querySelector('.repeater-title').textContent = `Chapter #${index + 1}`;
});
}
// Preview image
function previewImage(input) {
if (input.files && input.files[0]) {
const file = input.files[0];
const reader = new FileReader();
reader.onload = function(e) {
const preview = document.getElementById('artwork-preview');
const thumbnail = document.getElementById('artwork-thumbnail');
const name = document.getElementById('artwork-name');
const size = document.getElementById('artwork-size');
thumbnail.src = e.target.result;
name.textContent = file.name;
size.textContent = formatFileSize(file.size);
preview.style.display = 'flex';
// Update field source
const sourceElement = document.getElementById('episode_artwork_source');
sourceElement.textContent = 'Manual Upload';
sourceElement.className = 'field-source source-manual';
input.classList.remove('auto-populated');
};
reader.readAsDataURL(file);
}
}
// Preview audio
function previewAudio(input) {
if (input.files && input.files[0]) {
const file = input.files[0];
const preview = document.getElementById('audio-preview');
const name = document.getElementById('audio-name');
const size = document.getElementById('audio-size');
name.textContent = file.name;
size.textContent = formatFileSize(file.size);
preview.style.display = 'flex';
// Update field source
const sourceElement = document.getElementById('audio_file_source');
sourceElement.textContent = 'Manual Upload';
sourceElement.className = 'field-source source-manual';
input.classList.remove('auto-populated');
}
}
// Clear artwork
function clearArtwork() {
document.getElementById('episode_artwork').value = '';
document.getElementById('artwork-preview').style.display = 'none';
// Update field source
const sourceElement = document.getElementById('episode_artwork_source');
sourceElement.textContent = 'Missing';
sourceElement.className = 'field-source source-missing';
}
// Format file size
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Update meta description counter
function updateMetaDescCounter() {
const textarea = document.getElementById('meta_description');
const counter = document.getElementById('meta-desc-count');
counter.textContent = textarea.value.length;
}
// Initialize tag inputs
function initTagInputs() {
// Topics/keywords
const topicsInput = document.getElementById('topics_keywords_input');
const topicsContainer = document.getElementById('topics_keywords_container');
topicsInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && this.value.trim()) {
e.preventDefault();
addTag(this.value.trim(), topicsContainer);
this.value = '';
}
});
// Merchandise ideas
const merchInput = document.getElementById('merchandise_input');
const merchContainer = document.getElementById('merchandise_container');
merchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && this.value.trim()) {
e.preventDefault();
addTag(this.value.trim(), merchContainer);
this.value = '';
}
});
}
// Add tag
function addTag(text, container) {
const tag = document.createElement('div');
tag.className = 'tag';
tag.innerHTML = `
${text}
<button type="button" class="tag-remove" onclick="removeTag(this)">×</button>
`;
// Insert before the input
const input = container.querySelector('input');
container.insertBefore(tag, input);
}
// Remove tag
function removeTag(button) {
button.closest('.tag').remove();
}
// Reset form
function resetForm() {
if (confirm('Are you sure you want to reset the form? All changes will be lost.')) {
// Clear all inputs
document.querySelectorAll('input, textarea, select').forEach(element => {
if (element.type !== 'button') {
element.value = '';
element.classList.remove('auto-populated');
}
});
// Clear checkboxes
document.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.checked = false;
});
// Reset multi-selects
document.querySelectorAll('select[multiple]').forEach(select => {
Array.from(select.options).forEach(option => {
option.selected = false;
});
});
// Clear file previews
document.querySelectorAll('.file-preview').forEach(preview => {
preview.style.display = 'none';
});
// Clear containers
document.getElementById('guests-container').innerHTML = '';
document.getElementById('chapters-container').innerHTML = '';
// Reset field sources
document.querySelectorAll('.field-source').forEach(source => {
source.textContent = 'Missing';
source.className = 'field-source source-missing';
});
// Reset current episode
currentEpisode = null;
episodeData = {
field_sources: {}
};
// Update UI
document.getElementById('current-episode-info').innerHTML = `
<p>No episode imported yet.</p>
<p>Fetch an RSS feed and select an episode to get started.</p>
`;
document.getElementById('import-status-badge').textContent = 'No Data';
document.getElementById('import-status-badge').className = 'status-badge status-draft';
document.getElementById('episode-status').textContent = 'Draft';
document.getElementById('episode-status').className = 'status-badge status-draft';
}
}
// Save draft
function saveDraft() {
// Collect form data
collectFormData();
// In a real app, this would save to a database
console.log('Saving draft:', episodeData);
// Update UI
document.getElementById('episode-status').textContent = 'Draft Saved';
document.getElementById('episode-status').className = 'status-badge status-ready';
// Show success message
alert('Draft saved successfully!');
}
// Submit form
function submitForm() {
// Collect form data
collectFormData();
// Validate required fields
const requiredFields = [
'episode_title',
'episode_summary',
'episode_artwork',
'audio_file'
];
const missingFields = [];
for (const field of requiredFields) {
if (!episodeData[field]) {
missingFields.push(field.replace('_', ' '));
}
}
if (missingFields.length > 0) {
alert(`Please fill in the following required fields:\n\n• ${missingFields.join('\n• ')}`);
return;
}
// Submit to server (simulated)
console.log('Submitting episode:', episodeData);
// Show success message
alert('Episode submitted successfully!');
// Update status
document.getElementById('internal_status').value = 'published';
document.getElementById('episode-status').textContent = 'Published';
document.getElementById('episode-status').className = 'status-badge status-published';
}
// Collect form data
function collectFormData() {
// Collect basic fields
const fields = [
'episode_title', 'episode_subtitle', 'season_number', 'episode_number',
'series_information', 'publication_datetime', 'episode_duration',
'explicit_content', 'content_format', 'primary_category', 'episode_summary',
'show_notes', 'primary_cta_text', 'primary_cta_url', 'secondary_cta_text',
'secondary_cta_url', 'episode_slug', 'meta_description', 'internal_status',
'internal_notes', 'assigned_editor'
];
fields.forEach(field => {
const element = document.getElementById(field);
if (element) {
episodeData[field] = element.value;
}
});
// Collect checkboxes
episodeData.publish_apple = document.getElementById('publish_apple').checked;
episodeData.publish_spotify = document.getElementById('publish_spotify').checked;
episodeData.publish_youtube = document.getElementById('publish_youtube').checked;
episodeData.include_newsletter = document.getElementById('include_newsletter').checked;
// Collect multi-selects
episodeData.hosts = Array.from(document.getElementById('hosts').selectedOptions).map(opt => opt.value);
episodeData.related_episodes = Array.from(document.getElementById('related_episodes').selectedOptions).map(opt => opt.value);
// Collect tags
episodeData.topics_keywords = Array.from(document.querySelectorAll('#topics_keywords_container .tag'))
.map(tag => tag.textContent.replace('×', '').trim());
episodeData.merchandise_ideas = Array.from(document.querySelectorAll('#merchandise_container .tag'))
.map(tag => tag.textContent.replace('×', '').trim());
// Collect guests
episodeData.guests = [];
document.querySelectorAll('#guests-container .repeater-item').forEach(item => {
const guest = {
guest_name: item.querySelector('input[name="guest_name[]"]')?.value || '',
guest_title: item.querySelector('input[name="guest_title[]"]')?.value || '',
guest_bio: item.querySelector('textarea[name="guest_bio[]"]')?.value || '',
guest_website: item.querySelector('input[name="guest_website[]"]')?.value || '',
guest_headshot: item.querySelector('input[name="guest_headshot[]"]')?.files[0] || null
};
if (guest.guest_name) {
episodeData.guests.push(guest);
}
});
// Collect chapters
episodeData.chapters = [];
document.querySelectorAll('#chapters-container .repeater-item').forEach(item => {
const chapter = {
chapter_timecode: item.querySelector('input[name="chapter_timecode[]"]')?.value || '',
chapter_title: item.querySelector('input[name="chapter_title[]"]')?.value || '',
chapter_description: item.querySelector('textarea[name="chapter_description[]"]')?.value || ''
};
if (chapter.chapter_timecode && chapter.chapter_title) {
episodeData.chapters.push(chapter);
}
});
// Collect files
episodeData.episode_artwork = document.getElementById('episode_artwork').files[0] || null;
episodeData.audio_file = document.getElementById('audio_file').files[0] || null;
episodeData.transcript_file = document.getElementById('transcript_file').files[0] || null;
episodeData.subtitles_file = document.getElementById('subtitles_file').files[0] || null;
episodeData.glossary_file = document.getElementById('glossary_file').files[0] || null;
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
// Initialize tag inputs
initTagInputs();
// Set up meta description counter
document.getElementById('meta_description').addEventListener('input', updateMetaDescCounter);
// Set default publication date to tomorrow
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const formattedDate = tomorrow.toISOString().slice(0, 16);
document.getElementById('publication_datetime').value = formattedDate;
// Add sample guests and chapters
addGuest();
addTimestamp();
// Mark manual fields
const manualFields = [
'content_format', 'series_information', 'topics_keywords',
'primary_cta_text', 'primary_cta_url', 'secondary_cta_text', 'secondary_cta_url',
'merchandise_ideas', 'internal_status', 'internal_notes', 'assigned_editor'
];
manualFields.forEach(field => {
const sourceElement = document.getElementById(`${field}_source`);
if (sourceElement) {
sourceElement.textContent = 'Manual';
sourceElement.className = 'field-source source-manual';
}
});
});
</script>
</body>
</html>This form bridges the gap between RSS import (automatic) and manual content creation, making podcast episode management efficient while maintaining flexibility.
Raw Paste