Fix expanded state handling for trade offers and modify expand all on trade offers listing to persist between pages

This commit is contained in:
badblocks 2025-04-02 13:39:15 -07:00
parent 6a61b79bbe
commit 63e20bace6
7 changed files with 74 additions and 128 deletions

View file

@ -24,9 +24,9 @@
<div x-show="activeTab === 'dash'"> <div x-show="activeTab === 'dash'">
<div class="card bg-base-100 shadow-xl mb-4"> <div class="card bg-base-100 shadow-xl mb-4">
<div class="card-body"> <div class="card-body">
<h2 class="card-title">{{ _('Trade Summary') }}</h2> <h2 class="card-title mb-2">{{ _('Trade Summary') }}</h2>
<div class="flex flex-col md:flex-row justify-center gap-4"> <div class="flex flex-col md:flex-row justify-center gap-4">
<div class="stats shadow"> <div class="stats shadow-lg bg-base-300">
<div class="stat"> <div class="stat">
<div class="stat-title">{{ _('Your Reputation') }}</div> <div class="stat-title">{{ _('Your Reputation') }}</div>
<div class="stat-value">{{ request.user.reputation_score }}</div> <div class="stat-value">{{ request.user.reputation_score }}</div>
@ -38,7 +38,7 @@
<div class="stat-desc">{{ _('Active Offers') }}</div> <div class="stat-desc">{{ _('Active Offers') }}</div>
</div> </div>
</div> </div>
<div class="stats shadow"> <div class="stats shadow-lg bg-base-300">
<div class="stat"> <div class="stat">
<div class="stat-title">{{ _('Waiting on You') }}</div> <div class="stat-title">{{ _('Waiting on You') }}</div>
<div class="stat-value">{{ trade_acceptances_waiting_paginated.page_obj.count }}</div> <div class="stat-value">{{ trade_acceptances_waiting_paginated.page_obj.count }}</div>
@ -55,7 +55,7 @@
</div> </div>
<div class="card bg-base-100 shadow-xl"> <div class="card bg-base-100 shadow-xl">
<div class="card-body"> <div class="card-body">
<h2 class="card-title">{{ _('Quick Actions') }}</h2> <h2 class="card-title mb-2">{{ _('Quick Actions') }}</h2>
<div class="flex space-x-4"> <div class="flex space-x-4">
<a href="{% url 'trade_offer_create' %}" class="btn btn-primary grow">{{ _('Create New Offer') }}</a> <a href="{% url 'trade_offer_create' %}" class="btn btn-primary grow">{{ _('Create New Offer') }}</a>
<a href="{% url 'trade_offer_list' %}" class="btn btn-secondary grow">{{ _('View All Offers') }}</a> <a href="{% url 'trade_offer_list' %}" class="btn btn-secondary grow">{{ _('View All Offers') }}</a>

View file

