nice image selection from library
This commit is contained in:
parent
7319709749
commit
7e46dc71ec
5 changed files with 274 additions and 1 deletions
226
hub/services/admin/widgets.py
Normal file
226
hub/services/admin/widgets.py
Normal file
|
@ -0,0 +1,226 @@
|
|||
"""
|
||||
Custom widgets for Django admin interface
|
||||
"""
|
||||
|
||||
from django import forms
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from ..models import ImageLibrary
|
||||
|
||||
|
||||
class ImageLibraryWidget(forms.Select):
|
||||
"""Custom widget for selecting images from the library with visual preview"""
|
||||
|
||||
def __init__(self, attrs=None):
|
||||
super().__init__(attrs)
|
||||
self.attrs.update(
|
||||
{
|
||||
"class": "image-library-select",
|
||||
"style": "display: none;", # Hide the original select
|
||||
}
|
||||
)
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
"""Render the widget with image previews"""
|
||||
# Get the original select element
|
||||
original_select = super().render(name, value, attrs, renderer)
|
||||
|
||||
# Get all images from the library
|
||||
images = ImageLibrary.objects.all().order_by("-uploaded_at")
|
||||
|
||||
# Create the visual interface
|
||||
html_parts = [
|
||||
'<div class="image-library-widget">',
|
||||
original_select, # Keep the original select for form submission
|
||||
'<div class="image-library-grid">',
|
||||
]
|
||||
|
||||
# Add "No image" option
|
||||
no_image_selected = "selected" if not value else ""
|
||||
html_parts.append(
|
||||
f"""
|
||||
<div class="image-option {no_image_selected}" data-value="">
|
||||
<div class="image-preview no-image">
|
||||
<i class="fas fa-ban"></i>
|
||||
<span>No image</span>
|
||||
</div>
|
||||
<div class="image-info">
|
||||
<span class="image-name">No image selected</span>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
# Add each image as an option
|
||||
for image in images:
|
||||
selected = "selected" if str(image.pk) == str(value) else ""
|
||||
image_url = image.image.url if image.image else ""
|
||||
|
||||
html_parts.append(
|
||||
f"""
|
||||
<div class="image-option {selected}" data-value="{image.pk}">
|
||||
<div class="image-preview">
|
||||
<img src="{image_url}" alt="{image.alt_text}" loading="lazy">
|
||||
</div>
|
||||
<div class="image-info">
|
||||
<span class="image-name">{image.name}</span>
|
||||
<span class="image-category">{image.get_category_display()}</span>
|
||||
<span class="image-size">{image.width}x{image.height}</span>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
html_parts.extend(
|
||||
[
|
||||
"</div>",
|
||||
"</div>",
|
||||
self._get_styles(),
|
||||
self._get_javascript(),
|
||||
]
|
||||
)
|
||||
|
||||
return mark_safe("".join(html_parts))
|
||||
|
||||
def _get_styles(self):
|
||||
"""Return CSS styles for the widget"""
|
||||
return """
|
||||
<style>
|
||||
.image-library-widget {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.image-library-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
max-height: 800px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
padding: 15px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.image-option {
|
||||
background: white;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-option:hover {
|
||||
border-color: #007cba;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.image-option.selected {
|
||||
border-color: #007cba;
|
||||
background: #e3f2fd;
|
||||
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2);
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.image-preview.no-image {
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.image-preview.no-image i {
|
||||
font-size: 24px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.image-info {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.image-name {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
margin-bottom: 3px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.image-category {
|
||||
display: inline-block;
|
||||
background: #e0e0e0;
|
||||
color: #666;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.image-size {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
|
||||
def _get_javascript(self):
|
||||
"""Return JavaScript for the widget functionality"""
|
||||
return """
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Handle image selection
|
||||
document.querySelectorAll('.image-option').forEach(function(option) {
|
||||
option.addEventListener('click', function() {
|
||||
const widget = this.closest('.image-library-widget');
|
||||
const select = widget.querySelector('.image-library-select');
|
||||
const value = this.dataset.value;
|
||||
|
||||
// Update the hidden select
|
||||
select.value = value;
|
||||
|
||||
// Update visual selection
|
||||
widget.querySelectorAll('.image-option').forEach(function(opt) {
|
||||
opt.classList.remove('selected');
|
||||
});
|
||||
this.classList.add('selected');
|
||||
|
||||
// Trigger change event
|
||||
select.dispatchEvent(new Event('change'));
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
"""
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
"all": (
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css",
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue