{% 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 %}