Update tags feature

This commit is contained in:
2025-03-02 09:49:05 +11:00
parent 5e0e48ed1a
commit 7781165e69
3 changed files with 302 additions and 67 deletions
+33
View File
@@ -4,12 +4,32 @@ from django.contrib import messages
from django.shortcuts import get_object_or_404
from .models import Tag
from .forms import TagForm
from django.db.models import Count
class TagListView(ListView):
model = Tag
context_object_name = 'tags'
template_name = 'links/tag_list.html'
paginate_by = 20
def get_queryset(self):
queryset = super().get_queryset()
# Annotate each tag with its usage count
queryset = queryset.annotate(
links_count=Count('links', distinct=True),
pages_count=Count('pages', distinct=True),
posts_count=Count('posts', distinct=True),
image_collections_count=Count('image_collections', distinct=True),
)
# Add a total_count attribute to each tag
for tag in queryset:
tag.total_count = (
tag.links_count +
tag.pages_count +
tag.posts_count +
tag.image_collections_count
)
return queryset
class TagDetailView(DetailView):
model = Tag
@@ -19,7 +39,20 @@ class TagDetailView(DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Get all resource types that have this tag
context['links'] = self.object.links.all()
context['pages'] = self.object.pages.all()
context['posts'] = self.object.posts.all()
context['image_collections'] = self.object.image_collections.all()
# Calculate total usage count
total_count = (
context['links'].count() +
context['pages'].count() +
context['posts'].count() +
context['image_collections'].count()
)
context['total_usage_count'] = total_count
return context
class TagCreateView(CreateView):
+203 -35
View File
@@ -3,48 +3,216 @@
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="bg-white shadow rounded-lg p-6">
<div class="flex justify-between items-start mb-6">
<div>
<h1 class="text-2xl font-bold mb-2">{{ tag.name }}</h1>
{% if tag.description %}
<p class="text-gray-600">{{ tag.description }}</p>
{% endif %}
</div>
<div class="flex space-x-2">
<a href="{% url 'tag-update' tag.slug %}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
{% trans "Edit" %}
</a>
<a href="{% url 'tag-delete' tag.slug %}" class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">
{% trans "Delete" %}
</a>
<div class="bg-white shadow-lg rounded-lg overflow-hidden">
<!-- Tag Header -->
<div class="bg-gradient-to-r from-blue-500 to-indigo-600 p-6 text-white">
<div class="flex justify-between items-start">
<div>
<h1 class="text-3xl font-bold mb-2">{{ tag.name }}</h1>
{% if tag.description %}
<p class="text-white text-opacity-90 mb-2">{{ tag.description }}</p>
{% endif %}
<div class="inline-flex items-center bg-white bg-opacity-20 px-3 py-1 rounded-full text-sm">
<span class="font-medium">{% trans "Used" %}: {{ total_usage_count }} {% trans "times" %}</span>
</div>
</div>
<div class="flex space-x-2">
<a href="{% url 'tag-update' tag.slug %}" class="bg-white text-blue-600 hover:bg-blue-50 font-medium py-2 px-4 rounded-lg transition duration-150 ease-in-out">
{% trans "Edit" %}
</a>
<a href="{% url 'tag-delete' tag.slug %}" class="bg-red-500 hover:bg-red-600 text-white font-medium py-2 px-4 rounded-lg transition duration-150 ease-in-out">
{% trans "Delete" %}
</a>
</div>
</div>
</div>
<div class="mt-8">
<h2 class="text-xl font-semibold mb-4">{% trans "Links with this tag" %}</h2>
{% if links %}
<div class="space-y-4">
{% for link in links %}
<div class="border rounded-lg p-4">
<h3 class="font-medium">
<a href="{% url 'link_detail' pk=link.pk %}" class="text-blue-600 hover:text-blue-800">
{{ link.alias }}
</a>
</h3>
{% if link.description %}
<p class="text-gray-600 mt-1">{{ link.description }}</p>
{% endif %}
<div class="mt-2 text-sm text-gray-500">
{% trans "Created" %}: {{ link.created_at|date:"Y-m-d" }}
<!-- Resource Tabs -->
<div class="border-b border-gray-200">
<nav class="flex -mb-px overflow-x-auto" aria-label="Tabs">
<button class="resource-tab text-blue-600 border-blue-500 whitespace-nowrap py-4 px-6 border-b-2 font-medium text-sm" data-target="links-section">
{% trans "Links" %} ({{ links|length }})
</button>
<button class="resource-tab text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-6 border-b-2 border-transparent font-medium text-sm" data-target="posts-section">
{% trans "Posts" %} ({{ posts|length }})
</button>
<button class="resource-tab text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-6 border-b-2 border-transparent font-medium text-sm" data-target="pages-section">
{% trans "Pages" %} ({{ pages|length }})
</button>
<button class="resource-tab text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-6 border-b-2 border-transparent font-medium text-sm" data-target="collections-section">
{% trans "Image Collections" %} ({{ image_collections|length }})
</button>
</nav>
</div>
<!-- Resource Sections -->
<div class="p-6">
<!-- Links Section -->
<div id="links-section" class="resource-section">
<h2 class="text-xl font-semibold mb-4 flex items-center">
{% trans "Links with this tag" %}
<span class="ml-2 bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full">{{ links|length }}</span>
</h2>
{% if links %}
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{% for link in links %}
<div class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition duration-150 ease-in-out">
<h3 class="font-medium text-lg">
<a href="{% url 'link_detail' pk=link.pk %}" class="text-blue-600 hover:text-blue-800">
{{ link.alias }}
</a>
</h3>
{% if link.description %}
<p class="text-gray-600 mt-1 line-clamp-2">{{ link.description }}</p>
{% endif %}
<div class="mt-2 text-sm text-gray-500 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
{{ link.created_at|date:"Y-m-d" }}
</div>
</div>
{% endfor %}
</div>
{% endfor %}
{% else %}
<div class="bg-gray-50 rounded-lg p-6 text-center">
<p class="text-gray-500">{% trans "No links found with this tag." %}</p>
</div>
{% endif %}
</div>
<!-- Posts Section -->
<div id="posts-section" class="resource-section hidden">
<h2 class="text-xl font-semibold mb-4 flex items-center">
{% trans "Posts with this tag" %}
<span class="ml-2 bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">{{ posts|length }}</span>
</h2>
{% if posts %}
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{% for post in posts %}
<div class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition duration-150 ease-in-out">
<h3 class="font-medium text-lg">
<a href="{% url 'post-detail' pk=post.pk %}" class="text-blue-600 hover:text-blue-800">
{{ post.title }}
</a>
</h3>
{% if post.summary %}
<p class="text-gray-600 mt-1 line-clamp-2">{{ post.summary }}</p>
{% endif %}
<div class="mt-2 text-sm text-gray-500 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
{{ post.created_at|date:"Y-m-d" }}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="bg-gray-50 rounded-lg p-6 text-center">
<p class="text-gray-500">{% trans "No posts found with this tag." %}</p>
</div>
{% endif %}
</div>
<!-- Pages Section -->
<div id="pages-section" class="resource-section hidden">
<h2 class="text-xl font-semibold mb-4 flex items-center">
{% trans "Pages with this tag" %}
<span class="ml-2 bg-purple-100 text-purple-800 text-xs font-medium px-2.5 py-0.5 rounded-full">{{ pages|length }}</span>
</h2>
{% if pages %}
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{% for page in pages %}
<div class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition duration-150 ease-in-out">
<h3 class="font-medium text-lg">
<a href="{% url 'page-detail' pk=page.pk %}" class="text-blue-600 hover:text-blue-800">
{{ page.title|default:page.url }}
</a>
</h3>
{% if page.summary %}
<p class="text-gray-600 mt-1 line-clamp-2">{{ page.summary }}</p>
{% endif %}
<div class="mt-2 text-sm text-gray-500 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
{{ page.created_at|date:"Y-m-d" }}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="bg-gray-50 rounded-lg p-6 text-center">
<p class="text-gray-500">{% trans "No pages found with this tag." %}</p>
</div>
{% endif %}
</div>
<!-- Image Collections Section -->
<div id="collections-section" class="resource-section hidden">
<h2 class="text-xl font-semibold mb-4 flex items-center">
{% trans "Image Collections with this tag" %}
<span class="ml-2 bg-yellow-100 text-yellow-800 text-xs font-medium px-2.5 py-0.5 rounded-full">{{ image_collections|length }}</span>
</h2>
{% if image_collections %}
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{% for collection in image_collections %}
<div class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition duration-150 ease-in-out">
<h3 class="font-medium text-lg">
<a href="{% url 'collection-detail' pk=collection.pk %}" class="text-blue-600 hover:text-blue-800">
{{ collection.name }}
</a>
</h3>
{% if collection.description %}
<p class="text-gray-600 mt-1 line-clamp-2">{{ collection.description }}</p>
{% endif %}
<div class="mt-2 text-sm text-gray-500 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
{{ collection.created_at|date:"Y-m-d" }}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="bg-gray-50 rounded-lg p-6 text-center">
<p class="text-gray-500">{% trans "No image collections found with this tag." %}</p>
</div>
{% endif %}
</div>
{% else %}
<p class="text-gray-500 text-center py-4">{% trans "No links found with this tag." %}</p>
{% endif %}
</div>
</div>
</div>
<!-- JavaScript for tab functionality -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const tabs = document.querySelectorAll('.resource-tab');
const sections = document.querySelectorAll('.resource-section');
tabs.forEach(tab => {
tab.addEventListener('click', function() {
// Hide all sections
sections.forEach(section => {
section.classList.add('hidden');
});
// Show the selected section
const targetId = this.getAttribute('data-target');
document.getElementById(targetId).classList.remove('hidden');
// Update tab styles
tabs.forEach(t => {
t.classList.remove('text-blue-600', 'border-blue-500');
t.classList.add('text-gray-500', 'border-transparent');
});
this.classList.remove('text-gray-500', 'border-transparent');
this.classList.add('text-blue-600', 'border-blue-500');
});
});
});
</script>
{% endblock %}
+66 -32
View File
@@ -10,23 +10,43 @@
</a>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-6">
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{% for tag in tags %}
<a href="{% url 'tag-detail' tag.slug %}" class="block group">
<div class="bg-white shadow rounded-lg p-6 tag-card h-full transition-shadow duration-200 hover:shadow-lg">
<h2 class="text-xl font-semibold mb-2 text-gray-900 group-hover:text-gray-700">
{{ tag.name }}
</h2>
{% if tag.description %}
<p class="text-gray-600 mb-4 line-clamp-2">{{ tag.description }}</p>
{% endif %}
<div class="text-sm text-gray-500">
{% with link_count=tag.links.count %}
{{ link_count }} {% if link_count == 1 %}{% trans "link" %}{% else %}{% trans "links" %}{% endif %}
<div class="tag-card rounded-lg p-4 h-full flex flex-col items-center justify-center text-center shadow-md hover:shadow-lg transition-all duration-200">
<div class="mb-2">
{% with icon_classes="w-8 h-8 mx-auto" %}
{% if forloop.counter|divisibleby:5 %}
<svg class="{{ icon_classes }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
</svg>
{% elif forloop.counter|divisibleby:4 %}
<svg class="{{ icon_classes }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
{% elif forloop.counter|divisibleby:3 %}
<svg class="{{ icon_classes }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
{% elif forloop.counter|divisibleby:2 %}
<svg class="{{ icon_classes }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2" />
</svg>
{% else %}
<svg class="{{ icon_classes }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
</svg>
{% endif %}
{% endwith %}
</div>
<h2 class="text-lg font-semibold mb-1">{{ tag.name }}</h2>
<div class="mt-1">
<span class="inline-flex items-center justify-center bg-white bg-opacity-30 text-xs font-medium px-2 py-1 rounded-full">
{{ tag.total_count }} {% trans "uses" %}
</span>
</div>
</div>
<!-- </a> -->
</a>
{% empty %}
<div class="col-span-full text-center py-8 text-gray-500">
{% trans "No tags found." %}
@@ -55,28 +75,42 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
const colors = [
'bg-blue-50 hover:bg-blue-100',
'bg-green-50 hover:bg-green-100',
'bg-yellow-50 hover:bg-yellow-100',
'bg-red-50 hover:bg-red-100',
'bg-indigo-50 hover:bg-indigo-100',
'bg-purple-50 hover:bg-purple-100',
'bg-pink-50 hover:bg-pink-100',
'bg-cyan-50 hover:bg-cyan-100',
'bg-orange-50 hover:bg-orange-100',
'bg-teal-50 hover:bg-teal-100',
'bg-lime-50 hover:bg-lime-100',
'bg-emerald-50 hover:bg-emerald-100',
'bg-sky-50 hover:bg-sky-100',
'bg-violet-50 hover:bg-violet-100',
'bg-rose-50 hover:bg-rose-100',
'bg-amber-50 hover:bg-amber-100'
// Debug message
console.log('Script is running');
// Define colors for tags
const tagColors = [
'#3b82f6', // blue
'#10b981', // green
'#f59e0b', // yellow
'#ef4444', // red
'#6366f1', // indigo
'#8b5cf6', // purple
'#ec4899', // pink
'#06b6d4', // cyan
'#f97316', // orange
'#14b8a6', // teal
'#84cc16', // lime
'#10b981', // emerald
'#0ea5e9', // sky
'#8b5cf6', // violet
'#f43f5e', // rose
'#f59e0b' // amber
];
document.querySelectorAll('.tag-card').forEach(card => {
const randomColor = colors[Math.floor(Math.random() * colors.length)];
card.classList.add(...randomColor.split(' '));
// Get all tag cards
const tagCards = document.querySelectorAll('.tag-card');
console.log('Found tag cards:', tagCards.length);
// Apply random colors to each tag card
tagCards.forEach((card, index) => {
// Add a random color
const randomColor = tagColors[Math.floor(Math.random() * tagColors.length)];
card.style.backgroundColor = randomColor;
card.style.color = 'white';
// Log for debugging
console.log('Applied color:', randomColor, 'to card index:', index);
});
});
</script>