Add tags to image collection

This commit is contained in:
2024-12-19 17:07:32 +11:00
parent 4db9c6674f
commit 9eec1ec34f
8 changed files with 197 additions and 16 deletions
BIN
View File
Binary file not shown.
+2 -1
View File
@@ -88,9 +88,10 @@ class MultipleFileField(forms.FileField):
class ImageCollectionForm(forms.ModelForm):
class Meta:
model = ImageCollection
fields = ['name', 'description']
fields = ['name', 'description', 'tags']
widgets = {
'description': forms.Textarea(attrs={'rows': 3}),
'tags': forms.SelectMultiple(attrs={'class': 'select2'}),
}
class ImageUploadForm(forms.ModelForm):
@@ -0,0 +1,18 @@
# Generated by Django 5.0.9 on 2024-12-19 05:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('links', '0027_remove_tag_color'),
]
operations = [
migrations.AddField(
model_name='imagecollection',
name='tags',
field=models.ManyToManyField(blank=True, related_name='image_collections', to='links.tag', verbose_name='Tags'),
),
]
+1
View File
@@ -275,6 +275,7 @@ class ImageCollection(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(_('Name'), max_length=200)
description = models.TextField(_('Description'), blank=True)
tags = models.ManyToManyField('Tag', verbose_name=_('Tags'), blank=True, related_name='image_collections')
created_at = models.DateTimeField(_('Created at'), auto_now_add=True)
updated_at = models.DateTimeField(_('Updated at'), auto_now=True)
+25 -2
View File
@@ -176,10 +176,25 @@
<div class="flex flex-col space-y-4 sm:space-y-0 sm:flex-row sm:justify-between sm:items-start mb-6">
<!-- Title and Description -->
<div class="flex-1">
<h1 class="text-2xl sm:text-3xl font-bold text-gray-900">{{ collection.name }}</h1>
{% if collection.description %}
<p class="mt-2 text-gray-600">{{ collection.description }}</p>
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900 mb-2">{% trans "Description" %}</h3>
<p class="text-gray-600">{{ collection.description }}</p>
</div>
{% endif %}
<!-- Tags Section -->
<div class="mb-6">
<div class="flex flex-wrap gap-2">
{% for tag in collection.tags.all %}
<a href="{% url 'tag-detail' tag.slug %}" class="tag-item">
{{ tag.name }}
</a>
{% empty %}
<span class="text-sm text-gray-500">{% trans "No tags" %}</span>
{% endfor %}
</div>
</div>
</div>
<!-- Action Buttons -->
@@ -319,6 +334,14 @@
{% block extra_js %}
<script src="https://unpkg.com/dropzone@5/dist/min/dropzone.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const colors = ['blue', 'green', 'yellow', 'red', 'indigo', 'purple', 'pink'];
document.querySelectorAll('.tag-item').forEach(tag => {
const randomColor = colors[Math.floor(Math.random() * colors.length)];
tag.className = `tag-item inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-${randomColor}-50 text-${randomColor}-700 hover:bg-${randomColor}-100`;
});
});
Dropzone.autoDiscover = false;
const dropzone = new Dropzone("#uploadForm", {
+67 -3
View File
@@ -1,6 +1,24 @@
{% extends 'base.html' %}
{% load i18n %}
{% block extra_css %}
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<style>
.select2-container--classic .select2-selection--multiple {
border: 1px solid #d1d5db !important;
border-radius: 0.375rem !important;
min-height: 38px !important;
}
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
background-color: #e5e7eb !important;
border: 1px solid #d1d5db !important;
border-radius: 0.25rem !important;
padding: 2px 8px !important;
margin: 3px !important;
}
</style>
{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Back Button -->
@@ -38,7 +56,7 @@
<input type="text"
name="{{ form.name.name }}"
id="{{ form.name.id_for_label }}"
value="{{ form.name.value|default:'' }}"
value="{{ form.name.value|default_if_none:'' }}"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
required>
</div>
@@ -56,13 +74,26 @@
<textarea name="{{ form.description.name }}"
id="{{ form.description.id_for_label }}"
rows="3"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border border-gray-300 rounded-md">{{ form.description.value|default:'' }}</textarea>
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md">{{ form.description.value|default_if_none:'' }}</textarea>
</div>
{% if form.description.errors %}
<p class="mt-2 text-sm text-red-600">{{ form.description.errors.0 }}</p>
{% endif %}
</div>
<!-- Tags Field -->
<div>
<label for="{{ form.tags.id_for_label }}" class="block text-sm font-medium text-gray-700">
{{ form.tags.label }}
</label>
<div class="mt-1">
{{ form.tags }}
</div>
{% if form.tags.errors %}
<p class="mt-2 text-sm text-red-600">{{ form.tags.errors.0 }}</p>
{% endif %}
</div>
<!-- Action Buttons -->
<div class="pt-5">
<div class="flex justify-end space-x-3">
@@ -72,7 +103,11 @@
</a>
<button type="submit"
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm 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">
{% trans "Save" %}
{% if form.instance.pk %}
{% trans "Update Collection" %}
{% else %}
{% trans "Create Collection" %}
{% endif %}
</button>
</div>
</div>
@@ -81,3 +116,32 @@
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(document).ready(function() {
// Initialize Select2 for tags
$('#id_tags').select2({
theme: 'classic',
width: '100%',
placeholder: '{% trans "Select tags" %}',
allowClear: true,
tags: true, // Allow creating new tags
tokenSeparators: [',', ' '],
createTag: function(params) {
var term = $.trim(params.term);
if (term === '') {
return null;
}
return {
id: term,
text: term,
newTag: true
};
}
});
});
</script>
{% endblock %}
+36 -10
View File
@@ -49,17 +49,17 @@
<!-- Collection Info - Flex Grow to Fill Remaining Space -->
<div class="flex-1 p-3 sm:p-4 flex flex-col">
<h3 class="text-base sm:text-lg font-medium text-gray-900 group-hover:text-blue-600 transition-colors duration-200 line-clamp-1">
{{ collection.name }}
</h3>
<p class="mt-0.5 text-xs sm:text-sm text-gray-500">
{{ collection.images.count }} {% trans "images" %}
</p>
{% if collection.description %}
<p class="mt-1 text-xs sm:text-sm text-gray-600 line-clamp-2 flex-1">
{{ collection.description }}
<div class="flex-1">
<h3 class="text-lg font-semibold text-gray-900 mb-2">{{ collection.name }}</h3>
{% if collection.description %}
<p class="text-sm text-gray-600 mb-4">{{ collection.description }}</p>
{% endif %}
<!-- Image Count -->
<p class="mt-0.5 text-xs sm:text-sm text-gray-500">
{{ collection.images.count }} {% trans "images" %}
</p>
{% endif %}
</div>
</div>
</div>
</a>
@@ -142,6 +142,32 @@ function deleteCollection(collectionId) {
});
}
}
document.addEventListener('DOMContentLoaded', function() {
const colors = [
'bg-blue-50 hover:bg-blue-100 text-blue-700',
'bg-green-50 hover:bg-green-100 text-green-700',
'bg-yellow-50 hover:bg-yellow-100 text-yellow-700',
'bg-red-50 hover:bg-red-100 text-red-700',
'bg-indigo-50 hover:bg-indigo-100 text-indigo-700',
'bg-purple-50 hover:bg-purple-100 text-purple-700',
'bg-pink-50 hover:bg-pink-100 text-pink-700',
'bg-cyan-50 hover:bg-cyan-100 text-cyan-700',
'bg-orange-50 hover:bg-orange-100 text-orange-700',
'bg-teal-50 hover:bg-teal-100 text-teal-700',
'bg-lime-50 hover:bg-lime-100 text-lime-700',
'bg-emerald-50 hover:bg-emerald-100 text-emerald-700',
'bg-sky-50 hover:bg-sky-100 text-sky-700',
'bg-violet-50 hover:bg-violet-100 text-violet-700',
'bg-rose-50 hover:bg-rose-100 text-rose-700',
'bg-amber-50 hover:bg-amber-100 text-amber-700'
];
document.querySelectorAll('.tag-card').forEach(tag => {
const randomColor = colors[Math.floor(Math.random() * colors.length)];
tag.classList.add(...randomColor.split(' '));
});
});
</script>
{% endblock %}
{% endblock %}
+48
View File
@@ -795,6 +795,14 @@ video {
margin-top: 2rem;
}
.-ml-4 {
margin-left: -1rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
.line-clamp-1 {
overflow: hidden;
display: -webkit-box;
@@ -916,6 +924,14 @@ video {
min-height: 100vh;
}
.min-h-0 {
min-height: 0px;
}
.min-h-\[120px\] {
min-height: 120px;
}
.w-12 {
width: 3rem;
}
@@ -1046,6 +1062,10 @@ video {
user-select: all;
}
.resize-y {
resize: vertical;
}
.resize {
resize: both;
}
@@ -1120,6 +1140,10 @@ video {
gap: 1.5rem;
}
.gap-3 {
gap: 0.75rem;
}
.gap-x-4 {
-moz-column-gap: 1rem;
column-gap: 1rem;
@@ -1248,6 +1272,10 @@ video {
border-radius: 0.375rem;
}
.rounded-xl {
border-radius: 0.75rem;
}
.rounded-l-md {
border-top-left-radius: 0.375rem;
border-bottom-left-radius: 0.375rem;
@@ -1514,6 +1542,18 @@ video {
background-color: rgb(245 243 255 / var(--tw-bg-opacity));
}
.bg-black\/50 {
background-color: rgb(0 0 0 / 0.5);
}
.bg-black\/75 {
background-color: rgb(0 0 0 / 0.75);
}
.bg-gray-900\/75 {
background-color: rgb(17 24 39 / 0.75);
}
.bg-opacity-20 {
--tw-bg-opacity: 0.2;
}
@@ -1792,6 +1832,10 @@ video {
line-height: 1.25;
}
.leading-relaxed {
line-height: 1.625;
}
.tracking-normal {
letter-spacing: 0em;
}
@@ -2898,4 +2942,8 @@ video {
.xl\:grid-cols-6 {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.xl\:grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
}