@ -11,7 +11,7 @@
{# Render a trade acceptance using our new tag #} {# Render a trade acceptance using our new tag #}
{% render_trade_acceptance offer %} {% render_trade_acceptance offer %}
{% else %} {% else %}
{% render_trade_offer offer %} {% render_trade_offer offer expanded=expanded %}
{% endif %} {% endif %}
</div> </div>
{% empty %} {% empty %}

View file

@ -4,15 +4,48 @@
{% block title %}Trade Offers{% endblock title %} {% block title %}Trade Offers{% endblock title %}
{% block content %} {% block content %}
<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="{
<!-- Header--> // Initialize allExpanded from the URL if present, defaulting to false.
allExpanded: new URLSearchParams(window.location.search).get('expanded') === 'true',
page: {{ page_obj.number|default:1 }},
loadOffers() {
let url = new URL('{% url 'trade_offer_list' %}', window.location.origin);
let params = new URLSearchParams(window.location.search);
params.set('page', this.page);
// Include the expanded state if active
if (this.allExpanded) {
params.set('expanded', 'true');
} else {
params.delete('expanded');
}
url.search = params.toString();
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' }})
.then(response => response.text())
.then(html => { this.$refs.offersContainer.innerHTML = html; });
},
// Update the URL so that navigation preserves our expanded state.
updateUrl() {
let params = new URLSearchParams(window.location.search);
if (this.allExpanded) {
params.set('expanded', 'true');
} else {
params.delete('expanded');
}
history.replaceState(null, '', window.location.pathname + '?' + params.toString());
}
}"
x-init="loadOffers()"
x-on:change-page.window="page = $event.detail.page; loadOffers()">
<!-- Header with the toggle -->
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h1 class="text-2xl font-bold">Trade Offers</h1> <h1 class="text-2xl font-bold">Trade Offers</h1>
<div> <div>
<form method="get" class="flex items-center gap-4" x-data> <form method="get" class="flex items-center gap-4" x-data>
<label class="cursor-pointer flex items-center gap-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 }); updateUrl();">
</label> </label>
<label class="cursor-pointer flex items-center gap-2"> <label class="cursor-pointer flex items-center gap-2">
<span>Only Closed</span> <span>Only Closed</span>
@ -27,22 +60,8 @@
<div class="flex justify-end mb-4"> <div class="flex justify-end mb-4">
<a href="{% url 'trade_offer_create' %}" class="btn btn-success">Create New Offer</a> <a href="{% url 'trade_offer_create' %}" class="btn btn-success">Create New Offer</a>
</div> </div>
<div id="all-trade-offers" <div id="all-trade-offers" x-ref="offersContainer">
x-data="{ {% include "trades/_trade_offer_list.html" with offers=offers page_obj=page_obj expanded=expanded %}
page: {{ page_obj.number|default:1 }},
loadOffers() {
let url = new URL('{% url 'trade_offer_list' %}', window.location.origin);
let params = new URLSearchParams(window.location.search);
params.set('page', this.page);
url.search = params.toString();
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' }})
.then(response => response.text())
.then(html => { this.$el.innerHTML = html; });
}
}"
x-init="loadOffers()"
x-on:change-page.window="page = $event.detail.page; loadOffers()">
{% include "trades/_trade_offer_list.html" with offers=offers page_obj=page_obj %}
</div> </div>
</section> </section>
</div> </div>

View file

@ -7,7 +7,7 @@
<div class="container mx-auto max-w-2xl mt-6"> <div class="container mx-auto max-w-2xl mt-6">
<h2 class="text-2xl font-bold">Trade Offer Details</h2> <h2 class="text-2xl font-bold">Trade Offer Details</h2>
<div class="flex justify-center mt-10"> <div class="flex justify-center mt-10">
{% render_trade_offer object %} {% render_trade_offer object expanded=True %}
</div> </div>
{% if acceptance_form %} {% if acceptance_form %}
<div class="w-3/4 mx-auto mt-4"> <div class="w-3/4 mx-auto mt-4">

View file

