use <a> tags for card_badge and trade_offer clickable areas (except for main card_badge row on trade_offers, still uses @click for now because the a tag can't wrap that content for some reason). closes #14

This commit is contained in:
badblocks 2025-04-15 00:15:08 -07:00
parent 86b061c971
commit afaa392b2f
22 changed files with 247 additions and 227 deletions

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-04-14 04:07 # Generated by Django 5.1.2 on 2025-04-14 20:58
import accounts.models import accounts.models
import django.contrib.auth.models import django.contrib.auth.models

View file

@ -20,7 +20,7 @@ class CustomUser(AbstractUser):
enable_email_notifications = models.BooleanField( enable_email_notifications = models.BooleanField(
default=True, default=True,
verbose_name="Enable Email Notifications", verbose_name="Enable Email Notifications",
help_text="Receive new trade notifications via email." help_text="Receive trade notifications via email."
) )
reputation_score = models.IntegerField(default=0) reputation_score = models.IntegerField(default=0)

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-04-14 04:07 # Generated by Django 5.1.2 on 2025-04-14 20:58
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

View file

@ -22,8 +22,8 @@ class HomePageView(TemplateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
try: try:
# Get all cards ordered by name # Get all cards ordered by name, exclude cards with rarity level > 5
context["cards"] = Card.objects.all().order_by("name") context["cards"] = Card.objects.filter(rarity_level__lte=5).order_by("name", "rarity_level")
# Reuse base trade offer queryset for market stats # Reuse base trade offer queryset for market stats
base_offer_qs = TradeOffer.objects.filter(is_closed=False) base_offer_qs = TradeOffer.objects.filter(is_closed=False)
@ -39,7 +39,7 @@ class HomePageView(TemplateView):
# Most Offered Cards # Most Offered Cards
try: try:
context["most_offered_cards"] = ( context["most_offered_cards"] = (
Card.objects.filter(tradeofferhavecard__isnull=False) Card.objects.filter(tradeofferhavecard__isnull=False).filter(rarity_level__lte=5)
.annotate(offer_count=Sum("tradeofferhavecard__quantity")) .annotate(offer_count=Sum("tradeofferhavecard__quantity"))
.order_by("-offer_count")[:6] .order_by("-offer_count")[:6]
) )
@ -50,7 +50,7 @@ class HomePageView(TemplateView):
# Most Wanted Cards # Most Wanted Cards
try: try:
context["most_wanted_cards"] = ( context["most_wanted_cards"] = (
Card.objects.filter(tradeofferwantcard__isnull=False) Card.objects.filter(tradeofferwantcard__isnull=False).filter(rarity_level__lte=5)
.annotate(offer_count=Sum("tradeofferwantcard__quantity")) .annotate(offer_count=Sum("tradeofferwantcard__quantity"))
.order_by("-offer_count")[:6] .order_by("-offer_count")[:6]
) )
@ -61,7 +61,7 @@ class HomePageView(TemplateView):
# Least Offered Cards # Least Offered Cards
try: try:
context["least_offered_cards"] = ( context["least_offered_cards"] = (
Card.objects.annotate( Card.objects.filter(rarity_level__lte=5).annotate(
offer_count=Coalesce(Sum("tradeofferhavecard__quantity"), 0) offer_count=Coalesce(Sum("tradeofferhavecard__quantity"), 0)
) )
.order_by("offer_count")[:6] .order_by("offer_count")[:6]

View file

@ -23,7 +23,9 @@
* *
*/ */
@theme {} @theme {
--breakpoint-xs: 24rem;
}
/* /*

View file

@ -12,19 +12,19 @@ module.exports = {
*/ */
/* Templates within theme app (<tailwind_app_name>/templates), e.g. base.html. */ /* Templates within theme app (<tailwind_app_name>/templates), e.g. base.html. */
'../templates/**/*.html', "../templates/**/*.html",
/* /*
* Main templates directory of the project (BASE_DIR/templates). * Main templates directory of the project (BASE_DIR/templates).
* Adjust the following line to match your project structure. * Adjust the following line to match your project structure.
*/ */
'../../templates/**/*.html', "../../templates/**/*.html",
/* /*
* Templates in other django apps (BASE_DIR/<any_app_name>/templates). * Templates in other django apps (BASE_DIR/<any_app_name>/templates).
* Adjust the following line to match your project structure. * Adjust the following line to match your project structure.
*/ */
'../../**/templates/**/*.html', "../../**/templates/**/*.html",
/** /**
* JS: If you use Tailwind CSS in JavaScript, uncomment the following lines and make sure * JS: If you use Tailwind CSS in JavaScript, uncomment the following lines and make sure
@ -42,19 +42,19 @@ module.exports = {
// '../../**/*.py' // '../../**/*.py'
], ],
safelist: [ safelist: [
'alert-info', "alert-info",
'alert-success', "alert-success",
'alert-warning', "alert-warning",
'alert-error', "alert-error",
'btn-info', "btn-info",
'btn-success', "btn-success",
'btn-warning', "btn-warning",
'btn-error', "btn-error",
'bg-info', "bg-info",
'bg-success', "bg-success",
'bg-warning', "bg-warning",
'bg-error', "bg-error",
'text-gray-700' "text-gray-700",
], ],
theme: { theme: {
extend: {}, extend: {},
@ -65,9 +65,9 @@ module.exports = {
* for forms. If you don't like it or have own styling for forms, * for forms. If you don't like it or have own styling for forms,
* comment the line below to disable '@tailwindcss/forms'. * comment the line below to disable '@tailwindcss/forms'.
*/ */
require('@tailwindcss/forms'), require("@tailwindcss/forms"),
require('@tailwindcss/typography'), require("@tailwindcss/typography"),
require('@tailwindcss/aspect-ratio'), require("@tailwindcss/aspect-ratio"),
], ],
darkMode: 'class', darkMode: "class",
} };

View file

@ -106,8 +106,8 @@
{% with gravatar_profile=request.user.email|gravatar_profile_data %} {% with gravatar_profile=request.user.email|gravatar_profile_data %}
<div class="hovercard-profile mb-4 text-center"> <div class="hovercard-profile mb-4 text-center">
<div class="avatar block mx-auto w-32"> <div class="avatar block mx-auto max-w-32">
<div class="W-32 rounded-full"> <div class="rounded-full">
{{ request.user.email|gravatar:128 }} {{ request.user.email|gravatar:128 }}
</div> </div>
</div> </div>

View file

@ -79,13 +79,13 @@
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="hidden sm:block"> <div class="hidden sm:block">
<div class="btn btn-ghost btn-circle avatar"> <div class="btn btn-ghost btn-circle avatar">
<div class="w-10 rounded-full"> <div class="max-w-10 rounded-full">
<a tabindex="0" role="button" href="{% url 'dashboard' %}">{{ user.email|gravatar_no_hover:40 }}</a> <a tabindex="0" role="button" href="{% url 'dashboard' %}">{{ user.email|gravatar_no_hover:40 }}</a>
</div> </div>
</div> </div>
</div> </div>
{% else %} {% else %}
<div class="flex gap-2"> <div class="gap-2 hidden sm:flex">
<a tabindex="0" role="button" class="btn btn-primary" href="{% url 'account_login' %}">Login</a> <a tabindex="0" role="button" class="btn btn-primary" href="{% url 'account_login' %}">Login</a>
<a tabindex="1" role="button" class="btn btn-secondary" href="{% url 'account_signup' %}">Sign Up</a> <a tabindex="1" role="button" class="btn btn-secondary" href="{% url 'account_signup' %}">Sign Up</a>
</div> </div>
@ -94,7 +94,7 @@
</div> </div>
<!-- Main Content --> <!-- Main Content -->
<main class="container mx-auto p-4 w-full xl:w-256"> <main class="container mx-auto p-4 w-full xl:max-w-256">
{% include '_messages.html' %} {% include '_messages.html' %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
@ -121,7 +121,7 @@
<span class="dock-label">Trades</span> <span class="dock-label">Trades</span>
</button> </button>
<button @click="window.location.href = '{{ dashboard_url }}'" class="{% if request.path == dashboard_url or request.path == settings_url %}dock-active{% endif %}"> <button @click="window.location.href = '{{ dashboard_url }}'" class="{% if request.path == dashboard_url or request.path == settings_url %}dock-active{% endif %}">
{% if user.is_authenticated %}<div tabindex="0" role="button" class="avatar"><div class="w-6 rounded-full">{{ user.email|gravatar_no_hover:40 }}</div></div>{% else %}<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor" stroke-linejoin="miter" stroke-linecap="butt"><circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></circle><path d="m22,13.25v-2.5l-2.318-.966c-.167-.581-.395-1.135-.682-1.654l.954-2.318-1.768-1.768-2.318.954c-.518-.287-1.073-.515-1.654-.682l-.966-2.318h-2.5l-.966,2.318c-.581.167-1.135.395-1.654.682l-2.318-.954-1.768,1.768.954,2.318c-.287.518-.515,1.073-.682,1.654l-2.318.966v2.5l2.318.966c.167.581.395,1.135.682,1.654l-.954,2.318,1.768,1.768,2.318-.954c.518.287,1.073.515,1.654.682l.966,2.318h2.5l.966-2.318c.581-.167,1.135-.395,1.654-.682l2.318.954,1.768-1.768-.954-2.318c.287-.518.515-1.073.682-1.654l2.318-.966Z" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></path></g></svg>{% endif %} {% if user.is_authenticated %}<div tabindex="0" role="button" class="avatar"><div class="max-w-6 rounded-full">{{ user.email|gravatar_no_hover:40 }}</div></div>{% else %}<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor" stroke-linejoin="miter" stroke-linecap="butt"><circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></circle><path d="m22,13.25v-2.5l-2.318-.966c-.167-.581-.395-1.135-.682-1.654l.954-2.318-1.768-1.768-2.318.954c-.518-.287-1.073-.515-1.654-.682l-.966-2.318h-2.5l-.966,2.318c-.581.167-1.135.395-1.654.682l-2.318-.954-1.768,1.768.954,2.318c-.287.518-.515,1.073-.682,1.654l-2.318.966v2.5l2.318.966c.167.581.395,1.135.682,1.654l-.954,2.318,1.768,1.768,2.318-.954c.518.287,1.073.515,1.654.682l.966,2.318h2.5l.966-2.318c.581-.167,1.135-.395,1.654-.682l2.318.954,1.768-1.768-.954-2.318c.287-.518.515-1.073.682-1.654l2.318-.966Z" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></path></g></svg>{% endif %}
<span class="dock-label">Dashboard</span> <span class="dock-label">Dashboard</span>
</button> </button>
</div> </div>

View file

@ -38,7 +38,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</div> </div>
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-1 w-26 p-2 shadow-sm"> <ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-1 max-w-26 p-2 shadow-sm">
<li> <li>
<a href="#" @click.prevent="order = 'newest'; page = 1; loadOffers()"> <a href="#" @click.prevent="order = 'newest'; page = 1; loadOffers()">
Newest Newest
@ -81,7 +81,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</div> </div>
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-1 w-26 p-2 shadow-sm"> <ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-1 max-w-26 p-2 shadow-sm">
<li> <li>
<a href="#" @click.prevent="order = 'newest'; page = 1; loadOffers()"> <a href="#" @click.prevent="order = 'newest'; page = 1; loadOffers()">
Newest Newest

View file

@ -32,7 +32,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</div> </div>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52"> <ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box max-w-52">
<li><a href="#" @click.prevent="order = 'absolute'; page = 1; loadCards()">Cardset</a></li> <li><a href="#" @click.prevent="order = 'absolute'; page = 1; loadCards()">Cardset</a></li>
<li><a href="#" @click.prevent="order = 'alphabetical'; page = 1; loadCards()">Alphabetical</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> <li><a href="#" @click.prevent="order = 'rarity'; page = 1; loadCards()">Rarity</a></li>
@ -46,7 +46,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</div> </div>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52"> <ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box max-w-52">
<li><a href="#" @click.prevent="groupBy = 'none'; page = 1; loadCards()">None</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 = '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 = 'cardset'; page = 1; loadCards()">Cardset</a></li>

View file

@ -4,7 +4,6 @@
{% block title %}Edit Friend Code{% endblock %} {% block title %}Edit Friend Code{% endblock %}
{% block content %} {% block content %}
<div class="container mx-auto max-w-3xl mt-6">
<h1 class="text-3xl font-bold mb-4">Edit Friend Code</h1> <h1 class="text-3xl font-bold mb-4">Edit Friend Code</h1>
<!-- Display the friend code as plain text --> <!-- Display the friend code as plain text -->
@ -46,5 +45,4 @@
<button type="submit" form="edit-friendcode-form" class="btn btn-primary">Update</button> <button type="submit" form="edit-friendcode-form" class="btn btn-primary">Update</button>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View file

@ -4,11 +4,10 @@
{% block title %}{{title}}{% endblock title %} {% block title %}{{title}}{% endblock title %}
{% block content %} {% block content %}
<div class="container mx-auto mt-6">
<h2 class="text-2xl font-bold">Trade Offer Details</h2> <h2 class="text-2xl font-bold">Trade Offer Details</h2>
<div class="card card-border bg-base-100 shadow-lg mx-auto p-6 m-4 text-sm"> <div class="card card-border bg-base-100 shadow-lg mx-auto px-2 py-4 m-2 text-sm">
{% if acceptance_form %} {% if acceptance_form %}
<div class="col-span-2 w-3/4 mx-auto card card-border bg-base-200 shadow-lg p-6 mb-8"> <div class="col-span-2 max-w-3/4 mx-auto card card-border bg-base-200 shadow-lg p-6 mb-8">
<h3 class="text-xl font-semibold mb-2">Accept A Trade</h3> <h3 class="text-xl font-semibold mb-2">Accept A Trade</h3>
<form method="post" action="{% url 'trade_acceptance_create' offer_pk=object.pk %}"> <form method="post" action="{% url 'trade_acceptance_create' offer_pk=object.pk %}">
{% csrf_token %} {% csrf_token %}
@ -53,15 +52,15 @@
</div> </div>
{% endif %} {% endif %}
<div class="grid grid-cols-2 justify-items-end items-baseline gap-4"> <div class="grid grid-cols-2 justify-items-end items-baseline gap-4">
<div class="font-semibold text-right text-lg">Cards They Have: <div class="font-semibold text-right xs:text-lg">Cards They Have:
<div class="flex flex-wrap flex-col gap-2 text-left mt-4"> <div class="flex flex-wrap flex-col gap-2 text-left mt-4 items-center">
{% for card in object.have_cards_available %} {% for card in object.have_cards_available %}
{% card_badge card.card card.qty_available %} {% card_badge card.card card.qty_available %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="font-semibold justify-self-start text-lg">Cards They Want: <div class="font-semibold justify-self-start xs:text-lg">Cards They Want:
<div class="flex flex-wrap flex-col gap-2 mt-4"> <div class="flex flex-wrap flex-col gap-2 mt-4 items-center">
{% for card in object.want_cards_available %} {% for card in object.want_cards_available %}
{% card_badge card.card card.qty_available %} {% card_badge card.card card.qty_available %}
{% endfor %} {% endfor %}
@ -80,18 +79,17 @@
<div class="justify-self-start">{{ object.updated_at }}</div> <div class="justify-self-start">{{ object.updated_at }}</div>
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<div class="font-semibold text-center text-lg mb-2">Trade History:</div> <div class="font-semibold text-center sm:text-lg mb-2">Trade History:</div>
<div class="flex flex-row flex-wrap justify-start items-baseline w-90 lg:w-181 mx-auto"> <div class="flex flex-row flex-wrap justify-start items-baseline gap-y-2 w-65 xs:w-88 sm:w-90 lg:w-181 mx-auto">
{% for trade in object.acceptances.all %} {% for trade in object.acceptances.all %}
<div class="p-2 grid grid-cols-2 justify-items-center items-baseline gap-1 w-90"> <div class="grid grid-cols-2 justify-items-between items-baseline w-65 xs:w-88 sm:w-90 lg:w-181">
<div class="">{% card_badge trade.offered_card %}</div> <div class="">{% card_badge trade.offered_card %}</div>
<div class="">{% card_badge trade.requested_card %}</div> <div class="">{% card_badge trade.requested_card %}</div>
<div class="col-span-2 text-center">{{ trade.accepted_by.user.username }} &bull; <a class="link link-hover" href="{% url 'trade_acceptance_update' pk=trade.pk %}">#{{ trade.hash }}</a> &bull; {{ trade.get_state_display }}</div> <div class="col-span-2 text-center mt-1">{{ trade.accepted_by.user.username }} &bull; <a class="link link-hover" href="{% url 'trade_acceptance_update' pk=trade.pk %}">#{{ trade.hash }}</a> &bull; {{ trade.get_state_display }}</div>
</div> </div>
{% empty %} {% empty %}
<div class="text-center justify-center w-full text-center">No trades yet.</div> <div class="text-center justify-center w-full text-center">No trades yet.</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div>
{% endblock content %} {% endblock content %}

View file

@ -10,10 +10,10 @@
{% csrf_token %} {% csrf_token %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
{% card_multiselect "have_cards" "I Have:" "Select zero or more cards..." cards have_cards %} {% card_multiselect "have_cards" "I Have:" "Select some cards..." cards have_cards %}
</div> </div>
<div> <div>
{% card_multiselect "want_cards" "I Want:" "Select zero or more cards..." cards want_cards %} {% card_multiselect "want_cards" "I Want:" "Select some cards..." cards want_cards %}
</div> </div>
</div> </div>
<div class="flex flex-col md:flex-row gap-4"> <div class="flex flex-col md:flex-row gap-4">

View file

@ -1,8 +1,8 @@
<a href="{{ url }}" @click.stop> <a href="{{ url }}" @click.stop>
<div class="relative block"> <div class="relative block">
{% if not expanded %} {% if not expanded %}
<div class="flex flex-row items-center h-[32px] p-1.5 w-40 text-white shadow-lg" style="{{ style }}"> <div class="flex flex-row items-center h-[32px] p-1.5 w-30 sm:w-40 text-white shadow-lg" style="{{ style }}">
<div class="grow"><div class="truncate text-ellipsis font-semibold leading-tight text-sm marquee-calc max-w-24">{{ name }}</div></div> <div class="grow"><div class="truncate text-ellipsis font-semibold leading-tight text-sm marquee-calc w-4/5 sm:w-24">{{ name }}</div></div>
<div class="grow-0 shrink-0 text-right truncate font-semibold leading-tight text-sm">{{ cardset }}</div> <div class="grow-0 shrink-0 text-right truncate font-semibold leading-tight text-sm">{{ cardset }}</div>
{% if quantity != None %} {% if quantity != None %}
<div class="grow-0 shrink-0 relative w-fit ps-1"> <div class="grow-0 shrink-0 relative w-fit ps-1">
@ -11,7 +11,7 @@
{% endif %} {% endif %}
</div> </div>
{% else %} {% else %}
<div class="grid grid-rows-2 grid-cols-4 h-[52px] p-1.5 w-40 text-white shadow-lg" style="{{ style }}"> <div class="grid grid-rows-2 grid-cols-4 h-[52px] p-1.5 w-30 sm:w-40 text-white shadow-lg" style="{{ style }}">
<div class="row-start-1 col-start-1 {% if quantity != None %}col-span-3{% else %}col-span-4{% endif %} self-start leading-tight truncate text-ellipsis"><span class="font-semibold text-sm marquee-calc">{{ name }}</span></div> <div class="row-start-1 col-start-1 {% if quantity != None %}col-span-3{% else %}col-span-4{% endif %} self-start leading-tight truncate text-ellipsis"><span class="font-semibold text-sm marquee-calc">{{ name }}</span></div>
{% if quantity != None %} {% if quantity != None %}
<div class="row-start-1 col-start-4 col-span-1 self-start ms-auto leading-tight relative w-fit ps-1"> <div class="row-start-1 col-start-4 col-span-1 self-start ms-auto leading-tight relative w-fit ps-1">

View file

@ -1,14 +1,14 @@
{% load gravatar card_badge %} {% load gravatar card_badge %}
<div class="card card-border bg-base-100 shadow-lg w-90 mx-auto"> <div class="card card-border bg-base-100 shadow-lg max-w-90 mx-auto">
<div class="cursor-pointer" @click.stop="window.location.href='{% url 'trade_acceptance_update' pk=acceptance.pk %}'">
<!-- Header --> <!-- Header -->
<a href="{% url 'trade_acceptance_update' pk=acceptance.pk %}">
<div class="my-4 mx-4"> <div class="my-4 mx-4">
<div class="flex justify-start items-center"> <div class="flex justify-start items-center">
<!-- Left: Initiator's avatar and "Has" --> <!-- Left: Initiator's avatar and "Has" -->
<div class="flex items-center"> <div class="flex items-center">
<div class="avatar me-2 tooltip tooltip-top cursor-default" @click.stop.prevent data-tip="{{ acceptance.trade_offer.initiated_by.user.username }} | {{ acceptance.trade_offer.initiated_by.user.reputation_score }} rep"> <div class="avatar me-2 tooltip tooltip-top cursor-default" @click.stop.prevent data-tip="{{ acceptance.trade_offer.initiated_by.user.username }} | {{ acceptance.trade_offer.initiated_by.user.reputation_score }} rep">
<div class="w-10 rounded-full"> <div class="max-w-10 rounded-full">
{{ acceptance.trade_offer.initiated_by.user.email|gravatar:40 }} {{ acceptance.trade_offer.initiated_by.user.email|gravatar:40 }}
</div> </div>
</div> </div>
@ -22,6 +22,7 @@
</div> </div>
</div> </div>
</div> </div>
</a>
<!-- Main Card Row: Single row with the acceptance's cards --> <!-- Main Card Row: Single row with the acceptance's cards -->
<div class="px-2 pb-0"> <div class="px-2 pb-0">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
@ -33,6 +34,7 @@
</div> </div>
</div> </div>
</div> </div>
<a href="{% url 'trade_acceptance_update' pk=acceptance.pk %}">
<div class="my-4 mx-4"> <div class="my-4 mx-4">
<div class="flex justify-end items-center"> <div class="flex justify-end items-center">
<!-- Right: "Wants" with the acceptor's avatar --> <!-- Right: "Wants" with the acceptor's avatar -->
@ -45,12 +47,12 @@
{% endif %} {% endif %}
</div> </div>
<div class="avatar ms-2 tooltip tooltip-bottom cursor-default" @click.stop.prevent data-tip="{{ acceptance.accepted_by.user.username}} | {{ acceptance.accepted_by.user.reputation_score }} rep"> <div class="avatar ms-2 tooltip tooltip-bottom cursor-default" @click.stop.prevent data-tip="{{ acceptance.accepted_by.user.username}} | {{ acceptance.accepted_by.user.reputation_score }} rep">
<div class="w-10 rounded-full"> <div class="max-w-10 rounded-full">
{{ acceptance.accepted_by.user.email|gravatar:40 }} {{ acceptance.accepted_by.user.email|gravatar:40 }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </a>
</div> </div>

View file

@ -3,7 +3,7 @@
{% cache 60 trade_offer offer_pk %} {% cache 60 trade_offer offer_pk %}
<div x-data="{ flipped: {{flipped|lower}}, offerExpanded: {{flipped|yesno:'false,true'}}, acceptanceExpanded: {{flipped|lower}} }" x-ref="tradeOffer" class="transition-all duration-500 trade-offer-card"> <div x-data="{ flipped: {{flipped|lower}}, offerExpanded: {{flipped|yesno:'false,true'}}, acceptanceExpanded: {{flipped|lower}} }" x-ref="tradeOffer" class="transition-all duration-500 trade-offer-card">
<div class="flip-container"> <div class="flip-container">
<div 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 flipped %} rotate-y-180{% endif %}" <div class="flip-inner grid grid-cols-1 grid-rows-1 card bg-base-100 card-border shadow-lg max-w-90 transform transition-transform duration-500 ease-in-out{%if flipped %} rotate-y-180{% endif %}"
:class="{'rotate-y-180': flipped}"> :class="{'rotate-y-180': flipped}">
<div class="flip-face front rotate-y-0 col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between"> <div class="flip-face front rotate-y-0 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="flip-face-header self-start">
@ -15,7 +15,7 @@
</div> </div>
<div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center"> <div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center">
<div class="avatar tooltip tooltip-top cursor-default" @click.stop.prevent data-tip="{{ initiated_by_username }} | {{ initiated_reputation }} rep"> <div class="avatar tooltip tooltip-top cursor-default" @click.stop.prevent data-tip="{{ initiated_by_username }} | {{ initiated_reputation }} rep">
<div class="w-10 rounded-full"> <div class="max-w-10 rounded-full">
{{ initiated_by_email|gravatar:40 }} {{ initiated_by_email|gravatar:40 }}
</div> </div>
</div> </div>
@ -79,7 +79,7 @@
</div> </div>
<div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center"> <div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center">
<div class="avatar tooltip tooltip-top cursor-default" @click.stop.prevent data-tip="{{ initiated_by_username }} | {{ initiated_reputation }} rep"> <div class="avatar tooltip tooltip-top cursor-default" @click.stop.prevent data-tip="{{ initiated_by_username }} | {{ initiated_reputation }} rep">
<div class="w-10 rounded-full"> <div class="max-w-10 rounded-full">
{{ initiated_by_email|gravatar:40 }} {{ initiated_by_email|gravatar:40 }}
</div> </div>
</div> </div>
@ -88,16 +88,16 @@
</a> </a>
</div> </div>
<div class="flip-face-body self-start"> <div class="flip-face-body self-start">
<div class="px-2 space-y-3 text-xs"> <div class="text-xs">
<div x-show="acceptanceExpanded" x-collapse.duration.500ms class="flex flex-row flex-wrap justify-start items-baseline w-90 lg:w-181 mx-auto"> <div x-show="acceptanceExpanded" x-collapse.duration.500ms class="flex flex-row flex-wrap justify-center items-baseline mx-auto">
{% for trade in acceptances %} {% for trade in acceptances %}
<div class="p-2 grid grid-cols-2 justify-items-center items-baseline gap-1"> <div class="mb-2 grid grid-cols-2 justify-items-center items-baseline gap-x-3 gap-y-1">
<div class="">{% card_badge trade.offered_card %}</div> <div class="">{% card_badge trade.offered_card %}</div>
<div class="">{% card_badge trade.requested_card %}</div> <div class="">{% card_badge trade.requested_card %}</div>
<div class="col-span-2 text-center">{{ trade.accepted_by.user.username }} &bull; <a class="link link-hover" href="{% url 'trade_acceptance_update' pk=trade.pk %}">#{{ trade.hash }}</a> &bull; {{ trade.get_state_display }}</div> <div class="col-span-2 text-center tooltip tooltip-bottom" data-tip="{{ trade.accepted_by.user.username }} | {{ trade.accepted_by.user.reputation_score }} rep">{{ trade.accepted_by.user.username }} &bull; <a class="link link-hover" href="{% url 'trade_acceptance_update' pk=trade.pk %}">#{{ trade.hash }}</a> &bull; {{ trade.get_state_display }}</div>
</div> </div>
{% empty %} {% empty %}
<div class="text-center justify-center w-full text-center">No trades yet.</div> <div class="w-full text-center">No trades yet.</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View file

@ -1,51 +1,63 @@
{% load gravatar card_badge tailwind_tags %}<!DOCTYPE html> {% load gravatar card_badge tailwind_tags %}<!DOCTYPE html>
<html style="background-color: transparent !important;"> <html style="background-color: transparent !important">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<style> <style>
{% include 'static/css/dist/styles.css' %} {% include 'static/css/dist/styles.css' %}
</style> </style>
</head> </head>
<body style="background-color: transparent !important;"> <body style="background-color: transparent !important">
<div class="trade-offer-card-screenshot p-4 h-full w-auto flex justify-center" style="background-color: transparent !important;"> <div
class="trade-offer-card-screenshot p-4 h-full w-auto flex justify-center"
style="background-color: transparent !important"
>
<div class="transition-all duration-500 trade-offer-card my-auto"> <div class="transition-all duration-500 trade-offer-card my-auto">
<!-- Flip container providing perspective --> <!-- Flip container providing perspective -->
<div class="flip-container"> <div class="flip-container">
<div class="flip-inner grid grid-cols-1 grid-rows-1 card bg-base-100 card-border shadow-lg {% if expanded %}w-180{% else %}w-96{% endif %} transform transition-transform duration-700 ease-in-out"> <div
class="flip-inner grid grid-cols-1 grid-rows-1 card bg-base-100 card-border shadow-lg {% if expanded %}w-180{% else %}w-96{% endif %} transform transition-transform duration-700 ease-in-out"
>
<!-- Front Face: Trade Offer --> <!-- Front Face: Trade Offer -->
<div class="flip-face front mb-2 col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between"> <div
class="flip-face front mb-2 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">
<div class="mt-6 mb-4 mx-2 sm:mx-4"> <div class="mt-6 mb-4 mx-2 sm:mx-4">
<!-- Two-column grid for the labels --> <!-- Two-column grid for the labels -->
<div class="flex flex-row justify-around w-full relative"> <div class="flex flex-row justify-around w-full relative">
<div class="text-sm font-semibold text-center ms-3">Has</div> <div class="text-sm font-semibold text-center ms-2">
<div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center"> Has
</div>
<div
class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center"
>
<div class="avatar"> <div class="avatar">
<div class="w-10 rounded-full"> <div class="w-10 rounded-full">
{{ initiated_by_email|gravatar:40 }} {{ initiated_by_email|gravatar:40 }}
</div> </div>
</div> </div>
</div> </div>
<div class="text-sm font-semibold text-center {% if expanded %}ms-8{% else %}me-2{% endif %}">Wants</div> <div
class="text-sm font-semibold text-center {% if expanded %}ms-5{% endif %}"
>
Wants
</div>
</div> </div>
</div> </div>
</a>
</div> </div>
<!-- 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">
<div class="px-4 main-badges pb-0"> <div class="px-4 main-badges pb-0">
{% if expanded %} {% if expanded %}
<div class="flex flex-row gap-1 justify-around"> <div class="flex flex-row justify-around">
<!-- Has Side (inner grid of 2 columns) --> <!-- Has Side (inner grid of 2 columns) -->
<div class="grid grid-cols-2 content-start"> <div class="grid grid-cols-2 content-start gap-1">
{% for card in have_cards_available %} {% for card in have_cards_available %} {% card_badge card.card card.quantity %} {% empty %}
{% card_badge card.card card.quantity %} <div class="text-xs text-center mb-2 ms-3 col-span-2">
None left.
</div>
{% endfor %} {% endfor %}
</div> </div>
<!-- Vertical Divider --> <!-- Vertical Divider -->
@ -53,9 +65,11 @@
<div class="w-px bg-gray-300 h-full"></div> <div class="w-px bg-gray-300 h-full"></div>
</div> </div>
<!-- Wants Side (inner grid of 2 columns) --> <!-- Wants Side (inner grid of 2 columns) -->
<div class="grid grid-cols-2 content-start"> <div class="grid grid-cols-2 content-start gap-1">
{% for card in want_cards_available %} {% for card in want_cards_available %} {% card_badge card.card card.quantity %} {% empty %}
{% card_badge card.card card.quantity %} <div class="text-xs text-center mb-2 ms-3 col-span-2">
None left.
</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -63,31 +77,36 @@
<!-- 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-1 justify-around"> <div class="flex flex-row gap-1 justify-around">
<!-- Has Side --> <!-- Has Side -->
<div class="flex flex-col content-start"> <div class="flex flex-col content-start gap-1">
{% for card in have_cards_available %} {% for card in have_cards_available %} {% card_badge card.card card.quantity %} {% empty %}
{% card_badge card.card card.quantity %} <div class="text-xs text-center mb-2 ms-3">
{% empty %} None left.
<div class="text-xs text-center mb-2 ms-3">None left.</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<!-- Wants Side --> <!-- Wants Side -->
<div class="flex flex-col content-start"> <div class="flex flex-col content-start gap-1">
{% for card in want_cards_available %} {% for card in want_cards_available %} {% card_badge card.card card.quantity %} {% empty %}
{% card_badge card.card card.quantity %} <div
{% empty %} class="text-xs text-center mb-2{% if expanded %} ms-8{% else %} me-4{% endif %}"
<div class="text-xs text-center mb-2{% if expanded %} ms-8{% else %} me-4{% endif %}">None left.</div> >
None left.
</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</a>
</div> </div>
<div class="flip-face-footer self-end"> <div class="flip-face-footer self-end">
<div class="flex flex-col gap-2 text-center"> <div class="flex flex-col gap-2 text-center">
<div class="text-sm font-semibold text-base-content"> <div class="text-sm font-semibold text-base-content">
{{ in_game_name }} {% if show_friend_code %}<span class="text-base-content/50"></span> {{ friend_code }}{% endif %} {% if show_friend_code %}{{ in_game_name }} <span
class="text-base-content/50"
>•</span
>
{{ friend_code }}{% endif %}
</div> </div>
</div> </div>
</div> </div>

View file

@ -55,15 +55,15 @@ class TradeAcceptanceCreateForm(forms.ModelForm):
elif default_friend_code and friend_codes.filter(pk=default_friend_code.pk).exists(): elif default_friend_code and friend_codes.filter(pk=default_friend_code.pk).exists():
self.initial["accepted_by"] = default_friend_code.pk self.initial["accepted_by"] = default_friend_code.pk
# Update available requested_card choices from the TradeOffer's "have" side. # Fix: Convert available 'have' cards (from through model) to Card objects.
active_states = [ self.fields["requested_card"].queryset = Card.objects.filter(
TradeAcceptance.AcceptanceState.ACCEPTED, pk__in=trade_offer.have_cards_available_qs.values_list("card_id", flat=True)
TradeAcceptance.AcceptanceState.SENT, )
TradeAcceptance.AcceptanceState.RECEIVED,
]
self.fields["requested_card"].queryset = trade_offer.have_cards_available_qs
self.fields["offered_card"].queryset = trade_offer.want_cards_available_qs # Similarly for offered_card.
self.fields["offered_card"].queryset = Card.objects.filter(
pk__in=trade_offer.want_cards_available_qs.values_list("card_id", flat=True)
)
def clean(self): def clean(self):
""" """

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-04-14 04:07 # Generated by Django 5.1.2 on 2025-04-14 20:58
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

View file

@ -94,6 +94,8 @@ class TradeOffer(models.Model):
if len(rarity_levels) > 1: if len(rarity_levels) > 1:
raise ValidationError("All cards in a trade offer must have the same rarity.") raise ValidationError("All cards in a trade offer must have the same rarity.")
first_card = cards[0] first_card = cards[0]
if first_card.rarity_level > 5:
raise ValidationError("Cannot trade cards above one-star rarity.")
if self.rarity_level != first_card.rarity_level or self.rarity_icon != first_card.rarity_icon: if self.rarity_level != first_card.rarity_level or self.rarity_icon != first_card.rarity_icon:
self.rarity_level = first_card.rarity_level self.rarity_level = first_card.rarity_level
self.rarity_icon = first_card.rarity_icon self.rarity_icon = first_card.rarity_icon
@ -140,11 +142,11 @@ class TradeOfferHaveCard(models.Model):
return self.quantity - self.qty_accepted return self.quantity - self.qty_accepted
def __str__(self): def __str__(self):
return f"{self.card.name} x{self.quantity} (Accepted: {self.qty_accepted})" return f"#{self.card.cardnum} {self.card.cardset} {self.card.rarity_icon} {self.card.name}"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.trade_offer.update_rarity_fields() self.trade_offer.update_rarity_fields()
super().save(*args, **kwargs)
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
trade_offer = self.trade_offer trade_offer = self.trade_offer
@ -174,7 +176,7 @@ class TradeOfferWantCard(models.Model):
return self.quantity - self.qty_accepted return self.quantity - self.qty_accepted
def __str__(self): def __str__(self):
return f"{self.card.name} x{self.quantity} (Accepted: {self.qty_accepted})" return f"#{self.card.cardnum} {self.card.cardset} {self.card.rarity_icon} {self.card.name}"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)

View file

@ -40,7 +40,7 @@ def adjust_qty_for_trade_offer(trade_offer, card, side, delta):
def update_trade_offer_closed_status(trade_offer): def update_trade_offer_closed_status(trade_offer):
""" """
Check if both sides of the trade offer meet the quantity requirement. Check if both sides of the trade offer meet the quantity requirement.
Mark the trade_offer as closed if all cards have qty_accepted Mark the trade_offer as closed if all cards on one side have qty_accepted
greater than or equal to quantity; otherwise, mark it as open. greater than or equal to quantity; otherwise, mark it as open.
""" """
have_complete = not TradeOfferHaveCard.objects.filter( have_complete = not TradeOfferHaveCard.objects.filter(
@ -107,6 +107,8 @@ def trade_acceptance_email_notification(sender, instance, created, **kwargs):
# return # return
acting_user = instance._actioning_user acting_user = instance._actioning_user
del instance._actioning_user
state = instance.state state = instance.state
if state == TradeAcceptance.AcceptanceState.ACCEPTED: if state == TradeAcceptance.AcceptanceState.ACCEPTED:
@ -169,9 +171,6 @@ def trade_acceptance_email_notification(sender, instance, created, **kwargs):
[recipient_user.email], [recipient_user.email],
) )
# Clean up the temporary attribute.
del instance._actioning_user
@receiver(post_save, sender=TradeAcceptance) @receiver(post_save, sender=TradeAcceptance)
def trade_acceptance_reputation_update(sender, instance, created, **kwargs): def trade_acceptance_reputation_update(sender, instance, created, **kwargs):
""" """

View file

@ -42,7 +42,7 @@ class TradeOfferCreateView(LoginRequiredMixin, CreateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
from cards.models import Card from cards.models import Card
# Ensure available_cards is a proper QuerySet # Ensure available_cards is a proper QuerySet
context["cards"] = Card.objects.all().order_by("name", "rarity_level") context["cards"] = Card.objects.filter(rarity_level__lte=5).order_by("name", "rarity_level")
friend_codes = self.request.user.friend_codes.all() friend_codes = self.request.user.friend_codes.all()
if "initiated_by" in self.request.GET: if "initiated_by" in self.request.GET:
try: try:
@ -245,8 +245,8 @@ class TradeOfferSearchView(ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
from cards.models import Card from cards.models import Card
# Populate available_cards to re-populate the multiselects. # Populate available_cards to re-populate the multiselects. Exclude cards with rarity level > 5.
context["cards"] = Card.objects.all().order_by("name") context["cards"] = Card.objects.filter(rarity_level__lte=5).order_by("name", "rarity_level")
if self.request.method == "POST": if self.request.method == "POST":
context["have_cards"] = self.request.POST.getlist("have_cards") context["have_cards"] = self.request.POST.getlist("have_cards")
context["want_cards"] = self.request.POST.getlist("want_cards") context["want_cards"] = self.request.POST.getlist("want_cards")
@ -515,7 +515,7 @@ class TradeOfferPNGView(View):
return HttpResponse(trade_offer.image.read(), content_type="image/png") return HttpResponse(trade_offer.image.read(), content_type="image/png")
tag_context = render_trade_offer_png( tag_context = render_trade_offer_png(
{'request': request}, trade_offer, show_friend_code=True {'request': request}, trade_offer, show_friend_code=trade_offer.initiated_by.user.show_friend_code_on_link_previews
) )
image_width = tag_context.get('image_width') image_width = tag_context.get('image_width')
image_height = tag_context.get('image_height') image_height = tag_context.get('image_height')
@ -547,7 +547,7 @@ class TradeOfferPNGView(View):
page.on("console", lambda msg: print(f"Console {msg.type}: {msg.text}")) page.on("console", lambda msg: print(f"Console {msg.type}: {msg.text}"))
page.on("pageerror", lambda err: print(f"Page error: {err}")) page.on("pageerror", lambda err: print(f"Page error: {err}"))
page.on("requestfailed", lambda req: print(f"Failed to load: {req.url} - {req.failure.error_text}")) page.on("requestfailed", lambda req: print(f"Failed to load: {req.url} - {req.failure.error_text}"))
page.set_content(html, wait_until="domcontentloaded") page.set_content(html, wait_until="networkidle")
element = page.wait_for_selector(".trade-offer-card-screenshot") element = page.wait_for_selector(".trade-offer-card-screenshot")
screenshot_bytes = element.screenshot(type="png", omit_background=True) screenshot_bytes = element.screenshot(type="png", omit_background=True)
browser.close() browser.close()