609 lines
23 KiB
JavaScript
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);
|
|
});
|
|
}
|
|
}); |