Another attempt at getting the trade flipping and collapsing looking correct

This commit is contained in:
badblocks 2025-04-04 22:50:07 -07:00
parent 7c62c57433
commit 37d8bd5981
7 changed files with 386 additions and 233 deletions

View file

@ -124,61 +124,4 @@ const $$ = selector => Array.from(document.querySelector(selector));
resetCardMultiselectState();
}
});
// Expose tradeOfferCard globally if not already defined.
if (!window.tradeOfferCard) {
window.tradeOfferCard = function() {
return {
flipped: false,
show_back: false,
collapsed: false,
/*
* flipWithCollapse() now applies the height transition directly on the visible card face.
* It measures the current face (front or back) and the target face's height,
* then animates the current face's height change before toggling the flip.
*
* Make sure your template markup includes:
* - x-ref="front" on the front card face
* - x-ref="back" on the back card face
*/
flipWithCollapse() {
// Determine the currently visible face and the target face.
const currentFace = this.flipped ? this.$refs.back : this.$refs.front;
const targetFace = this.flipped ? this.$refs.front : this.$refs.back;
const container = this.$refs.container;
// Temporarily force target face to display to measure its height.
const originalHeight = targetFace.style.height;
const originalDisplay = targetFace.style.display;
const originalPosition = targetFace.style.position;
const originalVisibility = targetFace.style.visibility;
targetFace.style.height = 'auto';
targetFace.style.display = 'block';
targetFace.style.position = 'absolute';
targetFace.style.visibility = 'hidden';
const targetHeight = targetFace.offsetHeight + 18;
targetFace.style.height = originalHeight;
targetFace.style.display = originalDisplay;
targetFace.style.position = originalPosition;
targetFace.style.visibility = originalVisibility;
container.setAttribute('x-collapse.duration.500ms.min.' + targetHeight + 'px', '');
this.collapsed = true;
setTimeout(() => {
this.show_back = this.flipped;
this.flipped = !this.flipped;
setTimeout(() => {
this.show_back = !this.show_back;
}, 250)
setTimeout(() => {
this.collapsed = false;
setTimeout(() => {
container.removeAttribute('x-collapse.duration.500ms.min.' + targetHeight + 'px');
}, 550);
}, 550);
}, 550);
}
}
};
}
})();

View file

@ -107,9 +107,8 @@
{% if gravatar_profile %}
<div class="hovercard-profile mb-4 text-center">
<img src="{{ gravatar_profile.thumbnailUrl }}" alt="{{ gravatar_profile.displayName|default:request.user.username }}" class="rounded-full mb-2 mx-auto" />
<h3 class="text-xl mb-2">{{ gravatar_profile.displayName|default:request.user.username }}</h3>
<a href="https://gravatar.com/profile/" target="_blank" rel="noopener noreferrer" class="btn btn-primary">
Edit Profile on Gravatar
<a href="https://gravatar.com/profile/avatars" target="_blank" rel="noopener noreferrer" class="btn btn-primary mt-4">
Edit Avatar on Gravatar
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
@ -122,16 +121,16 @@
{% endwith %}
<div class="divider"></div>
<h2 class="text-base font-semibold pt-0">What is Gravatar?</h2>
<p class="mb-4 text-sm">Gravatar (Globally Recognized Avatar) is a free service that links your email address to a profile picture and, optionally, a profile. Many websites, including this one, use Gravatar to display your avatar and profile automatically.</p>
<p class="mb-4 text-sm">Gravatar (Globally Recognized Avatar) is a free service that links your email address to a profile picture. Many websites, including this one, use Gravatar to display your preferred avatar automatically.</p>
<h2 class="text-base font-semibold">How does it work?</h2>
<p class="mb-4 text-sm">If you've set up a Gravatar, your profile picture will appear here whenever you use your email on supported sites. When someone hovers over or clicks on your avatar, your Gravatar profile will also appear if you have one. If you don't have a Gravatar yet, you'll see a default image instead.</p>
<p class="mb-4 text-sm">If you've set up a Gravatar, your profile picture will appear here whenever you use your email on supported sites. If you don't have a Gravatar yet, you'll see a default image instead.</p>
<h2 class="text-base font-semibold">Is it safe? What about privacy?</h2>
<p class="mb-4 text-sm">Gravatar is completely optional, opt-in, and prioritizes your security and privacy. Your email is never shared and only a hashed version is sent to Gravatar, protecting your identity while ensuring that your email address is not exposed to bots or scrapers. Your personal data remains secure, and you maintain full control over your public profile.</p>
<p class="mb-4 text-sm">Gravatar is completely optional, opt-in, and prioritizes your security and privacy. Your email is never visible to anyone and only a hashed version is shown on the page and sent to Gravatar, protecting your identity while ensuring that your email address is not exposed to bots or scrapers.</p>
<h2 class="text-base font-semibold">Want to update or add a Gravatar?</h2>
<p class="mb-4 text-sm">Go to Gravatar.com to set up or change your avatar or profile. Your updates will appear here once saved!</p>
<p class="mb-4 text-sm">Go to Gravatar.com to set up or change your avatar. Your updates will appear here once saved!</p>
</div>
</div>