@ -1,24 +1,8 @@
{% load gravatar card_badge cache %} {% load gravatar card_badge cache %}
{% cache 60 trade_offer offer_pk %} {% cache 60 trade_offer offer_pk %}
<div class="trade-offer-card-screenshot p-4 h-full w-auto flex justify-center" <div class="trade-offer-card m-2 h-full w-auto flex justify-center">
{% if screenshot_mode %} <div x-data="tradeOfferCard()" x-init="defaultExpanded = {{expanded|lower}}; badgeExpanded = defaultExpanded; acceptanceExpanded = defaultExpanded; flipped = {{flipped|lower}}" class="transition-all duration-500 trade-offer-card my-auto"
x-data="{
setDimension() {
const aspectRatio = 1.91;
// If the element is taller than it is wide,
// adjust the width based on the element's height.
if ($el.offsetHeight > $el.offsetWidth) {
$el.style.width = ($el.offsetHeight * aspectRatio) + 'px';
} else {
// Otherwise, adjust the height based on the element's width.
$el.style.height = ($el.offsetWidth / aspectRatio) + 'px';
}
}
}"
x-init="setDimension(); window.addEventListener('resize', setDimension)"
{% endif %}>
<div x-data="tradeOfferCard()" x-init="badgeExpanded = {{expanded|lower}}" class="transition-all duration-500 trade-offer-card my-auto"
@toggle-all.window="setBadge($event.detail.expanded)"> @toggle-all.window="setBadge($event.detail.expanded)">
<!-- Flip container providing perspective --> <!-- Flip container providing perspective -->
@ -27,12 +11,12 @@
The rotating element (.flip-inner) now uses CSS Grid to stack its children in a single cell. The rotating element (.flip-inner) now uses CSS Grid to stack its children in a single cell.
Persistent border, shadow, and rounding are applied here and the card rotates entirely. Persistent border, shadow, and rounding are applied here and the card rotates entirely.
--> -->
<div class="flip-inner freeze-bg-color grid grid-cols-1 grid-rows-1 card bg-base-100 card-border shadow-lg {% if screenshot_mode and num_cards_available >= 4 %}w-160{% else %}w-84 md:w-84 lg:w-84{% endif %} transform transition-transform duration-700 ease-in-out" <div class="flip-inner freeze-bg-color grid grid-cols-1 grid-rows-1 card bg-base-100 card-border shadow-lg w-84 transform transition-transform duration-700 ease-in-out"
:class="{'rotate-y-180': flipped}"> :class="{'rotate-y-180': flipped}">
<!-- Front Face: Trade Offer --> <!-- Front Face: Trade Offer -->
<!-- Using grid placement classes (col-start-1 row-start-1) ensures both faces overlap --> <!-- Using grid placement classes (col-start-1 row-start-1) ensures both faces overlap -->
<div class="flip-face front {% if screenshot_mode %}mb-2{% endif %} col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between"> <div class="flip-face front col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between">
<!-- Header --> <!-- Header -->
<div class="flip-face-header self-start"> <div class="flip-face-header 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">
@ -58,71 +42,26 @@
<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 pb-0"> <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">
<!-- Has Side (inner grid of 2 columns) -->
<div class="flex flex-row gap-2">
{% for card in have_cards_available|slice:"0:2" %}
{% card_badge card.card card.quantity %}
{% endfor %}
</div>
<!-- Vertical Divider -->
<div class="flex justify-center">
<div class="w-px bg-gray-300 h-full"></div>
</div>
<!-- Wants Side (inner grid of 2 columns) -->
<div class="flex flex-row gap-2">
{% for card in want_cards_available|slice:"0:2" %}
{% card_badge card.card card.quantity %}
{% endfor %}
</div>
</div>
{% else %}
<!-- Normal mode: just use an outer grid with 2 columns --> <!-- Normal mode: just use an outer grid with 2 columns -->
<div class="flex flex-row gap-2 justify-around">
<!-- Has Side -->
<div class="flex flex-col gap-2">
{% for card in have_cards_available|slice:"0:1" %}
{% card_badge card.card card.quantity %}
{% endfor %}
</div>
<!-- Wants Side -->
<div class="flex flex-col gap-2">
{% for card in want_cards_available|slice:"0:1" %}
{% card_badge card.card card.quantity %}
{% endfor %}
</div>
</div>
{% endif %}
</div>
</a>
<!-- Extra Card Badges (Collapsible) -->
{% if screenshot_mode and num_cards_available >= 4 %}
<div class="px-2 extra-badges">
<!-- In screenshot mode, add a vertical divider between the Has and Wants sides -->
<div class="flex flex-row gap-2 justify-around"> <div class="flex flex-row gap-2 justify-around">
<!-- Has Side Extra Badges --> <!-- Has Side -->
<div class="grid grid-cols-2 gap-2 {% if screenshot_mode and num_cards_available >= 4 %}w-[296px]{% endif %}"> <div class="flex flex-col gap-2">
{% for card in have_cards_available|slice:"2:" %} {% for card in have_cards_available|slice:"0:1" %}
{% card_badge card.card card.quantity %} {% card_badge card.card card.quantity %}
{% endfor %} {% endfor %}
</div> </div>
<!-- Vertical Divider --> <!-- Wants Side -->
<div class="flex justify-center"> <div class="flex flex-col gap-2">
<div class="w-px bg-gray-300 h-full"></div> {% for card in want_cards_available|slice:"0:1" %}
</div> {% card_badge card.card card.quantity %}
<!-- Wants Side Extra Badges -->
<div class="grid grid-cols-2 gap-2 {% if screenshot_mode and num_cards_available >= 4 %}w-[296px]{% endif %}">
{% for card in want_cards_available|slice:"2:" %}
{% card_badge card.card card.quantity %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div> </div>
{% else %} </a>
<div {% if not screenshot_mode %}x-show="badgeExpanded" x-collapse.duration.500ms x-cloak{% endif %} class="px-2 extra-badges">
<!-- Extra Card Badges (Collapsible) -->
<div x-show="badgeExpanded" x-collapse.duration.500ms x-cloak class="px-2 extra-badges">
<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="flex flex-row gap-2 justify-around"> <div class="flex flex-row gap-2 justify-around">
<!-- Has Side Extra Badges --> <!-- Has Side Extra Badges -->
@ -140,9 +79,7 @@
</div> </div>
</a> </a>
</div> </div>
{% endif %}
</div> </div>
{% if not screenshot_mode and have_cards_available|length > 1 and want_cards_available|length > 1 %}
<div @click="badgeExpanded = !badgeExpanded" class="flex justify-center h-5 cursor-pointer"> <div @click="badgeExpanded = !badgeExpanded" class="flex justify-center h-5 cursor-pointer">
<svg x-bind:class="{ 'rotate-180': badgeExpanded }" <svg x-bind:class="{ 'rotate-180': badgeExpanded }"
class="transition-transform duration-500 h-5 w-5 cursor-pointer" class="transition-transform duration-500 h-5 w-5 cursor-pointer"
@ -151,10 +88,6 @@
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 %}
{% if not screenshot_mode %}
<div class="flip-face-footer self-end"> <div class="flip-face-footer self-end">
<div class="flex justify-between px-2 pb-2"> <div class="flex justify-between px-2 pb-2">
<div class="text-gray-500 text-sm tooltip tooltip-right" data-tip="ID: {{ offer_hash }}"> <div class="text-gray-500 text-sm tooltip tooltip-right" data-tip="ID: {{ offer_hash }}">
@ -165,8 +98,7 @@
</svg> </svg>
</div> </div>
<!-- Front-to-back flip button --> <!-- Front-to-back flip button -->
<div class="cursor-pointer text-gray-500" <div class="cursor-pointer text-gray-500" @click="flipped = true; acceptanceExpanded = defaultExpanded">
@click="if(badgeExpanded){ badgeExpanded = false; setTimeout(() => { flipped = true; }, 500); } else { flipped = true; }">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 8.689c0-.864.933-1.406 1.683-.977l7.108 4.061a1.125 1.125 0 0 1 0 1.954l-7.108 4.061A1.125 1.125 0 0 1 3 16.811V8.69ZM12.75 8.689c0-.864.933-1.406 1.683-.977l7.108 4.061a1.125 1.125 0 0 1 0 1.954l-7.108 4.061a1.125 1.125 0 0 1-1.683-.977V8.69Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M3 8.689c0-.864.933-1.406 1.683-.977l7.108 4.061a1.125 1.125 0 0 1 0 1.954l-7.108 4.061A1.125 1.125 0 0 1 3 16.811V8.69ZM12.75 8.689c0-.864.933-1.406 1.683-.977l7.108 4.061a1.125 1.125 0 0 1 0 1.954l-7.108 4.061a1.125 1.125 0 0 1-1.683-.977V8.69Z" />
</svg> </svg>
@ -174,18 +106,10 @@
</div> </div>
</div> </div>
</div> </div>
{% else %}
<div class="flip-face-footer self-end">
<div class="flex flex-col gap-2 text-center">
<div class="text-sm font-semibold text-base-content">{{ in_game_name }} {% if show_friend_code %}<span class="text-base-content/50">&bull;</span> {{ friend_code }}{% endif %}</div>
</div>
</div>
{% endif %}
</div> </div>
<!-- Back Face: Acceptances View --> <!-- Back Face: Acceptances View -->
<!-- Placed in the same grid cell as the front face --> <!-- Placed in the same grid cell as the front face -->
{% if not screenshot_mode %}
<div class="flip-face back col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between" style="transform: rotateY(180deg);"> <div class="flip-face back col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between" style="transform: rotateY(180deg);">
<div class="self-start"> <div class="self-start">
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline"> <a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline">
@ -281,8 +205,7 @@
</div> </div>
<div class="flex justify-between px-2 pb-2 self-end"> <div class="flex justify-between px-2 pb-2 self-end">
<!-- Back-to-front flip button --> <!-- Back-to-front flip button -->
<div class="text-gray-500 cursor-pointer" <div class="text-gray-500 cursor-pointer" @click="flipped = false; badgeExpanded = defaultExpanded">
@click="if(acceptanceExpanded){ acceptanceExpanded = false; setTimeout(() => { flipped = false; }, 500); } else { flipped = false; }">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 16.811c0 .864-.933 1.406-1.683.977l-7.108-4.061a1.125 1.125 0 0 1 0-1.954l7.108-4.061A1.125 1.125 0 0 1 21 8.689v8.122ZM11.25 16.811c0 .864-.933 1.406-1.683.977l-7.108-4.061a1.125 1.125 0 0 1 0-1.954l7.108-4.061a1.125 1.125 0 0 1 1.683.977v8.122Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M21 16.811c0 .864-.933 1.406-1.683.977l-7.108-4.061a1.125 1.125 0 0 1 0-1.954l7.108-4.061A1.125 1.125 0 0 1 21 8.689v8.122ZM11.25 16.811c0 .864-.933 1.406-1.683.977l-7.108-4.061a1.125 1.125 0 0 1 0-1.954l7.108-4.061a1.125 1.125 0 0 1 1.683.977v8.122Z" />
</svg> </svg>
@ -301,7 +224,6 @@
</div> </div>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -324,10 +246,5 @@
.rotate-y-180 { .rotate-y-180 {
transform: rotateY(180deg); transform: rotateY(180deg);
} }
{% if screenshot_mode %}
*:not(.freeze-bg-color) {
background-color: transparent !important;
}
{% endif %}
</style> </style>
{% endcache %} {% endcache %}

