Various small bug fixes, break out pagination for cards into its own mixin and templatetag

This commit is contained in:
badblocks 2025-03-29 00:27:40 -07:00
parent 05a279fa3a
commit 138a929da6
17 changed files with 225 additions and 136 deletions

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-03-28 04:43 # Generated by Django 5.1.2 on 2025-03-29 03:33
import accounts.models import accounts.models
import django.contrib.auth.models import django.contrib.auth.models

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-03-28 04:43 # Generated by Django 5.1.2 on 2025-03-29 03:33
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

42
cards/mixins.py Normal file
View file

@ -0,0 +1,42 @@
from math import ceil
class ReusablePaginationMixin:
"""
A mixin that encapsulates reusable pagination logic.
Use in Django ListViews to generate custom pagination context.
"""
per_page = 10 # Default; can be overridden in your view.
def paginate_data(self, data_list, page_number):
"""
Paginate a list of items.
Arguments:
data_list (list): The list of items to paginate.
page_number (int): Current page number.
Returns:
tuple: (paginated_items, pagination_context)
"""
total_items = len(data_list)
num_pages = ceil(total_items / self.per_page) if self.per_page > 0 else 1
# Ensure page_number is within valid bounds.
if page_number < 1:
page_number = 1
elif page_number > num_pages:
page_number = num_pages
start = (page_number - 1) * self.per_page
end = page_number * self.per_page
items = data_list[start:end]
pagination_context = {
"number": page_number,
"has_previous": page_number > 1,
"has_next": page_number < num_pages,
"previous_page": page_number - 1 if page_number > 1 else 1,
"next_page": page_number + 1 if page_number < num_pages else num_pages,
"paginator": {"num_pages": num_pages},
}
return items, pagination_context

View file

@ -0,0 +1,11 @@
from django import template
register = template.Library()
@register.inclusion_tag("templatetags/pagination_controls.html", takes_context=True)
def render_pagination(context, page_obj):
"""
Renders pagination controls given a page_obj.
The controls use values like page_obj.number, page_obj.has_previous, etc.
"""
return {"page_obj": page_obj}

View file

