Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
"prettier.tabWidth": 2,
"editor.tabSize": 2
},
"djlint.ignore": "H006"
}
2 changes: 1 addition & 1 deletion src/core/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class Meta:
"review_date": forms.TextInput(
attrs={
"class": "input validator w-full",
"placeholder": _("YYYY, MM-YYYY, or DD-MM-YYYY"),
"placeholder": _("YYYY, MM-YYYY, or YYYY-MM-DD"),
}
),
"cover": CoverImageWidget(attrs={"class": "file-input file-input-ghost w-full max-w-xs"}),
Expand Down
Empty file.
75 changes: 75 additions & 0 deletions src/core/templatetags/media_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Custom template tags for media-related functionality."""

from django import template

register = template.Library()


MEDIA_TYPE_ICONS = {
"BOOK": "book-open",
"GAME": "computer-desktop",
"MUSIC": "musical-note",
"COMIC": "book-open",
"FILM": "film",
"TV": "tv",
"PERF": "ticket",
"BROADCAST": "microphone",
}

SIZE_CLASSES = {
"sm": "w-4 h-4",
"md": "w-6 h-6",
"lg": "w-8 h-8",
}

STATUS_CLASSES = {
"PLANNED": "badge-info",
"IN_PROGRESS": "badge-accent",
"COMPLETED": "badge-success",
"PAUSED": "badge-neutral",
"DNF": "badge-error",
}


@register.inclusion_tag("partials/media-icon.html")
def media_icon(media_type, size="sm"):
"""
Render a heroicon based on media type.

Args:
media_type: The type of media (BOOK, GAME, MUSIC, etc.)
size: Icon size (sm, md, lg) - default 'sm'

Returns:
Context dict with icon_name and size_class

Example usage:
{% load media_tags %}
{% media_icon media.media_type size="md" %}
"""

icon_name = MEDIA_TYPE_ICONS.get(media_type, "question-mark-circle")
size_class = SIZE_CLASSES.get(size, "w-4 h-4")

return {
"icon_name": icon_name,
"size_class": size_class,
}


@register.filter
def status_badge_class(status):
"""
Return the appropriate DaisyUI badge class for a given status.

Args:
status: The media status (PLANNED, IN_PROGRESS, etc.)

Returns:
String with DaisyUI badge class name

