mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
Add tags to image collection
This commit is contained in:
Binary file not shown.
+2
-1
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Vendored
+48
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user