@ -3,6 +3,7 @@ from django.urls import reverse_lazy
from django.views.generic import UpdateView, DeleteView, CreateView, ListView, DetailView from django.views.generic import UpdateView, DeleteView, CreateView, ListView, DetailView
from cards.models import Card from cards.models import Card
from trades.models import TradeOffer from trades.models import TradeOffer
from cards.mixins import ReusablePaginationMixin
class CardDetailView(DetailView): class CardDetailView(DetailView):
model = Card model = Card
@ -62,9 +63,9 @@ class TradeOfferWantCardListView(ListView):
context['side'] = 'want' context['side'] = 'want'
return context return context
class CardListView(ListView): class CardListView(ReusablePaginationMixin, ListView):
model = Card model = Card
paginate_by = 100 # For non-grouped mode; grouping mode will override default pagination. paginate_by = 36 # For non-grouped mode; grouping mode will override default pagination.
context_object_name = "cards" context_object_name = "cards"
def get_template_names(self): def get_template_names(self):
@ -102,13 +103,11 @@ class CardListView(ListView):
context["group_by"] = group_by context["group_by"] = group_by
if group_by in ("deck", "cardset", "rarity"): if group_by in ("deck", "cardset", "rarity"):
# Fetch the complete queryset (no slicing)
full_qs = self.get_queryset() full_qs = self.get_queryset()
all_cards = list(full_qs) all_cards = list(full_qs)
flat_cards = [] flat_cards = []
if group_by == "deck": if group_by == "deck":
# Each card may belong to multiple decks reproduce the existing logic.
for card in all_cards: for card in all_cards:
for deck in card.decks.all(): for deck in card.decks.all():
flat_cards.append({"group": deck.name, "card": card}) flat_cards.append({"group": deck.name, "card": card})
@ -119,23 +118,19 @@ class CardListView(ListView):
flat_cards.sort(key=lambda x: x["group"].lower()) flat_cards.sort(key=lambda x: x["group"].lower())
elif group_by == "rarity": elif group_by == "rarity":
for card in all_cards: for card in all_cards:
flat_cards.append({"group": card.rarity_level, "card": card}) flat_cards.append({"group": card.rarity_icon, "sort_group": card.rarity_level, "card": card})
flat_cards.sort(key=lambda x: x["group"], reverse=True) flat_cards.sort(key=lambda x: x["sort_group"], reverse=True)
total_cards = len(flat_cards)
try: try:
page_number = int(self.request.GET.get("page", 1)) page_number = int(self.request.GET.get("page", 1))
if page_number < 1:
page_number = 1
except ValueError: except ValueError:
page_number = 1 page_number = 1
per_page = 96 # Use our custom mixin logic here
start = (page_number - 1) * per_page self.per_page = 36
end = page_number * per_page page_flat_cards, pagination_context = self.paginate_data(flat_cards, page_number)
page_flat_cards = flat_cards[start:end]
# Reassemble the flat list into grouped structure for just this page. # Reassemble the flat list into groups for the current page.
page_groups = [] page_groups = []
for item in page_flat_cards: for item in page_flat_cards:
group_value = item["group"] group_value = item["group"]
@ -145,25 +140,22 @@ class CardListView(ListView):
else: else:
page_groups.append({"group": group_value, "cards": [card_obj]}) page_groups.append({"group": group_value, "cards": [card_obj]})
context["groups"] = page_groups context["groups"] = page_groups
context["page_obj"] = pagination_context
# Set up custom pagination context. context["total_cards"] = len(flat_cards)
from math import ceil
num_pages = ceil(total_cards / per_page)
page_obj = {
"number": page_number,
"has_previous": page_number > 1,
"has_next": page_number < num_pages,
"previous_page_number": page_number - 1 if page_number > 1 else None,
"next_page_number": page_number + 1 if page_number < num_pages else None,
"paginator": {
"num_pages": num_pages,
},
}
context["page_obj"] = page_obj
context["is_paginated"] = total_cards > per_page
context["total_cards"] = total_cards
# Optionally, keep the full queryset in object_list.
context["object_list"] = full_qs context["object_list"] = full_qs
return context return context
else:
return context # For non-grouped mode, transform the built-in paginator page
if "page_obj" in context:
page = context["page_obj"]
# Create a unified pagination context dict
custom_page_obj = {
"number": page.number,
"has_previous": page.has_previous(),
"has_next": page.has_next(),
"previous_page": page.previous_page_number() if page.has_previous() else 1,
"next_page": page.next_page_number() if page.has_next() else page.paginator.num_pages,
"paginator": {"num_pages": page.paginator.num_pages},
}
context["page_obj"] = custom_page_obj
return context

View file

@ -2,6 +2,7 @@ from collections import defaultdict, OrderedDict
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.db.models import Count, Q, Prefetch, Sum, F, IntegerField, Value, BooleanField, Case, When from django.db.models import Count, Q, Prefetch, Sum, F, IntegerField, Value, BooleanField, Case, When
from django.db.models.functions import Coalesce
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from trades.models import TradeOffer, TradeAcceptance, TradeOfferHaveCard, TradeOfferWantCard from trades.models import TradeOffer, TradeAcceptance, TradeOfferHaveCard, TradeOfferWantCard
from cards.models import Card from cards.models import Card
@ -61,7 +62,9 @@ class HomePageView(TemplateView):
# Least Offered Cards # Least Offered Cards
try: try:
context["least_offered_cards"] = ( context["least_offered_cards"] = (
Card.objects.annotate(offer_count=Sum("tradeofferhavecard__quantity")) Card.objects.annotate(
offer_count=Coalesce(Sum("tradeofferhavecard__quantity"), 0)
)
.order_by("offer_count")[:6] .order_by("offer_count")[:6]
) )
except Exception as e: except Exception as e:

View file