View file

@ -41,13 +41,6 @@
<script src="{% static 'js/floating-ui_core@1.6.9.9.min.js' %}"></script>
<script src="{% static 'js/floating-ui_dom@1.6.13.13.min.js' %}"></script>
<!-- Gravatar -->
<!-- Import the hovercard styles -->
<link rel="stylesheet" href="{% static 'css/hovercards.min.css' %}">
<!-- Import the hovercards library -->
<script src="{% static 'js/hovercards.min.js' %}"></script>
<script src="{% static 'js/base.js' %}"></script>
{% block css %}{% endblock %}
@ -151,29 +144,6 @@
<script defer src="{% static 'js/tooltip.js' %}"></script>
<!-- Gravatar -->
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof Gravatar !== 'undefined' && typeof Gravatar.Hovercards !== 'undefined') {
const hovercards = new Gravatar.Hovercards({
myHash: '{{ user.email|gravatar_hash }}',
});
hovercards.attach( document.body, { ignoreSelector: 'img[src*="gravatar.com/avatar/"].ignore' } );
}
});
// i18n: {
// 'Edit your profile →': 'Modifier votre profil →',
// 'View profile →': 'Voir le profil →',
// 'Contact': 'Contact',
// 'Send money': 'Envoyer de l\'argent',
// 'Sorry, we are unable to load this Gravatar profile.': 'Désolé, nous ne pouvons pas charger ce profil Gravatar.',
// 'Profile not found.': 'Profil non trouvé.',
// 'Too Many Requests.': 'Trop de demandes.',
// 'Internal Server Error.': 'Erreur interne du serveur.',
// }
</script>
{% block javascript %}{% endblock %}
</body>
</html>

View file

@ -96,7 +96,7 @@
<div id="featured-tab-contents">
<div class="tab-content" data-tab="featured-all">
{% if featured_offers.All %}
<div class="flex flex-col items-center gap-3 w-auto mx-auto">
<div class="flex flex-col items-center gap-6 w-auto mx-auto">
{% for offer in featured_offers.All %}
{% render_trade_offer offer %}
{% endfor %}
@ -145,7 +145,7 @@
<h5 class="text-xl font-semibold whitespace-nowrap truncate mb-0">Recent Offers</h5>
</div>
<div class="p-4">
<div class="flex flex-col items-center gap-3">
<div class="flex flex-col items-center gap-6">
{% for offer in recent_offers %}
{% render_trade_offer offer %}
{% empty %}

View file

