adminx 0.2.6

A powerful, modern admin panel framework for Rust built on Actix Web and MongoDB with automatic CRUD, role-based access control, and a beautiful responsive UI
Documentation
{% extends "layout.html.tera" %}

{% block title %}View {{ resource_name }}{% endblock title %}

{% block content %}
<!-- Toast Notification -->
{% if toast_message %}
<div id="toast" class="fixed top-4 right-4 z-50 flex items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert">
  <div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg {% if toast_type == 'success' %}text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200{% else %}text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200{% endif %}">
    {% if toast_type == "success" %}
      <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
        <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
      </svg>
    {% else %}
      <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
        <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"/>
      </svg>
    {% endif %}
  </div>
  <div class="ml-3 text-sm font-normal">{{ toast_message }}</div>
  <button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" onclick="document.getElementById('toast').remove()">
    <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
      <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
    </svg>
  </button>
</div>
<script>
  // Auto-hide toast after 5 seconds
  setTimeout(function() {
    const toast = document.getElementById('toast');
    if (toast) {
      toast.style.transition = 'opacity 0.3s ease-out';
      toast.style.opacity = '0';
      setTimeout(() => toast.remove(), 300);
    }
  }, 5000);
</script>
{% endif %}

<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
  <!-- Header -->
  <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
    <div class="flex justify-between items-center">
      <h2 class="text-2xl font-bold text-gray-900 dark:text-white">
        {{ resource_name | capitalize }} Details
      </h2>
      <div class="flex gap-2">
        <a href="/adminx/{{ base_path }}/edit/{{ record.id }}" 
           class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium">
          Edit
        </a>
        <a href="/adminx/{{ base_path }}/list" 
           class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium">
          Back to List
        </a>
      </div>
    </div>
  </div>

  <!-- Content -->
  <div class="px-6 py-4">
    <dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
      {% for key, value in record %}
        {% if key != "id" %} <!-- Don't show the technical ID -->
        <div class="media-field-{{ loop.index }}" data-key="{{ key }}" data-value="{{ value }}">
          <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
            {{ key | replace(from="_", to=" ") | title }}
          </dt>
          <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
            {% if not value or value == "" %}
              <span class="text-gray-400 italic">Not provided</span>
            {% else %}
              <div class="media-content">
                <span class="media-url-link">
                  <a href="{{ value }}" target="_blank" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 underline break-all">
                    {{ value }}
                  </a>
                </span>
                
                <!-- Image container -->
                <div class="media-image-container hidden mt-2 border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
                  <img src="{{ value }}" 
                       alt="Image preview" 
                       class="w-full h-auto max-h-96 object-contain bg-gray-50 dark:bg-gray-700"
                       loading="lazy"
                       onerror="this.parentElement.querySelector('.image-error').classList.remove('hidden'); this.classList.add('hidden');">
                  <div class="image-error hidden p-4 text-center text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-600">
                    <svg class="mx-auto w-12 h-12 mb-2" fill="currentColor" viewBox="0 0 24 24">
                      <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
                    </svg>
                    Unable to load image
                  </div>
                </div>

                <!-- SVG iframe container -->
                <div class="media-svg-container hidden mt-2 border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
                  <iframe src="{{ value }}" 
                          class="w-full h-64 border-0" 
                          sandbox="allow-scripts"
                          title="SVG Preview"
                          loading="lazy">
                  </iframe>
                </div>

                <!-- Video container -->
                <div class="media-video-container hidden mt-2 border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
                  <video controls class="w-full h-auto max-h-96" preload="metadata">
                    <source src="{{ value }}">
                    <p class="p-4 text-center text-gray-500 dark:text-gray-400">
                      Your browser doesn't support HTML5 video. 
                      <a href="{{ value }}" class="text-blue-600 underline">Download the video</a>.
                    </p>
                  </video>
                </div>

                <!-- PDF container -->
                <div class="media-pdf-container hidden mt-2 border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
                  <iframe src="{{ value }}" 
                          class="w-full h-96 border-0" 
                          title="PDF Preview"
                          loading="lazy">
                    <p class="p-4 text-center text-gray-500 dark:text-gray-400">
                      Unable to display PDF. 
                      <a href="{{ value }}" target="_blank" class="text-blue-600 underline">Open in new tab</a>.
                    </p>
                  </iframe>
                </div>

                <!-- YouTube container -->
                <div class="media-youtube-container hidden mt-2 border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
                  <iframe class="w-full h-64 border-0" 
                          allowfullscreen
                          title="YouTube Video"
                          loading="lazy">
                  </iframe>
                </div>

                <!-- Vimeo container -->
                <div class="media-vimeo-container hidden mt-2 border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
                  <iframe class="w-full h-64 border-0" 
                          allowfullscreen
                          title="Vimeo Video"
                          loading="lazy">
                  </iframe>
                </div>

                <!-- Generic iframe container -->
                <div class="media-iframe-container hidden mt-2">
                  <button onclick="toggleGenericIframe(this)" 
                          class="text-sm bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded mb-2">
                    Preview in frame
                  </button>
                  <div class="iframe-content hidden border border-gray-300 dark:border-gray-600 rounded overflow-hidden">
                    <iframe src="{{ value }}" 
                            class="w-full h-64 border-0" 
                            sandbox="allow-scripts allow-same-origin allow-forms"
                            title="Content Preview"
                            loading="lazy">
                    </iframe>
                  </div>
                </div>

                <!-- Regular text fallback -->
                <div class="media-text-container">
                  {{ value }}
                </div>
              </div>
            {% endif %}
          </dd>
        </div>
        {% endif %}
      {% endfor %}
    </dl>

    {% set item_id = record.id | default(value=record.id) %}
    {% include "view_custom_actions.html.tera" %}
  </div>

  <!-- Actions Footer -->
  <div class="px-6 py-4 bg-gray-50 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-600">
    <div class="flex justify-between items-center">
      <div class="flex gap-2">
        <a href="{{ base_path }}/edit/{{ record.id }}" 
           class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
          <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
          </svg>
          Edit
        </a>
        <form method="post" action="{{ base_path }}/delete/{{ record.id }}" 
              style="display:inline;" 
              onsubmit="return confirm('Are you sure you want to delete this {{ resource_name | lower }}?')">
          <button type="submit" 
                  class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
            <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
            </svg>
            Delete
          </button>
        </form>
      </div>
      <a href="{{ base_path }}/list" 
         class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-700">
        <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
        </svg>
        Back to List
      </a>
    </div>
  </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
  // Process each field to detect media types
  document.querySelectorAll('[data-value]').forEach(function(field) {
    const value = field.getAttribute('data-value');
    const mediaContent = field.querySelector('.media-content');
    
    if (!value || !mediaContent) return;
    
    // Check if it's a URL
    const isUrl = /^https?:\/\//i.test(value);
    
    if (!isUrl) {
      // Show as regular text
      mediaContent.querySelector('.media-text-container').style.display = 'block';
      mediaContent.querySelector('.media-url-link').style.display = 'none';
      return;
    }
    
    // It's a URL, so show the link
    const urlLower = value.toLowerCase();
    
    // Regex patterns for different media types
    const patterns = {
      image: /\.(jpe?g|png|gif|bmp|webp|ico|tiff?)(\?.*)?$/i,
      svg: /\.svg(\?.*)?$/i,
      video: /\.(mp4|webm|ogg|avi|mov|wmv|flv|mkv)(\?.*)?$/i,
      pdf: /\.pdf(\?.*)?$/i,
      youtube: /(youtube\.com\/watch\?v=|youtu\.be\/)/i,
      vimeo: /vimeo\.com\//i
    };
    
    let mediaType = null;
    
    // Check each pattern
    for (const [type, pattern] of Object.entries(patterns)) {
      if (pattern.test(value)) {
        mediaType = type;
        break;
      }
    }
    
    // Hide text container and show appropriate media container
    mediaContent.querySelector('.media-text-container').style.display = 'none';
    
    if (mediaType === 'image') {
      const container = mediaContent.querySelector('.media-image-container');
      container.classList.remove('hidden');
      // Expand to full width for images
      field.classList.add('sm:col-span-2');
      
    } else if (mediaType === 'svg') {
      const container = mediaContent.querySelector('.media-svg-container');
      container.classList.remove('hidden');
      field.classList.add('sm:col-span-2');
      
    } else if (mediaType === 'video') {
      const container = mediaContent.querySelector('.media-video-container');
      container.classList.remove('hidden');
      field.classList.add('sm:col-span-2');
      
    } else if (mediaType === 'pdf') {
      const container = mediaContent.querySelector('.media-pdf-container');
      container.classList.remove('hidden');
      field.classList.add('sm:col-span-2');
      
    } else if (mediaType === 'youtube') {
      const container = mediaContent.querySelector('.media-youtube-container');
      const iframe = container.querySelector('iframe');
      
      // Extract video ID
      let videoId = '';
      const match = value.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\n?#]+)/);
      if (match) {
        videoId = match[1];
        iframe.src = `https://www.youtube.com/embed/${videoId}`;
        container.classList.remove('hidden');
        field.classList.add('sm:col-span-2');
      }
      
    } else if (mediaType === 'vimeo') {
      const container = mediaContent.querySelector('.media-vimeo-container');
      const iframe = container.querySelector('iframe');
      
      // Extract video ID
      const match = value.match(/vimeo\.com\/([0-9]+)/);
      if (match) {
        const videoId = match[1];
        iframe.src = `https://player.vimeo.com/video/${videoId}`;
        container.classList.remove('hidden');
        field.classList.add('sm:col-span-2');
      }
      
    } else {
      // Show generic iframe option for other URLs
      const container = mediaContent.querySelector('.media-iframe-container');
      container.classList.remove('hidden');
    }
  });
});

function toggleGenericIframe(button) {
  const content = button.nextElementSibling;
  if (content.classList.contains('hidden')) {
    content.classList.remove('hidden');
    button.textContent = 'Hide preview';
  } else {
    content.classList.add('hidden');
    button.textContent = 'Preview in frame';
  }
}
</script>
{% endblock content %}