@ -4463,7 +4463,7 @@
"decks": [ "decks": [
3 3
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -4479,7 +4479,7 @@
"decks": [ "decks": [
2 2
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -4495,7 +4495,7 @@
"decks": [ "decks": [
4 4
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -4511,7 +4511,7 @@
"decks": [ "decks": [
2 2
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -4527,7 +4527,7 @@
"decks": [ "decks": [
3 3
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -4543,7 +4543,7 @@
"decks": [ "decks": [
4 4
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -4559,7 +4559,7 @@
"decks": [ "decks": [
3 3
], ],
"rarity_icon": "🌟🌟🌟", "rarity_icon": "⭐️⭐️⭐️",
"rarity_level": 7, "rarity_level": 7,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -4575,7 +4575,7 @@
"decks": [ "decks": [
4 4
], ],
"rarity_icon": "🌟🌟🌟", "rarity_icon": "⭐️⭐️⭐️",
"rarity_level": 7, "rarity_level": 7,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -4591,7 +4591,7 @@
"decks": [ "decks": [
2 2
], ],
"rarity_icon": "🌟🌟🌟", "rarity_icon": "⭐️⭐️⭐️",
"rarity_level": 7, "rarity_level": 7,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -4609,7 +4609,7 @@
3, 3,
4 4
], ],
"rarity_icon": "🌟🌟🌟", "rarity_icon": "⭐️⭐️⭐️",
"rarity_level": 7, "rarity_level": 7,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -5991,7 +5991,7 @@
"decks": [ "decks": [
5 5
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -6007,7 +6007,7 @@
"decks": [ "decks": [
5 5
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -6023,7 +6023,7 @@
"decks": [ "decks": [
5 5
], ],
"rarity_icon": "🌟🌟🌟", "rarity_icon": "⭐️⭐️⭐️",
"rarity_level": 7, "rarity_level": 7,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -9218,7 +9218,7 @@
"decks": [ "decks": [
6 6
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -9234,7 +9234,7 @@
"decks": [ "decks": [
7 7
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -9250,7 +9250,7 @@
"decks": [ "decks": [
6 6
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -9266,7 +9266,7 @@
"decks": [ "decks": [
7 7
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -9282,7 +9282,7 @@
"decks": [ "decks": [
6 6
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -9298,7 +9298,7 @@
"decks": [ "decks": [
7 7
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -9314,7 +9314,7 @@
"decks": [ "decks": [
6 6
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -9330,7 +9330,7 @@
"decks": [ "decks": [
7 7
], ],
"rarity_icon": "🌟🌟", "rarity_icon": "⭐️⭐️",
"rarity_level": 6, "rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -9346,7 +9346,7 @@
"decks": [ "decks": [
7 7
], ],
"rarity_icon": "🌟🌟🌟", "rarity_icon": "⭐️⭐️⭐️",
"rarity_level": 7, "rarity_level": 7,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -9362,7 +9362,7 @@
"decks": [ "decks": [
6 6
], ],
"rarity_icon": "🌟🌟🌟", "rarity_icon": "⭐️⭐️⭐️",
"rarity_level": 7, "rarity_level": 7,
"created_at": "2025-02-17T02:44:18.706Z", "created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z" "updated_at": "2025-02-17T02:44:18.706Z"
@ -12484,7 +12484,7 @@
"decks": [ "decks": [
9 9
], ],
"rarity_level": 6, "rarity_level": 5,
"rarity_icon": "✨", "rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z", "created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z" "updated_at": "2025-03-26T12:25:17.706Z"
@ -12500,7 +12500,7 @@
"decks": [ "decks": [
9 9
], ],
"rarity_level": 6, "rarity_level": 5,
"rarity_icon": "✨", "rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z", "created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z" "updated_at": "2025-03-26T12:25:17.706Z"
@ -12516,7 +12516,7 @@
"decks": [ "decks": [
9 9
], ],
"rarity_level": 6, "rarity_level": 5,
"rarity_icon": "✨", "rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z", "created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z" "updated_at": "2025-03-26T12:25:17.706Z"
@ -12532,7 +12532,7 @@
"decks": [ "decks": [
9 9
], ],
"rarity_level": 6, "rarity_level": 5,
"rarity_icon": "✨", "rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z", "created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z" "updated_at": "2025-03-26T12:25:17.706Z"
@ -12548,7 +12548,7 @@
"decks": [ "decks": [
9 9
], ],
"rarity_level": 6, "rarity_level": 5,
"rarity_icon": "✨", "rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z", "created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z" "updated_at": "2025-03-26T12:25:17.706Z"
@ -12564,7 +12564,7 @@
"decks": [ "decks": [
9 9
], ],
"rarity_level": 6, "rarity_level": 5,
"rarity_icon": "✨", "rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z", "created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z" "updated_at": "2025-03-26T12:25:17.706Z"
@ -12580,7 +12580,7 @@
"decks": [ "decks": [
9 9
], ],
"rarity_level": 6, "rarity_level": 5,
"rarity_icon": "✨", "rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z", "created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z" "updated_at": "2025-03-26T12:25:17.706Z"
@ -12596,7 +12596,7 @@
"decks": [ "decks": [
9 9
], ],
"rarity_level": 6, "rarity_level": 5,
"rarity_icon": "✨", "rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z", "created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z" "updated_at": "2025-03-26T12:25:17.706Z"
@ -12612,7 +12612,7 @@
"decks": [ "decks": [
9 9
], ],
"rarity_level": 6, "rarity_level": 5,
"rarity_icon": "✨", "rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z", "created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z" "updated_at": "2025-03-26T12:25:17.706Z"
@ -12628,7 +12628,7 @@
"decks": [ "decks": [
9 9
], ],
"rarity_level": 6, "rarity_level": 5,
"rarity_icon": "✨", "rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z", "created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z" "updated_at": "2025-03-26T12:25:17.706Z"

View file

@ -58,5 +58,25 @@
"created_at": "2025-03-13T04:52:29.166Z", "created_at": "2025-03-13T04:52:29.166Z",
"updated_at": "2025-03-13T04:52:29.166Z" "updated_at": "2025-03-13T04:52:29.166Z"
} }
},
{
"model": "account.emailaddress",
"pk": 1,
"fields": {
"user": 1,
"email": "rob@badblocks.email",
"verified": true,
"primary": true
}
},
{
"model": "account.emailaddress",
"pk": 2,
"fields": {
"user": 2,
"email": "nathanward2016@gmail.com",
"verified": true,
"primary": true
}
} }
] ]

View file

@ -1,4 +1,5 @@
{% load card_badge %} {% load card_badge %}
{% load pagination_tags %}
{% if group_by and groups %} {% if group_by and groups %}
{% for group in groups %} {% for group in groups %}
<div class="divider">{{ group.group }}</div> <div class="divider">{{ group.group }}</div>
@ -20,21 +21,7 @@
</div> </div>
{% endif %} {% endif %}
<!-- Pagination Controls --> <!-- Somewhere in your template, e.g., after the card list: -->
<div class="mt-6"> {% if page_obj %}
{% if is_paginated %} {% render_pagination page_obj %}
<div class="flex justify-center space-x-2"> {% endif %}
{% if page_obj.has_previous %}
<button class="btn btn-outline" @click="$dispatch('change-page', { page: {{ page_obj.previous_page_number }} })">
Previous
</button>
{% endif %}
<span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
{% if page_obj.has_next %}
<button class="btn btn-outline" @click="$dispatch('change-page', { page: {{ page_obj.next_page_number }} })">
Next
</button>
{% endif %}
</div>
{% endif %}
</div>

View file

@ -18,35 +18,40 @@
x-init="loadCards()" x-init="loadCards()"
x-on:change-page.window="page = $event.detail.page; loadCards()" x-on:change-page.window="page = $event.detail.page; loadCards()"
> >
<h1 class="text-2xl font-bold mb-4">Cards</h1>
<div class="flex flex-wrap items-center justify-between mb-6"> <div class="flex flex-wrap items-center justify-between mb-6">
<!-- Sort Dropdown --> <div>
<div class="dropdown dropdown-end m-1"> <h1 class="text-2xl font-bold">Cards</h1>
<div tabindex="0" class="btn">
<span x-text="order === 'absolute' ? 'Absolute' : (order === 'alphabetical' ? 'Alphabetical' : 'Rarity')"></span> 🞃
</div>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
<li><a href="#" @click.prevent="order = 'absolute'; page = 1; loadCards()">Absolute</a></li>
<li><a href="#" @click.prevent="order = 'alphabetical'; page = 1; loadCards()">Alphabetical</a></li>
<li><a href="#" @click.prevent="order = 'rarity'; page = 1; loadCards()">Rarity</a></li>
</ul>
</div> </div>
<!-- Grouping Dropdown --> <div>
<div class="dropdown dropdown-end m-1"> <!-- Sort Dropdown -->
<div tabindex="0" class="btn"> <div class="dropdown dropdown-end m-1">
<span x-text="groupBy === 'none' ? 'No Group' : (groupBy.charAt(0).toUpperCase() + groupBy.slice(1))"></span> 🞃 <div tabindex="0" class="btn">
Sort by: <span x-text="order === 'absolute' ? 'None' : (order === 'alphabetical' ? 'Alphabetical' : 'Rarity')"></span> 🞃
</div>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
<li><a href="#" @click.prevent="order = 'absolute'; page = 1; loadCards()">None</a></li>
<li><a href="#" @click.prevent="order = 'alphabetical'; page = 1; loadCards()">Alphabetical</a></li>
<li><a href="#" @click.prevent="order = 'rarity'; page = 1; loadCards()">Rarity</a></li>
</ul>
</div>
<!-- Grouping Dropdown -->
<div class="dropdown dropdown-end m-1">
<div tabindex="0" class="btn">
Group by: <span x-text="groupBy === 'none' ? 'None' : (groupBy.charAt(0).toUpperCase() + groupBy.slice(1))"></span> 🞃
</div>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
<li><a href="#" @click.prevent="groupBy = 'none'; page = 1; loadCards()">None</a></li>
<li><a href="#" @click.prevent="groupBy = 'deck'; page = 1; loadCards()">Deck</a></li>
<li><a href="#" @click.prevent="groupBy = 'cardset'; page = 1; loadCards()">Cardset</a></li>
<li><a href="#" @click.prevent="groupBy = 'rarity'; page = 1; loadCards()">Rarity</a></li>
</ul>
</div> </div>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
<li><a href="#" @click.prevent="groupBy = 'none'; page = 1; loadCards()">No Group</a></li>
<li><a href="#" @click.prevent="groupBy = 'deck'; page = 1; loadCards()">Deck</a></li>
<li><a href="#" @click.prevent="groupBy = 'cardset'; page = 1; loadCards()">Cardset</a></li>
<li><a href="#" @click.prevent="groupBy = 'rarity'; page = 1; loadCards()">Rarity</a></li>
</ul>
</div> </div>
</div> </div>
<!-- Container for the partial card list --> <!-- Container for the partial card list -->
<div x-ref="cardList"> <div x-ref="cardList">
<!-- The contents of _card_list.html will be loaded here via AJAX -->
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,21 +1,8 @@
{% load card_badge %} {% load card_badge %}
{% comment %}
This partial expects:
- cards: a list of card objects
- mode: a string that determines the render style.
It should be "offered" for Most Offered Cards and "wanted" for Most Wanted Cards.
- Optional:
'show_zero' flag (default False): if True, also display cards with 0 offers.
'layout' variable: if set to "auto", use an auto-fit grid based on available horizontal space.
{% endcomment %}
{% if cards %} {% if cards %}
<div class="mx-4 grid gap-3 grid-cols-[repeat(auto-fit,minmax(150px,1fr))] justify-items-center"> <div class="mx-4 grid gap-3 grid-cols-[repeat(auto-fit,minmax(150px,1fr))] justify-items-center">
{% for card in cards %} {% for card in cards %}
{% if mode == "offered" %}
<a href="{% url 'cards:card_detail' card.id %}" <a href="{% url 'cards:card_detail' card.id %}"
{% else %}
<a href="{% url 'cards:card_detail' card.id %}"
{% endif %}
class="flex justify-between items-center text-primary no-underline"> class="flex justify-between items-center text-primary no-underline">
{% card_badge card card.offer_count %} {% card_badge card card.offer_count %}
</a> </a>

View file

@ -48,7 +48,7 @@
</div> </div>
<div class="card-body my-4 p-0"> <div class="card-body my-4 p-0">
{% cache 3600 most_offered_cards %} {% cache 3600 most_offered_cards %}
{% include "home/_card_list.html" with cards=most_offered_cards mode="wanted" %} {% include "home/_card_list.html" with cards=most_offered_cards %}
{% endcache %} {% endcache %}
</div> </div>
</div> </div>
@ -61,7 +61,7 @@
</div> </div>
<div class="card-body my-4 p-0"> <div class="card-body my-4 p-0">
{% cache 3600 most_wanted_cards %} {% cache 3600 most_wanted_cards %}
{% include "home/_card_list.html" with cards=most_wanted_cards mode="offered" %} {% include "home/_card_list.html" with cards=most_wanted_cards %}
{% endcache %} {% endcache %}
</div> </div>
</div> </div>
@ -74,7 +74,7 @@
</div> </div>
<div class="card-body my-4 p-0"> <div class="card-body my-4 p-0">
{% cache 3600 least_offered_cards %} {% cache 3600 least_offered_cards %}
{% include "home/_card_list.html" with cards=least_offered_cards mode="wanted" %} {% include "home/_card_list.html" with cards=least_offered_cards %}
{% endcache %} {% endcache %}
</div> </div>
</div> </div>
@ -121,8 +121,8 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
<!-- DaisyUI Tabs for Featured Offers --> <!-- DaisyUI Tabs for Featured Offers (hidden for now) -->
<div class="card card-border bg-base-100 shadow-lg w-96 md:w-80 lg:w-96 mt-8 mx-auto"> <div class="card card-border bg-base-100 shadow-lg w-96 md:w-80 lg:w-96 mt-8 mx-auto hidden">
<!-- Tabs navigation using daisyUI tabs-box --> <!-- Tabs navigation using daisyUI tabs-box -->
<div class="tabs tabs-box bg-white dark:bg-base-100 grid grid-cols-3 gap-1.5 justify-items-stretch"> <div class="tabs tabs-box bg-white dark:bg-base-100 grid grid-cols-3 gap-1.5 justify-items-stretch">
<!-- Radio inputs for controlling tab state --> <!-- Radio inputs for controlling tab state -->

View file

@ -7,14 +7,14 @@
<div class="container mx-auto max-w-4xl mt-6" x-data="{ allExpanded: false }"> <div class="container mx-auto max-w-4xl mt-6" x-data="{ allExpanded: false }">
<!-- Header--> <!-- Header-->
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<a href="{% url 'trade_offer_create' %}" class="btn btn-success">Create New Offer</a> <h1 class="text-2xl font-bold">All Trade Offers</h1>
<div> <div>
<form method="get" class="flex items-center space-x-4" x-data> <form method="get" class="flex items-center gap-4" x-data>
<label class="cursor-pointer flex items-center space-x-2"> <label class="cursor-pointer flex items-center gap-2">
<span x-text="allExpanded ? 'Collapse All' : 'Expand All'"></span> <span x-text="allExpanded ? 'Collapse All' : 'Expand All'"></span>
<input type="checkbox" name="all_expanded" value="true" class="toggle toggle-primary" @click="allExpanded = !allExpanded; $dispatch('toggle-all', { expanded: allExpanded })"> <input type="checkbox" name="all_expanded" value="true" class="toggle toggle-primary" @click="allExpanded = !allExpanded; $dispatch('toggle-all', { expanded: allExpanded })">
</label> </label>
<label class="cursor-pointer flex items-center space-x-2"> <label class="cursor-pointer flex items-center gap-2">
<span>Only Closed</span> <span>Only Closed</span>
<input type="checkbox" name="show_closed" value="true" class="toggle toggle-primary" @change="$el.form.submit()" {% if show_closed %}checked{% endif %}> <input type="checkbox" name="show_closed" value="true" class="toggle toggle-primary" @change="$el.form.submit()" {% if show_closed %}checked{% endif %}>
</label> </label>
@ -24,7 +24,9 @@
</div> </div>
<!-- Trade Offers --> <!-- Trade Offers -->
<section class="mb-12"> <section class="mb-12">
<h2 class="text-2xl font-bold mb-4">All Trade Offers</h2> <div class="flex justify-end mb-4">
<a href="{% url 'trade_offer_create' %}" class="btn btn-success">Create New Offer</a>
</div>
<div <div
id="all-trade-offers" id="all-trade-offers"
x-data="tradeOffersPagination('{% url 'trade_offer_list' %}?')" x-data="tradeOffersPagination('{% url 'trade_offer_list' %}?')"

View file

@ -4,5 +4,5 @@
<div class="rarity row-start-2 col-span-2 truncate self-end align-bottom text-xs">{{ rarity }}</div> <div class="rarity row-start-2 col-span-2 truncate self-end align-bottom text-xs">{{ rarity }}</div>
<div class="cardset row-start-2 col-start-3 col-span-2 text-right truncate self-end align-bottom font-semibold leading-none text-sm">{{ cardset }}</div> <div class="cardset row-start-2 col-start-3 col-span-2 text-right truncate self-end align-bottom font-semibold leading-none text-sm">{{ cardset }}</div>
</div> </div>
{% if quantity %}<span class="card-quantity-badge freeze-bg-color absolute top-3.75 right-1.5 bg-gray-600 text-white text-sm font-semibold rounded-full px-1.5">{{ quantity }}</span>{% endif %} {% if quantity != "" %}<span class="card-quantity-badge freeze-bg-color absolute top-3.75 right-1.5 bg-gray-600 text-white text-sm font-semibold rounded-full px-1.5">{{ quantity }}</span>{% endif %}
</div> </div>

View file

@ -0,0 +1,38 @@
<div class="flex justify-center items-center space-x-2">
<!-- First Button -->
<button class="btn btn-outline btn-md"
{% if not page_obj.has_previous %}disabled{% endif %}
@click="$dispatch('change-page', { page: 1 })">
First
</button>
<!-- Previous Button -->
<button class="btn btn-outline btn-md"
{% if not page_obj.has_previous %}disabled{% endif %}
@click="$dispatch('change-page', { page: {{ page_obj.previous_page }} })">
Prev
</button>
<!-- Goto Page -->
<span class="flex items-center space-x-1 gap-2">
<input type="number" min="1" max="{{ page_obj.paginator.num_pages }}" value="{{ page_obj.number }}"
class="input input-xs text-center" id="gotoPageInput">
<button class="btn btn-outline btn-md"
@click="(document.getElementById('gotoPageInput').value >= 1 && document.getElementById('gotoPageInput').value <= {{ page_obj.paginator.num_pages }}) && $dispatch('change-page', { page: parseInt(document.getElementById('gotoPageInput').value) })">
Go
</button>
</span>
<!-- Next Button -->
<button class="btn btn-outline btn-md"
{% if not page_obj.has_next %}disabled{% endif %}
@click="$dispatch('change-page', { page: {{ page_obj.next_page }} })">
Next
</button>
<!-- Last Button -->
<button class="btn btn-outline btn-md"
{% if page_obj.number == page_obj.paginator.num_pages %}disabled{% endif %}
@click="$dispatch('change-page', { page: {{ page_obj.paginator.num_pages }} })">
Last
</button>
</div>
<div class="flex items-center justify-center mt-2">
<span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
</div>

View file

@ -57,7 +57,7 @@
<!-- Main Trade Offer Row --> <!-- Main Trade Offer Row -->
<div class="flip-face-body self-start"> <div class="flip-face-body self-start">
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline block"> <a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline block">
<div class="px-2 main-badges {% if not screenshot_mode and have_cards_available|length == 1 and want_cards_available|length == 1 %}py-[14px]{%else%}pb-0{% endif %}"> <div class="px-2 main-badges pb-0">
{% if screenshot_mode and num_cards_available >= 4 %} {% if screenshot_mode and num_cards_available >= 4 %}
<!-- When screenshot_mode is true, use an outer grid with 3 columns: Has side, a vertical divider, and Wants side --> <!-- When screenshot_mode is true, use an outer grid with 3 columns: Has side, a vertical divider, and Wants side -->
<div class="flex flex-row gap-2 justify-around"> <div class="flex flex-row gap-2 justify-around">
@ -151,6 +151,8 @@
d="M19 9l-7 7-7-7" /> d="M19 9l-7 7-7-7" />
</svg> </svg>
</div> </div>
{% else %}
<div class="h-5"></div>
{% endif %} {% endif %}
{% if not screenshot_mode %} {% if not screenshot_mode %}
<div class="flip-face-footer self-end"> <div class="flip-face-footer self-end">

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-03-28 04:43 # Generated by Django 5.1.2 on 2025-03-29 03:33
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models