← 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()">&times;</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})">&times;</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>