@ -1,148 +1,131 @@
{% load gravatar card_badge cache %}
{% url 'trade_offer_detail' pk=offer_pk as trade_offer_detail_url %}
{% cache 60 trade_offer offer_pk %}
<div class="trade-offer-card m-2 h-full w-auto flex justify-center">
<div x-data="tradeOfferCard()" x-init="flipped = {{flipped|lower}}; show_back = {{flipped|lower}}" class="trade-offer-card my-auto">
<div {% if request_path != trade_offer_detail_url %}@click="window.location.href = '{{ trade_offer_detail_url }}'"{% endif %} class="no-underline block flip-container{% if request_path != trade_offer_detail_url %} cursor-pointer{% endif %}" style="perspective: 1000px;">
<div x-ref="container" x-show="!collapsed" class="overflow-hidden flip-inner grid grid-cols-1 grid-rows-1 card bg-base-100 card-border shadow-lg w-90 transform transition-transform duration-500 ease-in-out" :class="{'rotate-y-180': flipped}">
<!-- Front Face: Trade Offer -->
<!-- Using grid placement classes (col-start-1 row-start-1) ensures both faces overlap -->
<div x-ref="front" x-show="!show_back" x-cloak class="flip-face front col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between">
<!-- Header -->
<div class="flip-face-header self-start">
<!-- Set this container as relative to position the avatar absolutely -->
<div class="relative mt-6 mb-4 mx-2 sm:mx-4">
<!-- Two-column grid for the labels -->
<div class="grid grid-cols-2">
<span class="text-sm font-semibold text-center">Has</span>
<span class="text-sm font-semibold text-center">Wants</span>
</div>
<!-- The avatar is placed absolutely and centered -->
<div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center">
<div class="avatar">
<div class="w-10 rounded-full">
{{ initiated_by_email|gravatar:40 }}
</div>
</div>
</div>
<div x-data="{ flipped: {{flipped|lower}}, offerExpanded: true, acceptanceExpanded: false }" x-ref="tradeOffer" class="transition-all duration-500 trade-offer-card my-auto h-full w-auto justify-center">
<div class="flip-container">
<div{% if not on_detail_page %} @click="window.location.href = '{% url 'trade_offer_detail' pk=offer_pk %}'"{% endif %} class="flip-inner grid grid-cols-1 grid-rows-1 card bg-base-100 card-border shadow-lg w-90 transform transition-transform duration-500 ease-in-out{% if not on_detail_page %} cursor-pointer{% endif %}"
:class="{'rotate-y-180': flipped}">
<div class="flip-face front col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between">
<div class="flip-face-header self-start">
<div class="relative mt-6 mb-4 mx-2 sm:mx-4">
<div class="grid grid-cols-2 items-center">
<span class="text-sm font-semibold text-center">Has</span>
<span class="text-sm font-semibold text-center">Wants</span>
</div>
</div>
<!-- Main Trade Offer Row -->
<div class="flip-face-body self-start">
{% if not flipped %}
<div class="px-2 main-badges pb-0">
<!-- Normal mode: just use an outer grid with 2 columns -->
<div class="flex flex-row justify-around">
<!-- Has Side -->
<div class="flex flex-col">
{% for card in have_cards_available %}
<a href="{% url 'cards:card_detail' card.pk %}">
{% card_badge card.card card.quantity %}
</a>
{% endfor %}
</div>
<!-- Wants Side -->
<div class="flex flex-col">
{% for card in want_cards_available %}
<a href="{% url 'cards:card_detail' card.pk %}">
{% card_badge card.card card.quantity %}
</a>
{% endfor %}
</div>
</div>
</div>
{% else %}
<div class="flex justify-center mb-1">
<div class="text-sm">
All cards have been traded.
</div>
</div>
{% endif %}
</div>
<div class="flip-face-footer self-end">
<div class="flex justify-between px-2 pb-2">
<div class="text-gray-500 text-sm tooltip tooltip-right" data-tip="ID: {{ offer_hash }}">
<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="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
</div>
<div class="text-xs">{{ rarity_icon }}</div>
<div class="cursor-pointer text-gray-500 tooltip tooltip-left" data-tip="Flip to Accepted Trades" @click.stop.prevent="flipWithCollapse()">
<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" />
</svg>
</div>
</div>
</div>
</div>
<!-- Back Face: Acceptances View -->
<!-- Placed in the same grid cell as the front face -->
<div x-ref="back" x-show="show_back" x-cloak 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="relative mt-6 mb-4 mx-2 sm:mx-4">
<div class="grid grid-cols-2">
<span class="text-sm font-semibold text-center">Has</span>
<span class="text-sm font-semibold text-center">Wants</span>
</div>
<!-- The avatar is placed absolutely and centered -->
<div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center">
<div class="avatar">
<div class="w-10 rounded-full">
<div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center">
<div class="avatar tooltip tooltip-top" data-tip="{{ initiated_by_username }} | {{ initiated_reputation }}">
<div class="w-10 rounded-full">
{{ initiated_by_email|gravatar:40 }}
</div>
</div>
</div>
</div>
</div>
<div class="self-start">
<div class="px-2 pb-0">
<div class="space-y-3">
{% for acceptance in acceptances %}
<a href="{% url 'trade_acceptance_update' pk=acceptance.pk %}" class="no-underline block mb-2"
data-tooltip-html='<div class="flex items-center space-x-2">
<div class="avatar">
<div class="w-10 rounded-full">
{{ acceptance.accepted_by.user.email|gravatar:"40" }}
</div>
</div>
<div class="flex flex-col">
<span class="text-sm">Accepted by: {{ acceptance.accepted_by.user.username }}</span>
<span class="text-sm">State: {{ acceptance.state }}</span>
<span class="text-sm">Acceptance ID: {{ acceptance.hash }}</span>
</div>
</div>'>
<div class="grid grid-cols-2 justify-items-center">
{% card_badge acceptance.requested_card %}
{% card_badge acceptance.offered_card %}
<div class="flip-face-body self-start">
<div x-show="offerExpanded" x-collapse.duration.500ms class="px-2 badges">
<div class="flex flex-row justify-around">
{% if num_cards_available > 0 %}
<div class="flex flex-col">
{% for card in have_cards_available %}
<div class="cursor-pointer" @click.stop="window.location.href='{% url 'cards:card_detail' pk=card.card.pk %}'">{% card_badge card.card card.quantity %}</div>
{% endfor %}
</div>
</a>
{% empty %}
<div class="flex justify-center items-center">
<div class="text-sm">
No trades yet.
</div>
<div class="flex flex-col">
{% for card in want_cards_available %}
<div class="cursor-pointer" @click.stop="window.location.href='{% url 'cards:card_detail' pk=card.card.pk %}'">{% card_badge card.card card.quantity %}</div>
{% endfor %}
</div>
{% endfor %}
{% else %}
<div class="text-center text-sm mb-2">All cards traded!</div>
{% endif %}
</div>
</div>
</div>
<div class="flip-face-footer self-end">
<div class="flex justify-between px-2 pb-2">
<div class="text-gray-500 text-sm tooltip tooltip-right" data-tip="ID: {{ offer_hash }}">
<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="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
</div>
<div class="text-xs">{{ rarity_icon }}</div>
<div class="cursor-pointer text-gray-500"
@click.stop="$refs.tradeOffer.scrollIntoView({ behavior: 'auto', block: 'start' });
offerExpanded = false;
setTimeout(() => {
flipped = true;
setTimeout(() => { acceptanceExpanded = true; }, 500);
}, 500);">
<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" />
</svg>
</div>
</div>
</div>
<div class="flex justify-between px-2 pb-2 self-end items-center">
<!-- Back-to-front flip button -->
<div class="text-gray-500 cursor-pointer tooltip tooltip-right" data-tip="Flip to Offered Cards" @click.stop.prevent="flipWithCollapse()">
<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" />
</svg>
</div>
<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-header self-start">
<div class="relative mt-6 mb-4 mx-2 sm:mx-4">
<div class="grid grid-cols-2 items-center">
<span class="text-sm font-semibold text-center">Has</span>
<span class="text-sm font-semibold text-center">Wants</span>
</div>
<div class="text-xs">{{ rarity_icon }}</div>
<div class="px-1 text-center">
<span class="text-xs text-gray-500">
({{ acceptances|length }})
</span>
<div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center">
<div class="avatar">
<div class="w-10 rounded-full">
{{ initiated_by_email|gravatar:40 }}
</div>
</div>
</div>
</div>
</div>
<div class="flip-face-body self-start">
<div class="px-2">
<div x-show="acceptanceExpanded" x-collapse.duration.500ms class="space-y-3">
{% for acceptance in acceptances %}
<a href="{% url 'trade_acceptance_update' pk=acceptance.pk %}" class="no-underline"
data-tooltip-html='<div class="flex items-center space-x-2">
<div class="avatar">
<div class="w-10 rounded-full">
{{ acceptance.accepted_by.user.email|gravatar:"40" }}
</div>
</div>
<div class="flex flex-col">
<span class="text-sm">Accepted by: {{ acceptance.accepted_by.user.username }}</span>
<span class="text-sm">State: {{ acceptance.state }}</span>
<span class="text-sm">ID: {{ acceptance.hash }}</span>
</div>
</div>'>
<div class="grid grid-cols-2 gap-4 items-center">
<div>
{% card_badge acceptance.requested_card %}
</div>
<div>
{% card_badge acceptance.offered_card %}
</div>
</div>
</a>
{% empty %}
<div class="text-center text-sm mb-2">No trades yet!</div>
{% endfor %}
</div>
</div>
</div>
<div class="flip-face-footer self-end">
<div class="flex justify-between px-2 pb-2">
<div class="text-gray-500 cursor-pointer"
@click.stop="$refs.tradeOffer.scrollIntoView({ behavior: 'auto', block: 'start' }); acceptanceExpanded = false; setTimeout(() => { flipped = false; setTimeout(() => { offerExpanded = true; }, 500); }, 500);">
<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" />
</svg>
</div>
<div class="text-xs">{{ rarity_icon }}</div>
<div class="px-1 text-center text-gray-500">
<span class="text-sm font-semibold">
({{ acceptances|length }})
</span>
</div>
</div>
</div>
@ -151,20 +134,19 @@
</div>
</div>
<style>
/* Ensure proper 3D transformations on the rotating element */
.flip-container {
perspective: 1000px;
}
.flip-inner {
transform-style: preserve-3d;
}
/* Hide the back face of each card side */
.flip-face-back-hidden {
.flip-face {
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
/* The front face is unrotated by default */
.flip-face.front {
transform: rotateY(0);
}
/* The .rotate-y-180 class rotates the entire element by 180deg */
.rotate-y-180 {
transform: rotateY(180deg);
}

View file

@ -0,0 +1,260 @@
{% load gravatar card_badge cache %}
{% cache 60 trade_offer offer_pk %}
<div class="trade-offer-card m-2 h-full w-auto flex justify-center">
<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"
@toggle-all.window="setBadge($event.detail.expanded)">
<!-- Flip container providing perspective -->
<div class="flip-container" style="perspective: 1000px;">
<!--
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.
-->
<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}">
<!-- Front Face: Trade Offer -->
<!-- Using grid placement classes (col-start-1 row-start-1) ensures both faces overlap -->
<div class="flip-face front col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between">
<!-- Header -->
<div class="flip-face-header self-start">
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline block">
<!-- Set this container as relative to position the avatar absolutely -->
<div class="relative mt-6 mb-4 mx-2 sm:mx-4">
<!-- Two-column grid for the labels -->
<div class="grid grid-cols-2 items-center">
<span class="text-sm font-semibold text-center">Has</span>
<span class="text-sm font-semibold text-center">Wants</span>
</div>
<!-- The avatar is placed absolutely and centered -->
<div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center">
<div class="avatar">
<div class="w-10 rounded-full">
{{ initiated_by_email|gravatar:40 }}
</div>
</div>
</div>
</div>
</a>
</div>
<!-- Main Trade Offer Row -->
<div class="flip-face-body self-start">
{% if not flipped %}
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline block">
<div class="px-2 main-badges pb-0">
<!-- 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>
</div>
</a>
{% else %}
<div class="flex justify-center mt-8">
<div class="text-sm">
All cards have been accepted.
</div>
</div>
{% endif %}
<!-- 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">
<div class="flex flex-row gap-2 justify-around">
<!-- Has Side Extra Badges -->
<div class="flex flex-col gap-2">
{% for card in have_cards_available|slice:"1:" %}
{% card_badge card.card card.quantity %}
{% endfor %}
</div>
<!-- Wants Side Extra Badges -->
<div class="flex flex-col gap-2">
{% for card in want_cards_available|slice:"1:" %}
{% card_badge card.card card.quantity %}
{% endfor %}
</div>
</div>
</a>
</div>
</div>
{% if have_cards_available|length > 1 or want_cards_available|length > 1 %}
<div @click="badgeExpanded = !badgeExpanded" class="flex justify-center h-5 cursor-pointer">
<svg x-bind:class="{ 'rotate-180': badgeExpanded }"
class="transition-transform duration-500 h-5 w-5 cursor-pointer"
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="M19 9l-7 7-7-7" />
</svg>
</div>
{% else %}
<div class="h-5"></div>
{% endif %}
<div class="flip-face-footer self-end">
<div class="flex justify-between px-2 pb-2">
<div class="text-gray-500 text-sm tooltip tooltip-right" data-tip="ID: {{ offer_hash }}">
<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="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
</div>
<!-- Front-to-back flip button -->
<div class="cursor-pointer text-gray-500" @click="flipped = true; acceptanceExpanded = defaultExpanded">
<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" />
</svg>
</div>
</div>
</div>
</div>
<!-- Back Face: Acceptances View -->
<!-- Placed in the same grid cell as the front face -->
<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">
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline">
<div class="py-4 mx-2 sm:mx-4">
<div class="grid grid-cols-3 items-center">
<div class="flex justify-center items-center">
<span class="text-sm font-semibold">Has</span>
</div>
<div class="flex justify-center items-center">
<div class="avatar">
<div class="w-10 rounded-full">
{{ initiated_by_email|gravatar:40 }}
</div>
</div>
</div>
<div class="flex justify-center items-center">
<span class="text-sm font-semibold">Wants</span>
</div>
</div>
</div>
</a>
</div>
<div class="self-start">
<div class="px-2 pb-0">
<div class="overflow-hidden">
{% if acceptances.0 %}
<div class="space-y-3">
{% with acceptance=acceptances.0 %}
<a href="{% url 'trade_acceptance_update' pk=acceptance.pk %}" class="no-underline"
data-tooltip-html='<div class="flex items-center space-x-2">
<div class="avatar">
<div class="w-10 rounded-full">
{{ acceptance.accepted_by.user.email|gravatar:"40" }}
</div>
</div>
<div class="flex flex-col">
<span class="text-sm">Accepted by: {{ acceptance.accepted_by.user.username }}</span>
<span class="text-sm">State: {{ acceptance.state }}</span>
<span class="text-sm">Acceptance ID: {{ acceptance.hash }}</span>
</div>
</div>'>
<div class="grid grid-cols-2 gap-4 items-center">
<div>
{% card_badge acceptance.requested_card %}
</div>
<div>
{% card_badge acceptance.offered_card %}
</div>
</div>
</a>
{% endwith %}
</div>
{% endif %}
</div>
<div x-show="acceptanceExpanded" x-collapse.duration.500ms class="space-y-3">
{% for acceptance in acceptances|slice:"1:" %}
<a href="{% url 'trade_acceptance_update' pk=acceptance.pk %}" class="no-underline"
data-tooltip-html='<div class="flex items-center space-x-2">
<div class="avatar">
<div class="w-10 rounded-full">
{{ acceptance.accepted_by.user.email|gravatar:"40" }}
</div>
</div>
<div class="flex flex-col">
<span class="text-sm">Accepted by: {{ acceptance.accepted_by.user.username }}</span>
<span class="text-sm">State: {{ acceptance.state }}</span>
<span class="text-sm">Acceptance ID: {{ acceptance.hash }}</span>
</div>
</div>'>
<div class="grid grid-cols-2 gap-4 items-center">
<div>
{% card_badge acceptance.requested_card %}
</div>
<div>
{% card_badge acceptance.offered_card %}
</div>
</div>
</a>
{% endfor %}
</div>
</div>
<div class="flex justify-center h-5">
{% if acceptances|length > 1 %}
<svg @click="acceptanceExpanded = !acceptanceExpanded"
x-bind:class="{ 'rotate-180': acceptanceExpanded }"
class="transition-transform duration-500 h-5 w-5 cursor-pointer"
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="M19 9l-7 7-7-7" />
</svg>
{% endif %}
</div>
</div>
<div class="flex justify-between px-2 pb-2 self-end">
<!-- Back-to-front flip button -->
<div class="text-gray-500 cursor-pointer" @click="flipped = false; badgeExpanded = defaultExpanded">
<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" />
</svg>
</div>
<div class="px-1 text-center">
<span class="text-sm font-semibold">
Acceptances ({{ acceptances|length }})
</span>
</div>
<div class="text-gray-500 text-sm tooltip tooltip-left" data-tip="ID: {{ offer_hash }}">
<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="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
/* Ensure proper 3D transformations on the rotating element */
.flip-inner {
transform-style: preserve-3d;
}
/* Hide the back face of each card side */
.flip-face {
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
/* The front face is unrotated by default */
.flip-face.front {
transform: rotateY(0);
}
/* The .rotate-y-180 class rotates the entire element by 180deg */
.rotate-y-180 {
transform: rotateY(180deg);
}
</style>
{% endcache %}

View file

@ -38,13 +38,12 @@ def render_trade_offer(context, offer):
'rarity_icon': offer.rarity_icon,
'initiated_by_email': offer.initiated_by.user.email,
'initiated_by_username': offer.initiated_by.user.username,
'initiated_reputation': offer.initiated_by.user.reputation_score,
'acceptances': acceptances,
'have_cards_available': have_cards_available,
'want_cards_available': want_cards_available,
'in_game_name': offer.initiated_by.in_game_name,
'friend_code': offer.initiated_by.friend_code,
'num_cards_available': len(have_cards_available) + len(want_cards_available),
'request_path': context.get("request").path,
'on_detail_page': context.get("request").path.endswith("trades/"+str(offer.pk)+"/"),
}
@register.inclusion_tag('templatetags/trade_acceptance.html', takes_context=True)