Example usage:
<span class="badge {{ media.status|status_badge_class }}">
"""

return STATUS_CLASSES.get(status, "badge-ghost")
2 changes: 0 additions & 2 deletions src/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
path("media/validate_field/", validate_media_field, name="media_validate_field"),
path("media/<int:pk>/review-full/", views.media_review_full_htmx, name="media_review_full_htmx"),
path("media/<int:pk>/review-clamped/", views.media_review_clamped_htmx, name="media_review_clamped_htmx"),
path("media/<int:pk>/update-score/", views.media_update_score_htmx, name="media_update_score_htmx"),
path("media/<int:pk>/update-status/", views.media_update_status_htmx, name="media_update_status_htmx"),
# Backup management
path("backup/", views.backup_manage, name="backup_manage"),
path("backup/export/", views.backup_export, name="backup_export"),
Expand Down
48 changes: 0 additions & 48 deletions src/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,54 +280,6 @@ def media_review_full_htmx(request, pk):
return render(request, "partials/media-review-full.html", {"media": media})


@login_required
def media_update_score_htmx(request, pk):
"""HTMX view: update media score and return updated score widget."""
media = get_object_or_404(Media, pk=pk)

if request.method == "POST":
score_value = request.POST.get("score", "").strip()

# Handle empty score (clear)
if score_value == "":
media.score = None
media.save()
else:
try:
score = int(score_value)
# Validate that score is one of the valid choices
valid_scores = dict(Media.score.field.choices).keys()
if score in valid_scores:
media.score = score
media.save()
except ValueError:
# Invalid score format, don't update
pass

return render(request, "partials/score-editable.html", {"media": media})


@login_required
def media_update_status_htmx(request, pk):
"""HTMX view: update media status and return updated status widget."""
media = get_object_or_404(Media, pk=pk)

if request.method == "POST":
status_value = request.POST.get("status", "").strip()

# Validate that status is one of the valid choices
valid_statuses = dict(Media.status.field.choices).keys()
if status_value in valid_statuses:
media.status = status_value
media.save()

context = {
"media": media,
"status_choices": Media.status.field.choices,
}
return render(request, "partials/status-editable.html", context)


@login_required
def backup_export(request):
"""Export backup and download it."""
Expand Down
1 change: 0 additions & 1 deletion src/templates/backup_manage.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{% extends "base.html" %}
{% load static %}
{% load heroicons %}
{% load i18n %}
{% block title %}
{% translate "Backup Management" %} - Datakult
Expand Down
14 changes: 14 additions & 0 deletions src/templates/partials/media-contributors.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{# Renders the list of contributors with HTMX links #}
{# Parameters: media #}
{% if media.contributors.all %}
{% for contributor in media.contributors.all %}
<a href="#"
class="link link-hover contributor-link"
data-contributor-id="{{ contributor.id }}"
data-contributor-name="{{ contributor.name }}"
hx-get="{% url 'home' %}?contributor={{ contributor.id }}"
hx-target="#media-list"
hx-include="#view-mode-input, #sort, #type, #status, #score, #review-from, #review-to"
hx-push-url="true">{{ contributor.name }}</a>{% if not forloop.last %};{% endif %}
{% endfor %}
{% endif %}
16 changes: 16 additions & 0 deletions src/templates/partials/media-cover.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{% load i18n %}
{% load media_tags %}
<div class="relative w-24 h-32 bg-base-300 rounded">
{% if media.cover %}
<img src="{{ media.cover.url }}"
alt="{% translate "Cover of" %} {{ media.title }}"
class="w-full h-full object-contain rounded">
{% else %}
<div class="w-full h-full flex items-center justify-center text-base-content/30">
{% heroicon_outline "photo" class="w-12 h-12" %}
</div>
{% endif %}
<div class="absolute top-1 left-1">
<span class="badge badge-neutral badge-sm" aria-label="{{ media.get_media_type_display }}">{% media_icon media.media_type size="sm" %}</span>
</div>
</div>
7 changes: 7 additions & 0 deletions src/templates/partials/media-edit-button.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% load i18n %}
{# Renders the edit button for a media #}
{# Parameters: media #}
<a href="{% url 'media_edit' media.id %}"
class="btn btn-outline btn-sm btn-primary tooltip"
data-tip="{% translate "Edit" %}"
aria-label="{% translate "Edit" %} {{ media.title }}">{% heroicon_outline "pencil-square" %}</a>
1 change: 1 addition & 0 deletions src/templates/partials/media-icon.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% heroicon_outline icon_name class=size_class %}
127 changes: 54 additions & 73 deletions src/templates/partials/media-items.html
Original file line number Diff line number Diff line change
@@ -1,104 +1,85 @@
{% load i18n %}
{% load media_tags %}
{# This partial renders only the media items without the container #}
{% if view_mode == 'grid' %}
{# Grid items #}
{# Grid view - Cards #}
{% for media in media_list %}
<div class="card lg:card-side bg-base-200 shadow-md hover:shadow-lg transition-shadow">
<figure class="basis-1/4 shrink-0 grow-0 h-full bg-base-300">
<div class="card bg-base-200 shadow-sm">
<figure class="relative w-full h-64 bg-base-300">
{% if media.cover %}
<img src="{{ media.cover.url }}"
alt="{{ media.title }}"
width="200"
height="300"
class="w-full h-full object-cover">
alt="{% translate "Cover of" %} {{ media.title }}"
class="w-full h-full object-contain">
{% else %}
<div class="w-full h-full flex items-center justify-center text-base-content/30">
{% heroicon_outline "photo" class="w-16 h-16" %}
{% heroicon_outline "photo" class="w-24 h-24" %}
Comment thread
PascalRepond marked this conversation as resolved.
</div>
{% endif %}
<div class="absolute top-2 left-2">
<span class="badge badge-neutral badge-lg"
aria-label="{{ media.get_media_type_display }}">{% media_icon media.media_type size="md" %}</span>
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{% if media.score %}
<div class="absolute top-2 right-2">{% include "partials/media-score-badge.html" %}</div>
{% endif %}
</figure>
<div class="card-body basis-3/4 p-3 h-full">
<h3 class="card-title text-sm line-clamp-2">
<a href="{% url 'media_edit' media.id %}" class="link link-hover">{{ media.title }}</a>
</h3>
<div class="text-xs space-y-1">
{% for contributor in media.contributors.all %}
<a href="#"
class="link link-secondary contributor-link"
data-contributor-id="{{ contributor.id }}"
data-contributor-name="{{ contributor.name }}"
hx-get="{% url 'home' %}?contributor={{ contributor.id }}"
hx-target="#media-list"
hx-include="#view-mode-input, #sort, #type, #status, #score, #review-from, #review-to"
hx-push-url="true">{{ contributor.name }}</a>
{% if not forloop.last %};{% endif %}
{% endfor %}
<div class="flex items-center gap-2 mt-1">
<span class="badge badge-sm">{{ media.get_media_type_display }}</span>
{% include "partials/status-editable.html" with status_choices=status_choices %}
<div class="card-body p-3">
<div class="flex justify-between gap-2">
<div>
<h3 class="card-title inline">{{ media.title }}</h3>
{% if media.pub_year %}<span class="opacity-70">({{ media.pub_year }})</span>{% endif %}
<div>{% include "partials/media-contributors.html" %}</div>
</div>
{% include "partials/score-editable.html" %}
{% if media.review %}
<div id="review-cell-{{ media.id }}" class="text-sm review-text mt-2">
{% include "partials/media-review-clamped.html" %}
</div>
{% endif %}
{% if media.review_date %}
<div class="text-xs opacity-70 mt-1">{{ media.review_date }}</div>
{% endif %}
{% include "partials/media-edit-button.html" %}
</div>
{% include "partials/media-status-badge.html" %}
{% if media.review %}
<div id="review-cell-{{ media.id }}" class="text-sm review-text">
{% include "partials/media-review-clamped.html" %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
{# Table rows #}
{# List view - Table rows #}
{% for media in media_list %}
<tr>
<td>
{% if media.cover %}
<img src="{{ media.cover.url }}"
alt="{% translate "Cover" %}"
width="64"
height="96"
class="w-16 h-24 object-cover">
{% else %}
<div class="w-16 h-24 bg-gray-200 flex items-center justify-center text-xs">No cover</div>
{% endif %}
</td>
<td>
<a href="{% url 'media_edit' media.id %}" class="link link-secondary">{{ media.title }}</a>
</td>
<td class="max-w-xs">
{% for contributor in media.contributors.all %}
<a href="#"
class="link link-secondary contributor-link"
data-contributor-id="{{ contributor.id }}"
data-contributor-name="{{ contributor.name }}"
hx-get="{% url 'home' %}?contributor={{ contributor.id }}"
hx-target="#media-list"
hx-include="#view-mode-input, #sort, #type, #status, #score, #review-from, #review-to"
hx-push-url="true">{{ contributor.name }}</a>
{% if not forloop.last %};{% endif %}
{% endfor %}
</td>
<td>{{ media.get_media_type_display }}</td>
<td>
{% include "partials/status-editable.html" with status_choices=status_choices %}
{# Cover with media type badge #}
<td class="align-top p-2">{% include "partials/media-cover.html" %}</td>
{# Title, contributors #}
<td class="align-top p-2">
<div class="flex flex-col gap-1">
<h3 class="font-bold text-base">
{{ media.title }}
{% if media.pub_year %}<span class="opacity-70 text-sm font-normal">({{ media.pub_year }})</span>{% endif %}
</h3>
{% if media.contributors.all %}
<div class="text-sm opacity-70">{% include "partials/media-contributors.html" %}</div>
{% endif %}
</div>
</td>
<td>{{ media.pub_year | default:"" }}</td>
<td class="max-w-md">
{% include "partials/score-editable.html" %}
{# Review #}
<td class="align-top p-2 hidden md:table-cell">
{% include "partials/media-score-badge.html" with size="sm" extra_class="mb-2" %}
{% if media.review %}
<div id="review-cell-{{ media.id }}" class="text-sm review-text mt-2">
<div id="review-cell-{{ media.id }}" class="text-sm max-w-200 review-text">
Comment thread
PascalRepond marked this conversation as resolved.
{% include "partials/media-review-clamped.html" %}
</div>
{% endif %}
</td>
<td>
{# Status #}
<td class="align-top p-2 hidden sm:table-cell">{% include "partials/media-status-badge.html" %}</td>
{# Review date #}
<td class="align-top p-2 hidden lg:table-cell">
{% if media.review_date %}
<div class="text-xs opacity-70">{{ media.review_date }}</div>
<span class="text-sm">{{ media.review_date }}</span>
{% else %}
<span class="text-sm opacity-50">—</span>
{% endif %}
</td>
{# Actions #}
<td class="align-top p-2">{% include "partials/media-edit-button.html" %}</td>
</tr>
{% endfor %}
{% endif %}
18 changes: 8 additions & 10 deletions src/templates/partials/media-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,20 @@
{% if page_obj %}
{% if view_mode == 'grid' %}
{# Grid view - Cards layout #}
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mt-4"
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 mt-4"
id="media-container">{% include "partials/media-items-page.html" %}</div>
{% else %}
{# List view - Table layout #}
<div class="overflow-x-auto">
<div class="overflow-x-auto mt-4">
<table class="table table-zebra" id="media-container">
<thead>
<tr>
<th>{% translate "Cover" %}</th>
<th>{% translate "Title" %}</th>
<th>{% translate "Contributor" %}</th>
<th>{% translate "Type" %}</th>
<th>{% translate "Status" %}</th>
<th>{% translate "Year" %}</th>
<th>{% translate "Review" %}</th>
<th>{% translate "Reviewed on" %}</th>
<th class="w-32">{% translate "Cover" %}</th>
<th class="w-64 lg:w-80">{% translate "Title" %}</th>
<th class="hidden md:table-cell">{% translate "Review" %}</th>
<th class="hidden sm:table-cell w-32">{% translate "Status" %}</th>
<th class="hidden lg:table-cell w-32">{% translate "Review date" %}</th>
<th class="w-32">{% translate "Actions" %}</th>
</tr>
</thead>
<tbody>
Expand Down
Loading