View file

@ -24,9 +24,17 @@ def render_trade_offer(context, offer, expanded=False):
if acceptance.is_active if acceptance.is_active
] ]
# Determine if the offer should show its back side (acceptances view) by default.
# If either side has no available cards, then flip the offer.
if not have_cards_available or not want_cards_available:
flipped = True
else:
flipped = False
return { return {
'offer_pk': offer.pk, 'offer_pk': offer.pk,
'expanded': expanded, 'expanded': expanded,
'flipped': flipped, # new flag to control the default face
'offer_hash': offer.hash, 'offer_hash': offer.hash,
'rarity_icon': offer.rarity_icon, 'rarity_icon': offer.rarity_icon,
'initiated_by_email': offer.initiated_by.user.email, 'initiated_by_email': offer.initiated_by.user.email,

View file

@ -75,13 +75,15 @@ class TradeOfferAllListView(ReusablePaginationMixin, ListView):
self.per_page = 10 self.per_page = 10
paginated_offers, pagination_context = self.paginate_data(queryset, page_number) paginated_offers, pagination_context = self.paginate_data(queryset, page_number)
context["offers"] = paginated_offers context["offers"] = paginated_offers
# Set pagination context using the key expected in the template
context["page_obj"] = pagination_context context["page_obj"] = pagination_context
# Add the expanded flag to the context based on the URL query parameter.
context["expanded"] = request.GET.get("expanded", "false").lower() == "true"
return context return context
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
if self.request.headers.get("X-Requested-With") == "XMLHttpRequest": if self.request.headers.get("X-Requested-With") == "XMLHttpRequest":
show_closed = self.request.GET.get("show_closed", "false").lower() == "true" show_closed = self.request.GET.get("show_closed", "false").lower() == "true"
expanded = self.request.GET.get("expanded", "false").lower() == "true"
queryset = TradeOffer.objects.all() queryset = TradeOffer.objects.all()
if show_closed: if show_closed:
queryset = queryset.filter(is_closed=True) queryset = queryset.filter(is_closed=True)
@ -94,7 +96,7 @@ class TradeOfferAllListView(ReusablePaginationMixin, ListView):
return render( return render(
self.request, self.request,
"trades/_trade_offer_list.html", "trades/_trade_offer_list.html",
{"offers": paginated_offers, "page_obj": pagination_context} {"offers": paginated_offers, "page_obj": pagination_context, "expanded": expanded}
) )
return super().render_to_response(context, **response_kwargs) return super().render_to_response(context, **response_kwargs)