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 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
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 cards.models import Card
from trades.models import TradeOffer
from cards.mixins import ReusablePaginationMixin
class CardDetailView(DetailView):
model = Card
@ -62,9 +63,9 @@ class TradeOfferWantCardListView(ListView):
context['side'] = 'want'
return context
class CardListView(ListView):
class CardListView(ReusablePaginationMixin, ListView):
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"
def get_template_names(self):
@ -102,13 +103,11 @@ class CardListView(ListView):
context["group_by"] = group_by
if group_by in ("deck", "cardset", "rarity"):
# Fetch the complete queryset (no slicing)
full_qs = self.get_queryset()
all_cards = list(full_qs)
flat_cards = []
if group_by == "deck":
# Each card may belong to multiple decks reproduce the existing logic.
for card in all_cards:
for deck in card.decks.all():
flat_cards.append({"group": deck.name, "card": card})
@ -119,23 +118,19 @@ class CardListView(ListView):
flat_cards.sort(key=lambda x: x["group"].lower())
elif group_by == "rarity":
for card in all_cards:
flat_cards.append({"group": card.rarity_level, "card": card})
flat_cards.sort(key=lambda x: x["group"], reverse=True)
flat_cards.append({"group": card.rarity_icon, "sort_group": card.rarity_level, "card": card})
flat_cards.sort(key=lambda x: x["sort_group"], reverse=True)
total_cards = len(flat_cards)
try:
page_number = int(self.request.GET.get("page", 1))
if page_number < 1:
page_number = 1
except ValueError:
page_number = 1
per_page = 96
start = (page_number - 1) * per_page
end = page_number * per_page
page_flat_cards = flat_cards[start:end]
# Use our custom mixin logic here
self.per_page = 36
page_flat_cards, pagination_context = self.paginate_data(flat_cards, page_number)
# Reassemble the flat list into grouped structure for just this page.
# Reassemble the flat list into groups for the current page.
page_groups = []
for item in page_flat_cards:
group_value = item["group"]
@ -145,25 +140,22 @@ class CardListView(ListView):
else:
page_groups.append({"group": group_value, "cards": [card_obj]})
context["groups"] = page_groups
# Set up custom pagination context.
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["page_obj"] = pagination_context
context["total_cards"] = len(flat_cards)
context["object_list"] = full_qs
return context
else:
# 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.urls import reverse_lazy
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 trades.models import TradeOffer, TradeAcceptance, TradeOfferHaveCard, TradeOfferWantCard
from cards.models import Card
@ -61,7 +62,9 @@ class HomePageView(TemplateView):
# Least Offered Cards
try:
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]
)
except Exception as e:

View file

