nostr-poster/web/assets/js/content.js

609 lines
23 KiB
JavaScript

// content.js - JavaScript for the content management page
document.addEventListener('DOMContentLoaded', () => {
// ===================================================
// Global Variables
// ===================================================
let currentBotId = null;
// Media server configuration (global for all bots)
let globalMediaConfig = {
primaryService: 'nip94',
primaryURL: '',
fallbackService: 'none',
fallbackURL: ''
};
// ===================================================
// Element References
// ===================================================
const botSelect = document.getElementById('botSelect');
const loadContentBtn = document.getElementById('loadContentBtn');
const uploadFileInput = document.getElementById('uploadFileInput');
const uploadPreviewContainer = document.getElementById('uploadPreviewContainer');
const uploadPreview = document.getElementById('uploadPreview');
const uploadButton = document.getElementById('uploadButton');
const contentContainer = document.getElementById('contentContainer');
// Post creation elements
const postKindRadios = document.querySelectorAll('input[name="postKind"]');
const titleField = document.getElementById('titleField');
const postTitle = document.getElementById('postTitle');
const manualPostContent = document.getElementById('manualPostContent');
const postHashtags = document.getElementById('postHashtags');
const postMediaInput = document.getElementById('postMediaInput');
const quickUploadBtn = document.getElementById('quickUploadBtn');
const mediaPreviewContainer = document.getElementById('mediaPreviewContainer');
const mediaPreview = document.getElementById('mediaPreview');
const mediaLinkContainer = document.getElementById('mediaLinkContainer');
const submitPostBtn = document.getElementById('submitPostBtn');
// Media server settings
const primaryServer = document.getElementById('primaryServer');
const fallbackServer = document.getElementById('fallbackServer');
const saveMediaSettingsBtn = document.getElementById('saveMediaSettingsBtn');
const primaryServerURL = document.getElementById('primaryServerURL');
const fallbackServerURL = document.getElementById('fallbackServerURL');
// ===================================================
// Event Listeners
// ===================================================
// Check auth and load bots first
checkAuth();
// Load bot choices after a short delay to ensure auth is checked
setTimeout(() => {
const token = localStorage.getItem('authToken');
if (token) {
loadBotChoices();
}
}, 500);
// Button click handlers
if (loadContentBtn) loadContentBtn.addEventListener('click', handleLoadContent);
if (uploadButton) uploadButton.addEventListener('click', handleFileUpload);
if (quickUploadBtn) quickUploadBtn.addEventListener('click', handleQuickUpload);
if (submitPostBtn) submitPostBtn.addEventListener('click', handleSubmitPost);
if (saveMediaSettingsBtn) {
saveMediaSettingsBtn.addEventListener('click', handleSaveMediaSettings);
}
// File input change handlers
if (uploadFileInput) {
uploadFileInput.addEventListener('change', (e) => {
previewFile(e.target.files[0], uploadPreview, uploadPreviewContainer);
uploadButton.disabled = !e.target.files.length;
});
}
if (postMediaInput) {
postMediaInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) {
mediaPreviewContainer.classList.add('d-none');
return;
}
previewFile(file, mediaPreview, mediaPreviewContainer);
// Show filename if not an image
if (!file.type.startsWith('image/')) {
mediaPreview.style.display = 'none';
mediaLinkContainer.innerHTML =
`<p class="mb-0">Selected: ${file.name}</p>
<small class="text-muted">Click "Upload" to get media URL</small>`;
} else {
mediaPreview.style.display = 'block';
mediaLinkContainer.innerHTML =
'<small class="text-muted">Click "Upload" to get media URL</small>';
}
});
}
// Post kind radio button change handler
if (postKindRadios.length) {
postKindRadios.forEach(radio => {
radio.addEventListener('change', () => {
titleField.classList.toggle('d-none', radio.value !== '20');
// Show/hide required media elements for kind 20
const isKind20 = radio.value === '20';
const kind20MediaRequired = document.getElementById('kind20MediaRequired');
if (kind20MediaRequired) {
kind20MediaRequired.style.display = isKind20 ? 'inline-block' : 'none';
}
// For kind 20, validate that media is provided before posting
if (submitPostBtn) {
submitPostBtn.title = isKind20 ?
'Picture posts require a title and an image URL' :
'Create a standard text post';
}
});
});
}
// ===================================================
// Event Handler Functions
// ===================================================
// Load content when button is clicked
function handleLoadContent() {
if (!botSelect || !botSelect.value) {
alert('Please select a bot first!');
return;
}
currentBotId = botSelect.value;
contentContainer.classList.remove('d-none');
loadBotContent(currentBotId);
}
// Handle file upload button click
function handleFileUpload() {
if (!currentBotId) {
alert('Please select a bot first');
return;
}
if (!uploadFileInput.files.length) {
alert('Please select a file to upload');
return;
}
uploadBotFile(currentBotId);
}
// Handle quick upload for post creation
function handleQuickUpload() {
if (!currentBotId) {
alert('Please select a bot first');
return;
}
if (!postMediaInput.files.length) {
alert('Please select a file to upload');
return;
}
quickUploadMedia(currentBotId, postMediaInput.files[0]);
}
// Handle post submission
function handleSubmitPost() {
if (!currentBotId) {
alert('Please select a bot first');
return;
}
createManualPost(currentBotId)
.then(data => {
alert('Post created successfully!');
console.log('Post success response:', data);
// Display event information if present
if (data.event) {
displayEventInfo(data.event);
}
// Reset form
manualPostContent.value = '';
postHashtags.value = '';
postTitle.value = '';
postMediaInput.value = '';
if (mediaUrlInput) mediaUrlInput.value = '';
if (mediaAltText) mediaAltText.value = '';
mediaPreviewContainer.classList.add('d-none');
// Reset button
submitPostBtn.disabled = false;
submitPostBtn.textContent = 'Post Now';
})
.catch(error => {
console.error('Error creating post:', error);
alert('Error creating post: ' + error.message);
submitPostBtn.disabled = false;
submitPostBtn.textContent = 'Post Now';
});
}
// Handle saving media server settings
function handleSaveMediaSettings() {
globalMediaConfig.primaryService = primaryServer.value;
globalMediaConfig.primaryURL = primaryServerURL ? primaryServerURL.value.trim() : '';
globalMediaConfig.fallbackService = fallbackServer.value === 'none' ? '' : fallbackServer.value;
globalMediaConfig.fallbackURL = fallbackServerURL ? fallbackServerURL.value.trim() : '';
// Save to localStorage
localStorage.setItem('mediaConfig', JSON.stringify(globalMediaConfig));
alert('Media server settings saved!');
}
// ===================================================
// Utility Functions
// ===================================================
// Preview a file (image or video)
function previewFile(file, previewElement, containerElement) {
if (!file) {
containerElement.classList.add('d-none');
return;
}
containerElement.classList.remove('d-none');
// Show preview for images
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function (e) {
previewElement.src = e.target.result;
previewElement.style.display = 'block';
};
reader.readAsDataURL(file);
} else {
// For non-images (video, etc)
previewElement.style.display = 'none';
}
}
// Validates post data before submission
function validatePostData() {
const kind = parseInt(document.querySelector('input[name="postKind"]:checked').value);
const content = manualPostContent.value.trim();
// Basic validation for all post types
if (!content) {
alert('Post content is required');
return false;
}
// Additional validation for kind 20 posts
if (kind === 20) {
const title = postTitle.value.trim();
if (!title) {
alert('Title is required for Picture Posts (kind: 20)');
return false;
}
// Check if we have a media URL either in the content or in the mediaUrlInput
const mediaUrl = mediaUrlInput.value.trim();
const urlRegex = /(https?:\/\/[^\s]+)/g;
const contentContainsUrl = urlRegex.test(content);
if (!mediaUrl && !contentContainsUrl) {
alert('Picture posts require an image URL. Please upload an image or enter a URL in the Media URL field or in the content.');
return false;
}
}
return true;
}
// Add this function to content.js
function displayEventInfo(event) {
// Get the dedicated container
const container = document.getElementById('eventInfoContainer');
if (!container) return;
// Create HTML for the event info
const html = `
<div class="event-info card mb-3">
<div class="card-header">
<h5>Post Published Successfully</h5>
</div>
<div class="card-body">
<div class="mb-2">
<label class="form-label">Note ID (NIP-19):</label>
<div class="input-group">
<input type="text" class="form-control" value="${event.note || ''}" readonly>
<button class="btn btn-outline-secondary copy-btn" data-value="${event.note || ''}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
</svg>
</button>
</div>
</div>
<div class="mb-2">
<label class="form-label">Event with Relays (NIP-19):</label>
<div class="input-group">
<input type="text" class="form-control" value="${event.nevent || ''}" readonly>
<button class="btn btn-outline-secondary copy-btn" data-value="${event.nevent || ''}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
</svg>
</button>
</div>
</div>
</div>
</div>
`;
// Update the container and make it visible
container.innerHTML = html;
container.classList.remove('d-none');
}
// ===================================================
// API Functions
// ===================================================
// Load bot content files
function loadBotContent(botId) {
const token = localStorage.getItem('authToken');
// Load any saved media config from localStorage
const savedConfig = localStorage.getItem('mediaConfig');
if (savedConfig) {
try {
globalMediaConfig = JSON.parse(savedConfig);
if (primaryServer) primaryServer.value = globalMediaConfig.primaryService || 'nip94';
if (primaryServerURL) primaryServerURL.value = globalMediaConfig.primaryURL || '';
if (fallbackServer) {
fallbackServer.value = globalMediaConfig.fallbackService || 'none';
}
if (fallbackServerURL) {
fallbackServerURL.value = globalMediaConfig.fallbackURL || '';
}
} catch (e) {
console.error('Error parsing saved media config:', e);
}
}
// Fetch content files
fetch(`/api/content/${botId}`, {
headers: { 'Authorization': token }
})
.then(res => {
if (!res.ok) throw new Error('Failed to list content');
return res.json();
})
.then(files => {
renderContentFiles(botId, files);
})
.catch(err => {
console.error('Error loading content:', err);
alert('Error loading content: ' + err.message);
});
}
// Updated renderContentFiles function
function renderContentFiles(botId, files) {
const contentArea = document.getElementById('contentArea');
if (!contentArea) return;
// Generate ONLY the file list, no upload form
let html = '';
if (!files || files.length === 0) {
html = '<p>No files found. Upload some content!</p>';
} else {
html = '<ul class="list-group">';
for (const file of files) {
html += `
<li class="list-group-item d-flex justify-content-between align-items-center">
${file}
<button class="btn btn-sm btn-danger" onclick="deleteBotFile('${botId}', '${file}')">Delete</button>
</li>
`;
}
html += '</ul>';
}
// Set the content area HTML to just the files list
contentArea.innerHTML = html;
}
// Upload media for the manual post
function quickUploadMedia(botId, file) {
const formData = new FormData();
formData.append('file', file);
quickUploadBtn.disabled = true;
quickUploadBtn.textContent = 'Uploading...';
// Show loading state in preview
mediaLinkContainer.innerHTML =
'<div class="spinner-border spinner-border-sm text-primary" role="status"></div> Uploading...';
const token = localStorage.getItem('authToken');
// First upload the file to our server
fetch(`/api/content/${botId}/upload`, {
method: 'POST',
headers: { 'Authorization': token },
body: formData
})
.then(res => {
if (!res.ok) throw new Error('Upload failed');
return res.json();
})
.then(data => {
// Now upload to the media server
return fetch(`/api/content/${botId}/uploadToMediaServer`, {
method: 'POST',
headers: {
'Authorization': token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
filename: data.filename,
service: globalMediaConfig.primaryService,
serverURL: globalMediaConfig.primaryURL
})
});
})
.then(res => {
if (!res.ok) throw new Error('Media server upload failed');
return res.json();
})
.then(data => {
// Reset button state
quickUploadBtn.disabled = false;
quickUploadBtn.textContent = 'Upload';
// Insert the media URL into the post content
const textArea = document.getElementById('manualPostContent');
let mediaUrl = data.url;
// Also update the media URL input field
const mediaUrlInput = document.getElementById('mediaUrlInput');
if (mediaUrlInput) {
mediaUrlInput.value = mediaUrl;
}
// Update preview with media info
mediaLinkContainer.innerHTML =
`<p class="mb-0">Media URL:</p>
<code class="text-info">${mediaUrl}</code>`;
// Insert into text area
textArea.value += (textArea.value ? '\n\n' : '') + mediaUrl;
})
.catch(err => {
console.error('Upload error:', err);
quickUploadBtn.disabled = false;
quickUploadBtn.textContent = 'Upload';
mediaLinkContainer.innerHTML =
`<p class="text-danger">Upload error: ${err.message}</p>`;
alert('Upload error: ' + err.message);
});
}
// Create a manual post (improved for kind 20 posts)
function createManualPost(botId) {
// Validate the form data first
if (!validatePostData()) {
return;
}
const content = manualPostContent.value.trim();
const hashtagsValue = postHashtags.value.trim();
const hashtags = hashtagsValue
? hashtagsValue.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0)
: [];
const kind = parseInt(document.querySelector('input[name="postKind"]:checked').value);
let title = '';
if (kind === 20) {
title = postTitle.value.trim();
}
// Extract media URLs and alt text
const mediaUrl = mediaUrlInput ? mediaUrlInput.value.trim() : '';
const altText = mediaAltText ? mediaAltText.value.trim() : '';
// Build the post data based on kind
const postData = {
kind: kind,
content: content,
hashtags: hashtags
};
if (kind === 20) {
postData.title = title;
// For kind 20, we need to ensure we have a valid URL
// If we have a specific media URL field value, add it to the content if not already there
if (mediaUrl && !content.includes(mediaUrl)) {
postData.content = content + '\n\n' + mediaUrl;
}
// Add alt text if provided
if (altText) {
postData.alt = altText;
}
}
// Disable submit button during request
submitPostBtn.disabled = true;
submitPostBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Posting...';
const token = localStorage.getItem('authToken');
fetch(`/api/bots/${botId}/post`, {
method: 'POST',
headers: {
'Authorization': token,
'Content-Type': 'application/json'
},
body: JSON.stringify(postData)
})
.then(res => {
if (!res.ok) {
return res.json().then(data => {
throw new Error(data.error || 'Failed to create post');
});
}
return res.json();
})
.then(data => {
alert('Post created successfully!');
console.log('Post success response:', data);
// Reset form
manualPostContent.value = '';
postHashtags.value = '';
postTitle.value = '';
postMediaInput.value = '';
if (mediaUrlInput) mediaUrlInput.value = '';
if (mediaAltText) mediaAltText.value = '';
mediaPreviewContainer.classList.add('d-none');
// Reset button
submitPostBtn.disabled = false;
submitPostBtn.textContent = 'Post Now';
})
.catch(err => {
console.error('Error creating post:', err);
alert('Error creating post: ' + err.message);
// Reset button
submitPostBtn.disabled = false;
submitPostBtn.textContent = 'Post Now';
});
}
// ===================================================
// Initialize media config
// ===================================================
// Try to load media config from localStorage
const savedConfig = localStorage.getItem('mediaConfig');
if (savedConfig) {
try {
globalMediaConfig = JSON.parse(savedConfig);
if (primaryServer) primaryServer.value = globalMediaConfig.primaryService || 'nip94';
if (primaryServerURL) primaryServerURL.value = globalMediaConfig.primaryURL || '';
if (fallbackServer) {
fallbackServer.value = globalMediaConfig.fallbackService || 'none';
}
if (fallbackServerURL) {
fallbackServerURL.value = globalMediaConfig.fallbackURL || '';
}
} catch (e) {
console.error('Error parsing saved media config:', e);
}
}
});
document.addEventListener('click', function(e) {
if (e.target.closest('.copy-btn')) {
const btn = e.target.closest('.copy-btn');
const valueToCopy = btn.getAttribute('data-value');
navigator.clipboard.writeText(valueToCopy)
.then(() => {
const originalHTML = btn.innerHTML;
btn.innerHTML = 'Copied!';
setTimeout(() => {
btn.innerHTML = originalHTML;
}, 2000);
})
.catch(err => {
console.error('Failed to copy: ', err);
});
}
});