533 lines
16 KiB
HTML
533 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Site Content Editor - Admin Panel</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||
background: #f5f5f5;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
padding: 30px;
|
||
}
|
||
|
||
h1 {
|
||
margin-bottom: 10px;
|
||
color: #333;
|
||
}
|
||
|
||
.subtitle {
|
||
color: #666;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.toolbar {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
button {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
button:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #4CAF50;
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #2196F3;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #f44336;
|
||
color: white;
|
||
}
|
||
|
||
.btn-warning {
|
||
background: #ff9800;
|
||
color: white;
|
||
}
|
||
|
||
.locale-selector {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
align-items: center;
|
||
}
|
||
|
||
.locale-selector label {
|
||
font-weight: 600;
|
||
}
|
||
|
||
select {
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.content-grid {
|
||
display: grid;
|
||
gap: 15px;
|
||
}
|
||
|
||
.content-item {
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 4px;
|
||
padding: 15px;
|
||
background: #fafafa;
|
||
}
|
||
|
||
.content-item-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.content-key {
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 13px;
|
||
color: #1976D2;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.content-item input, .content-item textarea {
|
||
width: 100%;
|
||
padding: 8px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
font-family: inherit;
|
||
}
|
||
|
||
.content-item textarea {
|
||
min-height: 80px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.btn-delete {
|
||
background: #f44336;
|
||
color: white;
|
||
padding: 6px 12px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.new-item-form {
|
||
background: #e3f2fd;
|
||
border: 2px dashed #2196F3;
|
||
padding: 20px;
|
||
border-radius: 4px;
|
||
margin-bottom: 20px;
|
||
display: none;
|
||
}
|
||
|
||
.new-item-form.active {
|
||
display: block;
|
||
}
|
||
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 2fr;
|
||
gap: 10px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: #666;
|
||
}
|
||
|
||
.success-message {
|
||
background: #4CAF50;
|
||
color: white;
|
||
padding: 12px;
|
||
border-radius: 4px;
|
||
margin-bottom: 20px;
|
||
display: none;
|
||
}
|
||
|
||
.success-message.active {
|
||
display: block;
|
||
}
|
||
|
||
.error-message {
|
||
background: #f44336;
|
||
color: white;
|
||
padding: 12px;
|
||
border-radius: 4px;
|
||
margin-bottom: 20px;
|
||
display: none;
|
||
}
|
||
|
||
.error-message.active {
|
||
display: block;
|
||
}
|
||
|
||
.stats {
|
||
background: #f5f5f5;
|
||
padding: 15px;
|
||
border-radius: 4px;
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
gap: 30px;
|
||
}
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: #666;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>🎨 Site Content Editor</h1>
|
||
<p class="subtitle">Manage modal content, rules, and site text</p>
|
||
|
||
<div class="success-message" id="successMessage"></div>
|
||
<div class="error-message" id="errorMessage"></div>
|
||
|
||
<div class="stats">
|
||
<div class="stat-item">
|
||
<span class="stat-label">Total Items</span>
|
||
<span class="stat-value" id="totalItems">0</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">Current Locale</span>
|
||
<span class="stat-value" id="currentLocale">en</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toolbar">
|
||
<div class="locale-selector">
|
||
<label for="localeSelect">Locale:</label>
|
||
<select id="localeSelect">
|
||
<option value="en">English (en)</option>
|
||
<option value="zh">Chinese (zh)</option>
|
||
</select>
|
||
</div>
|
||
<button class="btn-primary" onclick="loadContent()">🔄 Reload</button>
|
||
<button class="btn-secondary" onclick="toggleNewItemForm()">➕ Add New Item</button>
|
||
<button class="btn-warning" onclick="initializeDefaults()">🔧 Initialize Defaults</button>
|
||
<button class="btn-primary" onclick="saveAllChanges()">💾 Save All Changes</button>
|
||
</div>
|
||
|
||
<div class="new-item-form" id="newItemForm">
|
||
<h3>Add New Content Item</h3>
|
||
<div class="form-row">
|
||
<input type="text" id="newKey" placeholder="Key (e.g., modal.rules.item.5)">
|
||
<textarea id="newValue" placeholder="Value"></textarea>
|
||
</div>
|
||
<button class="btn-primary" onclick="addNewItem()">Add Item</button>
|
||
<button class="btn-secondary" onclick="toggleNewItemForm()">Cancel</button>
|
||
</div>
|
||
|
||
<div id="contentContainer" class="loading">
|
||
Loading content...
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentContent = [];
|
||
let currentLocale = 'en';
|
||
let changedItems = new Set();
|
||
|
||
// Get JWT token from cookie
|
||
function getAuthToken() {
|
||
const cookies = document.cookie.split(';');
|
||
for (let cookie of cookies) {
|
||
const [name, value] = cookie.trim().split('=');
|
||
if (name === 'j') {
|
||
return value;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Show success message
|
||
function showSuccess(message) {
|
||
const el = document.getElementById('successMessage');
|
||
el.textContent = message;
|
||
el.classList.add('active');
|
||
setTimeout(() => el.classList.remove('active'), 3000);
|
||
}
|
||
|
||
// Show error message
|
||
function showError(message) {
|
||
const el = document.getElementById('errorMessage');
|
||
el.textContent = message;
|
||
el.classList.add('active');
|
||
setTimeout(() => el.classList.remove('active'), 5000);
|
||
}
|
||
|
||
// Toggle new item form
|
||
function toggleNewItemForm() {
|
||
const form = document.getElementById('newItemForm');
|
||
form.classList.toggle('active');
|
||
}
|
||
|
||
// Load content from API
|
||
async function loadContent() {
|
||
const locale = document.getElementById('localeSelect').value;
|
||
currentLocale = locale;
|
||
|
||
try {
|
||
const token = getAuthToken();
|
||
const response = await fetch('/api/admin/site-content', {
|
||
headers: {
|
||
'Cookie': `j=${token}`
|
||
},
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to load content');
|
||
}
|
||
|
||
const data = await response.json();
|
||
currentContent = data.content.filter(item => item.locale === locale);
|
||
|
||
renderContent();
|
||
updateStats();
|
||
showSuccess(`Loaded ${currentContent.length} items for locale: ${locale}`);
|
||
} catch (error) {
|
||
showError('Error loading content: ' + error.message);
|
||
console.error('Error:', error);
|
||
}
|
||
}
|
||
|
||
// Render content items
|
||
function renderContent() {
|
||
const container = document.getElementById('contentContainer');
|
||
|
||
if (currentContent.length === 0) {
|
||
container.innerHTML = '<div class="loading">No content found. Click "Initialize Defaults" to create default content.</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = '';
|
||
container.className = 'content-grid';
|
||
|
||
currentContent.forEach((item, index) => {
|
||
const itemEl = document.createElement('div');
|
||
itemEl.className = 'content-item';
|
||
itemEl.innerHTML = `
|
||
<div class="content-item-header">
|
||
<span class="content-key">${escapeHtml(item.key)}</span>
|
||
<button class="btn-delete" onclick="deleteItem('${escapeHtml(item.key)}')">🗑 Delete</button>
|
||
</div>
|
||
${item.value.length > 100
|
||
? `<textarea data-key="${escapeHtml(item.key)}" onchange="markChanged('${escapeHtml(item.key)}')">${escapeHtml(item.value)}</textarea>`
|
||
: `<input type="text" data-key="${escapeHtml(item.key)}" value="${escapeHtml(item.value)}" onchange="markChanged('${escapeHtml(item.key)}')">`
|
||
}
|
||
`;
|
||
container.appendChild(itemEl);
|
||
});
|
||
}
|
||
|
||
// Mark item as changed
|
||
function markChanged(key) {
|
||
changedItems.add(key);
|
||
}
|
||
|
||
// Update statistics
|
||
function updateStats() {
|
||
document.getElementById('totalItems').textContent = currentContent.length;
|
||
document.getElementById('currentLocale').textContent = currentLocale;
|
||
}
|
||
|
||
// Add new item
|
||
async function addNewItem() {
|
||
const key = document.getElementById('newKey').value.trim();
|
||
const value = document.getElementById('newValue').value.trim();
|
||
|
||
if (!key || !value) {
|
||
showError('Both key and value are required');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const token = getAuthToken();
|
||
const response = await fetch('/api/admin/site-content', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Cookie': `j=${token}`
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify({ key, value, locale: currentLocale })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to add item');
|
||
}
|
||
|
||
document.getElementById('newKey').value = '';
|
||
document.getElementById('newValue').value = '';
|
||
toggleNewItemForm();
|
||
loadContent();
|
||
showSuccess('Item added successfully');
|
||
} catch (error) {
|
||
showError('Error adding item: ' + error.message);
|
||
console.error('Error:', error);
|
||
}
|
||
}
|
||
|
||
// Delete item
|
||
async function deleteItem(key) {
|
||
if (!confirm(`Delete "${key}"?`)) return;
|
||
|
||
try {
|
||
const token = getAuthToken();
|
||
const response = await fetch(`/api/admin/site-content/${encodeURIComponent(key)}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Cookie': `j=${token}`
|
||
},
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to delete item');
|
||
}
|
||
|
||
loadContent();
|
||
showSuccess('Item deleted successfully');
|
||
} catch (error) {
|
||
showError('Error deleting item: ' + error.message);
|
||
console.error('Error:', error);
|
||
}
|
||
}
|
||
|
||
// Save all changes
|
||
async function saveAllChanges() {
|
||
const items = [];
|
||
const inputs = document.querySelectorAll('[data-key]');
|
||
|
||
inputs.forEach(input => {
|
||
const key = input.getAttribute('data-key');
|
||
if (changedItems.has(key)) {
|
||
items.push({
|
||
key: key,
|
||
value: input.value,
|
||
locale: currentLocale
|
||
});
|
||
}
|
||
});
|
||
|
||
if (items.length === 0) {
|
||
showError('No changes to save');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const token = getAuthToken();
|
||
const response = await fetch('/api/admin/site-content/bulk', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Cookie': `j=${token}`
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify({ items })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to save changes');
|
||
}
|
||
|
||
changedItems.clear();
|
||
loadContent();
|
||
showSuccess(`Saved ${items.length} items successfully`);
|
||
} catch (error) {
|
||
showError('Error saving changes: ' + error.message);
|
||
console.error('Error:', error);
|
||
}
|
||
}
|
||
|
||
// Initialize default content
|
||
async function initializeDefaults() {
|
||
if (!confirm('Initialize default content? This will create/update default modal content.')) return;
|
||
|
||
try {
|
||
const token = getAuthToken();
|
||
const response = await fetch('/api/admin/site-content/initialize', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Cookie': `j=${token}`
|
||
},
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to initialize defaults');
|
||
}
|
||
|
||
const data = await response.json();
|
||
loadContent();
|
||
showSuccess(`Initialized ${data.initialized} default items`);
|
||
} catch (error) {
|
||
showError('Error initializing defaults: ' + error.message);
|
||
console.error('Error:', error);
|
||
}
|
||
}
|
||
|
||
// Escape HTML
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// Event listeners
|
||
document.getElementById('localeSelect').addEventListener('change', loadContent);
|
||
|
||
// Initial load
|
||
loadContent();
|
||
</script>
|
||
</body>
|
||
</html>
|