← back to Jill Website
views/admin.ejs
582 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Content Management System - Admin</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f5f7fa;
color: #2d3748;
line-height: 1.6;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem 2rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header h1 {
font-size: 1.75rem;
font-weight: 600;
}
.container {
max-width: 1400px;
margin: 2rem auto;
padding: 0 2rem;
}
.page-list {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.page-item {
padding: 1.5rem;
border-bottom: 1px solid #e2e8f0;
cursor: pointer;
transition: background-color 0.2s;
}
.page-item:hover {
background: #f7fafc;
}
.page-item:last-child {
border-bottom: none;
}
.page-title {
font-size: 1.25rem;
font-weight: 600;
color: #2d3748;
margin-bottom: 0.5rem;
}
.page-slug {
font-size: 0.875rem;
color: #718096;
font-family: monospace;
background: #edf2f7;
padding: 0.25rem 0.5rem;
border-radius: 4px;
display: inline-block;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
overflow-y: auto;
}
.modal.active {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.modal-content {
background: white;
border-radius: 8px;
width: 100%;
max-width: 900px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04);
}
.modal-header {
padding: 1.5rem;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
font-size: 1.5rem;
font-weight: 600;
}
.modal-body {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #2d3748;
}
.form-group input[type="text"],
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #cbd5e0;
border-radius: 4px;
font-size: 1rem;
font-family: inherit;
transition: border-color 0.2s;
}
.form-group input[type="text"]:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-group textarea {
min-height: 200px;
resize: vertical;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.btn-secondary {
background: #e2e8f0;
color: #4a5568;
}
.btn-secondary:hover {
background: #cbd5e0;
}
.btn-success {
background: #48bb78;
color: white;
}
.btn-success:hover {
background: #38a169;
}
.modal-footer {
padding: 1.5rem;
border-top: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
gap: 1rem;
}
.gallery-section {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid #e2e8f0;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.gallery-item {
position: relative;
aspect-ratio: 1;
border-radius: 4px;
overflow: hidden;
border: 2px solid #e2e8f0;
}
.gallery-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.gallery-item-remove {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: rgba(220, 38, 38, 0.9);
color: white;
border: none;
width: 28px;
height: 28px;
border-radius: 50%;
cursor: pointer;
font-size: 1.25rem;
line-height: 1;
transition: all 0.2s;
}
.gallery-item-remove:hover {
background: #dc2626;
transform: scale(1.1);
}
.upload-area {
border: 2px dashed #cbd5e0;
border-radius: 4px;
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
margin-top: 1rem;
}
.upload-area:hover {
border-color: #667eea;
background: #f7fafc;
}
.upload-area.dragover {
border-color: #667eea;
background: #eef2ff;
}
.message {
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
.message.success {
background: #c6f6d5;
color: #22543d;
border: 1px solid #9ae6b4;
}
.message.error {
background: #fed7d7;
color: #742a2a;
border: 1px solid #fc8181;
}
.preview-section {
margin-top: 2rem;
padding: 1.5rem;
background: #f7fafc;
border-radius: 4px;
border: 1px solid #e2e8f0;
}
.preview-section h3 {
margin-bottom: 1rem;
font-size: 1.125rem;
}
.close-btn {
background: none;
border: none;
font-size: 2rem;
color: #718096;
cursor: pointer;
line-height: 1;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #2d3748;
}
@media (max-width: 768px) {
.container {
padding: 0 1rem;
}
.modal-content {
max-height: 95vh;
}
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
}
</style>
</head>
<body>
<div class="header">
<h1>Content Management System</h1>
</div>
<div class="container">
<div id="message-container"></div>
<div class="page-list">
<% pages.forEach(page => { %>
<div class="page-item" onclick="editPage(<%= page.id %>)">
<div class="page-title"><%= page.page_title %></div>
<span class="page-slug">/<%= page.page_slug %></span>
</div>
<% }); %>
</div>
</div>
<!-- Edit Modal -->
<div id="editModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modalTitle">Edit Page</h2>
<button class="close-btn" onclick="closeModal()">×</button>
</div>
<div class="modal-body">
<form id="editForm">
<input type="hidden" id="pageId">
<div class="form-group">
<label for="pageTitle">Page Title</label>
<input type="text" id="pageTitle" required>
</div>
<div class="form-group">
<label for="pageContent">Content</label>
<textarea id="pageContent" required></textarea>
</div>
<div class="gallery-section">
<h3>Gallery Images</h3>
<div class="gallery-grid" id="galleryGrid"></div>
<div class="upload-area" id="uploadArea">
<p>Click or drag images here to upload</p>
<input type="file" id="imageUpload" accept="image/*" style="display: none;">
</div>
</div>
<div class="preview-section">
<h3>Preview</h3>
<div id="previewContent"></div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveChanges()">Save Changes</button>
</div>
</div>
</div>
<script>
let currentPage = null;
let galleryImages = [];
function showMessage(message, type) {
const container = document.getElementById('message-container');
const messageEl = document.createElement('div');
messageEl.className = `message ${type}`;
messageEl.textContent = message;
container.appendChild(messageEl);
setTimeout(() => messageEl.remove(), 5000);
}
async function editPage(id) {
try {
const response = await fetch(`/admin/api/pages/${id}`, {
credentials: 'same-origin'
});
if (!response.ok) throw new Error('Failed to load page');
const result = await response.json();
currentPage = result.data;
galleryImages = currentPage.gallery_images || [];
document.getElementById('pageId').value = currentPage.id;
document.getElementById('pageTitle').value = currentPage.page_title;
document.getElementById('pageContent').value = currentPage.content;
updateGalleryDisplay();
updatePreview();
document.getElementById('editModal').classList.add('active');
} catch (error) {
showMessage('Failed to load page: ' + error.message, 'error');
}
}
function closeModal() {
document.getElementById('editModal').classList.remove('active');
currentPage = null;
galleryImages = [];
}
function updateGalleryDisplay() {
const grid = document.getElementById('galleryGrid');
grid.innerHTML = '';
galleryImages.forEach((url, index) => {
const item = document.createElement('div');
item.className = 'gallery-item';
item.innerHTML = `
<img src="${url}" alt="Gallery image">
<button class="gallery-item-remove" onclick="removeImage(${index})">×</button>
`;
grid.appendChild(item);
});
}
function removeImage(index) {
galleryImages.splice(index, 1);
updateGalleryDisplay();
updatePreview();
}
function updatePreview() {
const content = document.getElementById('pageContent').value;
const preview = document.getElementById('previewContent');
preview.innerHTML = `<p>${content}</p>`;
}
async function uploadImage(file) {
const formData = new FormData();
formData.append('image', file);
try {
const response = await fetch('/admin/api/upload', {
method: 'POST',
credentials: 'same-origin',
body: formData
});
if (!response.ok) throw new Error('Upload failed');
const result = await response.json();
return result.data.url;
} catch (error) {
showMessage('Failed to upload image: ' + error.message, 'error');
throw error;
}
}
async function saveChanges() {
try {
const id = document.getElementById('pageId').value;
const title = document.getElementById('pageTitle').value;
const content = document.getElementById('pageContent').value;
const response = await fetch(`/admin/api/pages/${id}`, {
method: 'PUT',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: title,
content: content,
images: galleryImages
})
});
if (!response.ok) throw new Error('Failed to save changes');
showMessage('Changes saved successfully!', 'success');
closeModal();
setTimeout(() => location.reload(), 1000);
} catch (error) {
showMessage('Failed to save changes: ' + error.message, 'error');
}
}
// Setup upload area
const uploadArea = document.getElementById('uploadArea');
const imageUpload = document.getElementById('imageUpload');
uploadArea.addEventListener('click', () => imageUpload.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', async (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const files = Array.from(e.dataTransfer.files);
for (const file of files) {
if (file.type.startsWith('image/')) {
try {
const url = await uploadImage(file);
galleryImages.push(url);
updateGalleryDisplay();
updatePreview();
} catch (error) {
// Error already shown by uploadImage
}
}
}
});
imageUpload.addEventListener('change', async (e) => {
const files = Array.from(e.target.files);
for (const file of files) {
try {
const url = await uploadImage(file);
galleryImages.push(url);
updateGalleryDisplay();
updatePreview();
} catch (error) {
// Error already shown by uploadImage
}
}
imageUpload.value = '';
});
// Update preview as user types
document.getElementById('pageContent').addEventListener('input', updatePreview);
document.getElementById('pageTitle').addEventListener('input', updatePreview);
// Close modal on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && document.getElementById('editModal').classList.contains('active')) {
closeModal();
}
});
</script>
</body>
</html>