@ -4463,7 +4463,7 @@
"decks": [
3
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -4479,7 +4479,7 @@
"decks": [
2
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -4495,7 +4495,7 @@
"decks": [
4
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -4511,7 +4511,7 @@
"decks": [
2
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -4527,7 +4527,7 @@
"decks": [
3
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -4543,7 +4543,7 @@
"decks": [
4
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -4559,7 +4559,7 @@
"decks": [
3
],
"rarity_icon": "🌟🌟🌟",
"rarity_icon": "⭐️⭐️⭐️",
"rarity_level": 7,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -4575,7 +4575,7 @@
"decks": [
4
],
"rarity_icon": "🌟🌟🌟",
"rarity_icon": "⭐️⭐️⭐️",
"rarity_level": 7,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -4591,7 +4591,7 @@
"decks": [
2
],
"rarity_icon": "🌟🌟🌟",
"rarity_icon": "⭐️⭐️⭐️",
"rarity_level": 7,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -4609,7 +4609,7 @@
3,
4
],
"rarity_icon": "🌟🌟🌟",
"rarity_icon": "⭐️⭐️⭐️",
"rarity_level": 7,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -5991,7 +5991,7 @@
"decks": [
5
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -6007,7 +6007,7 @@
"decks": [
5
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -6023,7 +6023,7 @@
"decks": [
5
],
"rarity_icon": "🌟🌟🌟",
"rarity_icon": "⭐️⭐️⭐️",
"rarity_level": 7,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -9218,7 +9218,7 @@
"decks": [
6
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -9234,7 +9234,7 @@
"decks": [
7
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -9250,7 +9250,7 @@
"decks": [
6
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -9266,7 +9266,7 @@
"decks": [
7
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -9282,7 +9282,7 @@
"decks": [
6
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -9298,7 +9298,7 @@
"decks": [
7
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -9314,7 +9314,7 @@
"decks": [
6
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -9330,7 +9330,7 @@
"decks": [
7
],
"rarity_icon": "🌟🌟",
"rarity_icon": "⭐️⭐️",
"rarity_level": 6,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -9346,7 +9346,7 @@
"decks": [
7
],
"rarity_icon": "🌟🌟🌟",
"rarity_icon": "⭐️⭐️⭐️",
"rarity_level": 7,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -9362,7 +9362,7 @@
"decks": [
6
],
"rarity_icon": "🌟🌟🌟",
"rarity_icon": "⭐️⭐️⭐️",
"rarity_level": 7,
"created_at": "2025-02-17T02:44:18.706Z",
"updated_at": "2025-02-17T02:44:18.706Z"
@ -12484,7 +12484,7 @@
"decks": [
9
],
"rarity_level": 6,
"rarity_level": 5,
"rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z"
@ -12500,7 +12500,7 @@
"decks": [
9
],
"rarity_level": 6,
"rarity_level": 5,
"rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z"
@ -12516,7 +12516,7 @@
"decks": [
9
],
"rarity_level": 6,
"rarity_level": 5,
"rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z"
@ -12532,7 +12532,7 @@
"decks": [
9
],
"rarity_level": 6,
"rarity_level": 5,
"rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z"
@ -12548,7 +12548,7 @@
"decks": [
9
],
"rarity_level": 6,
"rarity_level": 5,
"rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z"
@ -12564,7 +12564,7 @@
"decks": [
9
],
"rarity_level": 6,
"rarity_level": 5,
"rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z"
@ -12580,7 +12580,7 @@
"decks": [
9
],
"rarity_level": 6,
"rarity_level": 5,
"rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z"
@ -12596,7 +12596,7 @@
"decks": [
9
],
"rarity_level": 6,
"rarity_level": 5,
"rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z"
@ -12612,7 +12612,7 @@
"decks": [
9
],
"rarity_level": 6,
"rarity_level": 5,
"rarity_icon": "✨",
"created_at": "2025-03-26T12:25:17.706Z",
"updated_at": "2025-03-26T12:25:17.706Z"
@ -12628,7 +12628,7 @@
"decks": [
9
],
"rarity_level": 6,
"rarity_level": 5,
"rarity_icon": "✨",
"created_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",
"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 pagination_tags %}
{% if group_by and groups %}
{% for group in groups %}
<div class="divider">{{ group.group }}</div>
@ -20,21 +21,7 @@
</div>
{% endif %}
<!-- Pagination Controls -->
<div class="mt-6">
{% if is_paginated %}
<div class="flex justify-center space-x-2">
{% 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>
<!-- Somewhere in your template, e.g., after the card list: -->
{% if page_obj %}
{% render_pagination page_obj %}
{% endif %}

View file

@ -18,15 +18,19 @@
x-init="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>
<h1 class="text-2xl font-bold">Cards</h1>
</div>
<div>
<!-- Sort Dropdown -->
<div class="dropdown dropdown-end m-1">
<div tabindex="0" class="btn">
<span x-text="order === 'absolute' ? 'Absolute' : (order === 'alphabetical' ? 'Alphabetical' : 'Rarity')"></span> 🞃
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()">Absolute</a></li>
<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>
@ -34,19 +38,20 @@
<!-- Grouping Dropdown -->
<div class="dropdown dropdown-end m-1">
<div tabindex="0" class="btn">
<span x-text="groupBy === 'none' ? 'No Group' : (groupBy.charAt(0).toUpperCase() + groupBy.slice(1))"></span> 🞃
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()">No Group</a></li>
<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>
</div>
<!-- Container for the partial card list -->
<div x-ref="cardList">
<!-- The contents of _card_list.html will be loaded here via AJAX -->
</div>
</div>
{% endblock %}

View file

@ -1,21 +1,8 @@
{% 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 %}
<div class="mx-4 grid gap-3 grid-cols-[repeat(auto-fit,minmax(150px,1fr))] justify-items-center">
{% for card in cards %}
{% if mode == "offered" %}
<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">
{% card_badge card card.offer_count %}
</a>

View file

@ -48,7 +48,7 @@
</div>
<div class="card-body my-4 p-0">
{% 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 %}
</div>
</div>
@ -61,7 +61,7 @@
</div>
<div class="card-body my-4 p-0">
{% 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 %}
</div>
</div>
@ -74,7 +74,7 @@
</div>
<div class="card-body my-4 p-0">
{% 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 %}
</div>
</div>
@ -121,8 +121,8 @@
{% endif %}
{% endfor %}
</div>
<!-- DaisyUI Tabs for Featured Offers -->
<div class="card card-border bg-base-100 shadow-lg w-96 md:w-80 lg:w-96 mt-8 mx-auto">
<!-- 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 hidden">
<!-- 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">
<!-- 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 }">
<!-- Header-->
<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>
<form method="get" class="flex items-center space-x-4" x-data>
<label class="cursor-pointer flex items-center space-x-2">
<form method="get" class="flex items-center gap-4" x-data>
<label class="cursor-pointer flex items-center gap-2">
<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 })">
</label>
<label class="cursor-pointer flex items-center space-x-2">
<label class="cursor-pointer flex items-center gap-2">
<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 %}>
</label>
@ -24,7 +24,9 @@
</div>
<!-- Trade Offers -->
<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
id="all-trade-offers"
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="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>
{% 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>

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 -->
<div class="flip-face-body self-start">
<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 %}
<!-- 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">
@ -151,6 +151,8 @@
d="M19 9l-7 7-7-7" />
</svg>
</div>
{% else %}
<div class="h-5"></div>
{% endif %}
{% if not screenshot_mode %}
<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
from django.db import migrations, models