Fix gravatar hovercards, and add trade_offer image generation with playwright, for use with opengraph tags on trade_offer_detal.html
This commit is contained in:
parent
4c0db9f842
commit
f3a1366269
16 changed files with 372 additions and 123 deletions
|
|
@ -23,7 +23,9 @@ COPY .env.production /code/.env
|
|||
ENV HOME=/code
|
||||
|
||||
# Install NPM & node.js
|
||||
RUN apt-get update && apt-get install -y nodejs npm
|
||||
RUN apt-get update && apt-get install -y nodejs npm xvfb libnss3 libnspr4 libasound2t64 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdbus-1-3 libdrm2 libgbm1 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 libcairo-gobject2 libdbus-glib-1-2 libfontconfig1 libfreetype6 libgdk-pixbuf-2.0-0 libgtk-3-0 libharfbuzz0b libpangocairo-1.0-0 libx11-xcb1 libxcb-shm0 libxcursor1 libxi6 libxrender1 libxtst6 libsoup-3.0-0 gstreamer1.0-libav gstreamer1.0-plugins-base gstreamer1.0-plugins-good libegl1 libenchant-2-2 libepoxy0 libevdev2 libgles2 libglx0 libgstreamer-gl1.0-0 libgstreamer-plugins-base1.0-0 libgstreamer1.0-0 libgtk-4-1 libgudev-1.0-0 libharfbuzz-icu0 libhyphen0 libicu72 libjpeg62-turbo liblcms2-2 libmanette-0.2-0 libnotify4 libopengl0 libopenjp2-7 libopus0 libpng16-16 libproxy1v5 libsecret-1-0 libwayland-client0 libwayland-egl1 libwayland-server0 libwebp7 libwebpdemux2 libwoff1 libxml2 libxslt1.1 libatomic1 libevent-2.1-7 libavif16 xvfb fonts-noto-color-emoji fonts-unifont xfonts-scalable fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei fonts-tlwg-loma-otf fonts-freefont-ttf
|
||||
|
||||
RUN playwright install-deps && playwright install
|
||||
|
||||
# Expose port 8000
|
||||
EXPOSE 8000
|
||||
|
|
@ -32,5 +34,7 @@ EXPOSE 8000
|
|||
|
||||
RUN python manage.py collectstatic --noinput
|
||||
|
||||
#RUN python manage.py createcachetable django_cache
|
||||
|
||||
# Use gunicorn on port 8000
|
||||
CMD ["gunicorn", "--bind", ":8000", "--workers", "2", "django_project.wsgi"]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 5.1.2 on 2025-03-17 20:39
|
||||
# Generated by Django 5.1.2 on 2025-03-20 00:08
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ def gravatar_url(email, size=20):
|
|||
Returns the Gravatar URL for a given email. The URL includes parameters
|
||||
for the default image and the size.
|
||||
"""
|
||||
default = "wavatar"
|
||||
default = "retro"
|
||||
email_hash = gravatar_hash(email)
|
||||
params = urlencode({'d': default, 's': str(size)})
|
||||
return f"https://www.gravatar.com/avatar/{email_hash}?{params}"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 5.1.2 on 2025-03-17 20:39
|
||||
# Generated by Django 5.1.2 on 2025-03-20 00:08
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ INSTALLED_APPS = [
|
|||
"cards",
|
||||
"home",
|
||||
"trades.apps.TradesConfig",
|
||||
"meta",
|
||||
#"silk",
|
||||
]
|
||||
|
||||
|
|
@ -67,6 +68,11 @@ SILKY_META = True
|
|||
|
||||
TAILWIND_APP_NAME = 'theme'
|
||||
|
||||
META_SITE_NAME = 'PKMN Trade Club'
|
||||
META_SITE_PROTOCOL = 'https'
|
||||
META_USE_SITES = True
|
||||
META_IMAGE_URL = 'https://pkmntrade.club/'
|
||||
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
|
|
@ -299,6 +305,6 @@ else:
|
|||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
|
||||
"LOCATION": "site-cache",
|
||||
"LOCATION": "django_cache",
|
||||
}
|
||||
}
|
||||
|
|
@ -14,11 +14,13 @@ django-daisy==1.0.13
|
|||
django-debug-toolbar==4.4.6
|
||||
django-el-pagination==4.1.2
|
||||
django-environ==0.12.0
|
||||
django-meta==2.4.2
|
||||
django-silk==5.3.1
|
||||
django-tailwind-4[reload]==0.1.4
|
||||
django-widget-tweaks==1.5.0
|
||||
gunicorn==23.0.0
|
||||
idna==3.4
|
||||
imgkit==1.2.3
|
||||
oauthlib==3.2.2
|
||||
packaging==23.1
|
||||
psycopg==3.2.3
|
||||
|
|
@ -26,6 +28,7 @@ psycopg-binary==3.2.3
|
|||
pycparser==2.21
|
||||
PyJWT==2.6.0
|
||||
python3-openid==3.2.0
|
||||
playwright==1.51.0
|
||||
requests==2.28.2
|
||||
requests-oauthlib==1.3.1
|
||||
sqlparse==0.4.3
|
||||
|
|
|
|||
|
|
@ -10,13 +10,19 @@
|
|||
<div class="card-body">
|
||||
<div class="mx-auto mb-4">{{ user.email|gravatar:100 }}</div>
|
||||
<p class="text-center">All profile information is managed through Gravatar.</p>
|
||||
<p class="text-center mt-4"><a href="https://gravatar.com/profile/" target="_blank" rel="noopener noreferrer" class="btn btn-secondary">Edit Profile on Gravatar</a></p>
|
||||
<p class="text-center mt-4">
|
||||
<a href="https://gravatar.com/profile/" target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
|
||||
Edit Profile 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>
|
||||
</a></p>
|
||||
<div class="divider"></div>
|
||||
<h2 class="text-lg font-semibold pt-0">What is Gravatar?</h2>
|
||||
<p class="mb-4">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>
|
||||
|
||||
<h2 class="text-lg font-semibold">How does it work?</h2>
|
||||
<p class="mb-4">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 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">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>
|
||||
|
||||
<h2 class="text-lg font-semibold">Want to update or add a Gravatar?</h2>
|
||||
<p class="mb-4">Go to Gravatar.com to set up or change your avatar or profile. Your updates will appear here once saved!</p>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% include 'meta/meta.html' %}
|
||||
|
||||
<!-- Inline script to set the theme before rendering -->
|
||||
<script>
|
||||
|
|
@ -25,6 +26,7 @@
|
|||
</script>
|
||||
|
||||
<title>[PᴋMɴ Trade Club] {% block title %}{% endblock title %}</title>
|
||||
|
||||
<link rel="shortcut icon" href="{% static 'images/favicon.ico' %}">
|
||||
<!-- Choices.js -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js@11.0.6/public/assets/styles/choices.min.css" />
|
||||
|
|
@ -113,12 +115,7 @@
|
|||
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-32 p-2 shadow">
|
||||
<li>
|
||||
<a class="flex items-center justify-between" href="{% url 'profile' %}">
|
||||
<div>Profile</div>
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" 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>
|
||||
</div>
|
||||
Profile
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load trade_offer_tags %}
|
||||
|
||||
{% block title %}Trade Offer Detail{% endblock title %}
|
||||
{% block title %}{{title}}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-2xl mt-6">
|
||||
<h2 class="text-2xl font-bold">Trade Offer Details</h2>
|
||||
<div class="flex justify-center mt-10">
|
||||
{% if screenshot_mode == "true" %}
|
||||
{% render_trade_offer object True %}
|
||||
{% else %}
|
||||
{% render_trade_offer object %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if acceptance_form %}
|
||||
<div class="w-3/4 mx-auto mt-4">
|
||||
|
|
@ -50,7 +54,7 @@
|
|||
<a href="{% url 'trade_offer_list' %}" class="btn btn-secondary">Back to Trade Offers</a>
|
||||
{% if is_initiator %}
|
||||
<a href="{{ delete_close_url }}" class="btn btn-danger">Delete/Close Trade Offer</a>
|
||||
{% else %}
|
||||
{% elif request.user.is_authenticated %}
|
||||
<button type="submit" class="btn btn-primary">Submit Acceptance</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<div class="relative inline-block">
|
||||
<div class="grid grid-cols-4 grid-rows-2 my-2 px-2 py-2 h-16 w-36 text-white shadow-md shadow-black/50" style="{{ style }}">
|
||||
<div class="row-span-1 col-span-4 truncate text-ellipsis self-start font-semibold leading-tight text-sm max-w-7/8">{{ name }}</div>
|
||||
<div class="row-start-2 col-span-2 truncate self-end align-bottom text-xs">{{ rarity }}</div>
|
||||
<div class="row-start-2 col-start-3 col-span-2 text-right truncate self-end align-bottom font-semibold leading-none text-sm">{{ cardset }}</div>
|
||||
<div class="card-badge relative inline-block">
|
||||
<div class="card-badge-inner freeze-bg-color grid grid-cols-4 grid-rows-2 my-2 px-2 py-2 h-16 w-36 text-white shadow-md shadow-black/50" style="{{ style }}">
|
||||
<div class="cardname row-span-1 col-span-4 truncate text-ellipsis self-start font-semibold leading-tight text-sm max-w-7/8">{{ name }}</div>
|
||||
<div class="rarity row-start-2 col-span-2 truncate self-end align-bottom text-xs">{{ rarity }}</div>
|
||||
<div class="cardset row-start-2 col-start-3 col-span-2 text-right truncate self-end align-bottom font-semibold leading-none text-sm">{{ cardset }}</div>
|
||||
</div>
|
||||
<span class="card-quantity-badge absolute top-3.5 right-1 bg-gray-600 text-white text-xs font-semibold rounded-full px-2">
|
||||
<span class="card-quantity-badge freeze-bg-color absolute top-3.5 right-1 bg-gray-600 text-white text-xs font-semibold rounded-full px-2">
|
||||
{{ quantity }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -16,8 +16,24 @@
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div x-data="tradeOfferCard()" class="transition-all duration-500 trade-offer-card"
|
||||
<div class="trade-offer-card-screenshot p-4 h-full w-auto flex justify-center"
|
||||
{% if screenshot_mode %}
|
||||
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()" class="transition-all duration-500 trade-offer-card my-auto"
|
||||
@toggle-all.window="setBadge($event.detail.expanded)">
|
||||
|
||||
<!-- Flip container providing perspective -->
|
||||
|
|
@ -26,73 +42,123 @@
|
|||
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 grid grid-cols-1 grid-rows-1 card bg-base-100 card-border shadow-lg w-96 md:w-80 lg:w-96 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 {% if screenshot_mode and num_cards_available >= 4 %}w-160{% else %}w-96 md:w-80 lg:w-96{% endif %} 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">
|
||||
<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">
|
||||
<!-- Header -->
|
||||
<div class="self-start">
|
||||
<div class="flip-face-header self-start">
|
||||
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline block">
|
||||
<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>
|
||||
<!-- 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>
|
||||
<div class="flex justify-center items-center">
|
||||
<!-- 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 class="flex justify-center items-center">
|
||||
<span class="text-sm font-semibold">Wants</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<!-- Main Trade Offer Row -->
|
||||
<div class="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-2 pb-0">
|
||||
<div class="grid grid-cols-2 gap-2 items-center">
|
||||
<div class="flex flex-col items-center">
|
||||
{% if have_cards_available %}
|
||||
{% with first_have=have_cards_available.0 %}
|
||||
{% card_badge first_have.card first_have.quantity %}
|
||||
{% endwith %}
|
||||
<div class="px-2 pb-0 main-badges">
|
||||
{% 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-between">
|
||||
<!-- 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 -->
|
||||
<div class="flex flex-row gap-2 {% if not screenshot_mode %}justify-between{% else %}justify-around{% endif %}">
|
||||
<!-- 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>
|
||||
<div class="flex flex-col items-center">
|
||||
{% if want_cards_available %}
|
||||
{% with first_want=want_cards_available.0 %}
|
||||
{% card_badge first_want.card first_want.quantity %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Extra Card Badges (Collapsible) -->
|
||||
<div x-show="badgeExpanded" x-collapse.duration.500ms class="px-2">
|
||||
{% 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-between">
|
||||
<!-- Has Side Extra Badges -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="flex flex-col items-center">
|
||||
{% for th in have_cards_available|slice:"1:" %}
|
||||
{% card_badge th.card th.quantity %}
|
||||
{% for card in have_cards_available|slice:"2:" %}
|
||||
{% card_badge card.card card.quantity %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
{% for th in want_cards_available|slice:"1:" %}
|
||||
{% card_badge th.card th.quantity %}
|
||||
<!-- Vertical Divider -->
|
||||
<div class="flex justify-center">
|
||||
<div class="w-px bg-gray-300 h-full"></div>
|
||||
</div>
|
||||
<!-- Wants Side Extra Badges -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{% for card in want_cards_available|slice:"2:" %}
|
||||
{% card_badge card.card card.quantity %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div {% if screenshot_mode %}x-show="badgeExpanded" x-collapse.duration.500ms{% endif %} 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 {% if not screenshot_mode %}justify-between{% else %}justify-around{% endif %}">
|
||||
<!-- 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 class="flex justify-center my-1 h-5">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not screenshot_mode %}
|
||||
<div class="flex justify-center h-5">
|
||||
{% if have_cards_available|length > 1 or want_cards_available|length > 1 %}
|
||||
<svg @click="badgeExpanded = !badgeExpanded"
|
||||
x-bind:class="{ 'rotate-180': badgeExpanded }"
|
||||
|
|
@ -103,8 +169,9 @@
|
|||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="self-end">
|
||||
{% endif %}
|
||||
{% if not screenshot_mode %}
|
||||
<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"
|
||||
|
|
@ -123,10 +190,18 @@
|
|||
</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 }} <span class="text-base-content/50">•</span> {{ friend_code }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Back Face: Acceptances View -->
|
||||
<!-- 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="self-start">
|
||||
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline">
|
||||
|
|
@ -208,7 +283,7 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center my-1 h-5">
|
||||
<div class="flex justify-center h-5">
|
||||
{% if acceptances|length > 1 %}
|
||||
<svg @click="acceptanceExpanded = !acceptanceExpanded"
|
||||
x-bind:class="{ 'rotate-180': acceptanceExpanded }"
|
||||
|
|
@ -242,11 +317,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Ensure proper 3D transformations on the rotating element */
|
||||
.flip-inner {
|
||||
|
|
@ -265,5 +340,10 @@
|
|||
.rotate-y-180 {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
{% if screenshot_mode %}
|
||||
*:not(.freeze-bg-color) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
{% endif %}
|
||||
</style>
|
||||
{% endcache %}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 5.1.2 on 2025-03-17 20:39
|
||||
# Generated by Django 5.1.2 on 2025-03-20 00:08
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from django import template
|
|||
register = template.Library()
|
||||
|
||||
@register.inclusion_tag('templatetags/trade_offer.html', takes_context=True)
|
||||
def render_trade_offer(context, offer):
|
||||
def render_trade_offer(context, offer, screenshot_mode=False):
|
||||
"""
|
||||
Renders a trade offer including detailed trade acceptance information.
|
||||
Freezes the through-model querysets to avoid extra DB hits.
|
||||
|
|
@ -33,6 +33,10 @@ def render_trade_offer(context, offer):
|
|||
'acceptances': acceptances,
|
||||
'have_cards_available': have_cards_available,
|
||||
'want_cards_available': want_cards_available,
|
||||
'screenshot_mode': screenshot_mode,
|
||||
'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),
|
||||
}
|
||||
|
||||
@register.inclusion_tag('templatetags/trade_acceptance.html', takes_context=True)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from .views import (
|
|||
TradeAcceptanceUpdateView,
|
||||
TradeOfferDeleteView,
|
||||
TradeOfferSearchView,
|
||||
TradeOfferPNGView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
|
|
@ -17,6 +18,7 @@ urlpatterns = [
|
|||
path("my/", TradeOfferMyListView.as_view(), name="trade_offer_my_list"),
|
||||
path("search/", TradeOfferSearchView.as_view(), name="trade_offer_search"),
|
||||
path("<int:pk>/", TradeOfferDetailView.as_view(), name="trade_offer_detail"),
|
||||
path("<int:pk>.png", TradeOfferPNGView.as_view(), name="trade_offer_png"),
|
||||
path("delete/<int:pk>/", TradeOfferDeleteView.as_view(), name="trade_offer_delete"),
|
||||
path("offer/<int:offer_pk>", TradeAcceptanceCreateView.as_view(), name="trade_acceptance_create"),
|
||||
path("accept/<int:pk>/", TradeAcceptanceUpdateView.as_view(), name="trade_acceptance_update"),
|
||||
|
|
|
|||
149
trades/views.py
149
trades/views.py
|
|
@ -1,4 +1,5 @@
|
|||
from django.views.generic import TemplateView, DeleteView, CreateView, ListView, DetailView, UpdateView, FormView
|
||||
from django.views import View
|
||||
from django.urls import reverse_lazy
|
||||
from django.http import HttpResponseRedirect, JsonResponse
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
|
|
@ -12,11 +13,18 @@ from django.views.decorators.http import require_http_methods
|
|||
from django.core.paginator import Paginator
|
||||
from django.contrib import messages
|
||||
|
||||
from meta.views import Meta
|
||||
from .models import TradeOffer, TradeAcceptance
|
||||
from .forms import (TradeOfferAcceptForm,
|
||||
TradeAcceptanceCreateForm, TradeOfferCreateForm, TradeAcceptanceTransitionForm)
|
||||
from cards.models import Card
|
||||
#from silk.profiling.profiler import silk_profile
|
||||
import imgkit
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.template.loader import render_to_string
|
||||
from trades.templatetags.trade_offer_tags import render_trade_offer
|
||||
from django.template import RequestContext
|
||||
from playwright.sync_api import sync_playwright
|
||||
from django.conf import settings
|
||||
|
||||
class TradeOfferCreateView(LoginRequiredMixin, CreateView):
|
||||
model = TradeOffer
|
||||
|
|
@ -383,7 +391,7 @@ class TradeOfferSearchView(ListView):
|
|||
else:
|
||||
return super().render_to_response(context, **response_kwargs)
|
||||
|
||||
class TradeOfferDetailView(LoginRequiredMixin, DetailView):
|
||||
class TradeOfferDetailView(DetailView):
|
||||
"""
|
||||
Displays the details of a TradeOffer along with its active acceptances.
|
||||
If the offer is still open and the current user is not its initiator,
|
||||
|
|
@ -396,6 +404,57 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
trade_offer = self.get_object()
|
||||
screenshot_mode = self.request.GET.get("screenshot_mode")
|
||||
context["screenshot_mode"] = screenshot_mode
|
||||
|
||||
# Calculate the number of cards in each category.
|
||||
num_has = trade_offer.trade_offer_have_cards.count()
|
||||
num_wants = trade_offer.trade_offer_want_cards.count()
|
||||
num_cards = max(num_has, num_wants)
|
||||
|
||||
# Define the aspect ratio.
|
||||
aspect_ratio = 1.91
|
||||
|
||||
# Calculate a base height using our previous assumptions:
|
||||
# - 80px per card row (with rows computed as round(num_cards/2))
|
||||
# - plus 138px for header/footer.
|
||||
base_height = (round(num_cards / 2) * 80) + 138
|
||||
|
||||
# Calculate a base width by assuming two columns of card badges.
|
||||
# Here we assume each card badge is 80px wide plus the same horizontal offset of 138px.
|
||||
if (num_wants + num_has) >= 3:
|
||||
base_width = ((num_wants + num_has) * 144) + 96
|
||||
else:
|
||||
base_width = (4 * 144) + 96
|
||||
|
||||
if base_height > base_width:
|
||||
# The trade-offer card is taller than wide;
|
||||
# compute the width from the height.
|
||||
image_height = base_height
|
||||
image_width = int(round(image_height * aspect_ratio)) + 1
|
||||
else:
|
||||
# The trade-offer card is wider than tall;
|
||||
# compute the height from the width.
|
||||
image_width = base_width
|
||||
image_height = int(round(image_width / aspect_ratio))
|
||||
|
||||
# Build the meta tags with the computed dimensions.
|
||||
title = f'Trade Offer from {trade_offer.initiated_by.in_game_name} ({trade_offer.initiated_by.friend_code})'
|
||||
context["meta"] = Meta(
|
||||
title=title,
|
||||
description=f'Has: {", ".join([card.card.name for card in trade_offer.trade_offer_have_cards.all()])} • \nWants: {", ".join([card.card.name for card in trade_offer.trade_offer_want_cards.all()])}',
|
||||
image_object={
|
||||
"url": f'http://localhost:8000{reverse_lazy("trade_offer_png", kwargs={"pk": trade_offer.pk})}',
|
||||
"type": "image/png",
|
||||
"width": image_width,
|
||||
"height": image_height,
|
||||
},
|
||||
twitter_type="summary_large_image",
|
||||
use_og=True,
|
||||
use_twitter=True,
|
||||
use_facebook=True,
|
||||
use_schemaorg=True,
|
||||
)
|
||||
|
||||
# Define terminal (closed) acceptance states based on our new system:
|
||||
terminal_states = [
|
||||
|
|
@ -412,7 +471,7 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView):
|
|||
# Option 1: Filter active acceptances using the queryset lookup.
|
||||
context["active_acceptances"] = trade_offer.acceptances.exclude(state__in=terminal_states)
|
||||
|
||||
|
||||
if self.request.user.is_authenticated:
|
||||
user_friend_codes = self.request.user.friend_codes.all()
|
||||
|
||||
# Add context flag and deletion URL if the current user is the initiator
|
||||
|
|
@ -432,6 +491,11 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView):
|
|||
friend_codes=user_friend_codes,
|
||||
default_friend_code=default_friend_code
|
||||
)
|
||||
else:
|
||||
context["is_initiator"] = False
|
||||
context["delete_close_url"] = None
|
||||
context["acceptance_form"] = None
|
||||
|
||||
return context
|
||||
|
||||
class TradeAcceptanceCreateView(LoginRequiredMixin, CreateView):
|
||||
|
|
@ -539,3 +603,82 @@ class TradeAcceptanceUpdateView(LoginRequiredMixin, UpdateView):
|
|||
def get_success_url(self):
|
||||
return reverse_lazy("trade_offer_detail", kwargs={"pk": self.object.trade_offer.pk})
|
||||
|
||||
class TradeOfferPNGView(View):
|
||||
"""
|
||||
Generate a PNG screenshot of the rendered trade offer detail page using Playwright.
|
||||
|
||||
This view loads the trade offer detail page, waits for the trade offer element to render,
|
||||
simulates a click to expand extra badges, and then screenshots only the trade offer element.
|
||||
"""
|
||||
def get(self, request, *args, **kwargs):
|
||||
# For demonstration purposes, get the first trade offer.
|
||||
# In production, you might want to identify the offer via a URL parameter.
|
||||
trade_offer = TradeOffer.objects.get(pk=kwargs['pk'])
|
||||
if not trade_offer:
|
||||
raise Http404("Trade offer not found")
|
||||
|
||||
# Get the URL for the trade offer detail view.
|
||||
detail_url = request.build_absolute_uri(
|
||||
reverse_lazy("trade_offer_detail", kwargs={"pk": trade_offer.pk})+"?screenshot_mode=true"
|
||||
)
|
||||
|
||||
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
context = browser.new_context(
|
||||
viewport={"width": 1280, "height": 800},
|
||||
)
|
||||
|
||||
# If the request contains a Django session cookie,
|
||||
# add it to the browser context to bypass the login screen.
|
||||
session_cookie = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
|
||||
if session_cookie:
|
||||
cookie = {
|
||||
"name": settings.SESSION_COOKIE_NAME,
|
||||
"value": session_cookie,
|
||||
"domain": request.get_host().split(':')[0],
|
||||
"path": "/",
|
||||
"httpOnly": True,
|
||||
"secure": not settings.DEBUG,
|
||||
}
|
||||
context.add_cookies([cookie])
|
||||
|
||||
# Open a new page and navigate to the detail view.
|
||||
page = context.new_page()
|
||||
page.goto(detail_url, wait_until="networkidle")
|
||||
|
||||
# Inject CSS to force transparency.
|
||||
page.add_style_tag(content="""
|
||||
html, body, .bg-base-200 {
|
||||
background-color: rgba(255, 255, 255, 0) !important;
|
||||
}
|
||||
""")
|
||||
|
||||
trade_offer_selector = ".trade-offer-card-screenshot"
|
||||
page.wait_for_selector(trade_offer_selector)
|
||||
|
||||
# Simulate a click on the toggle button within the trade offer element
|
||||
# to force the extra details (e.g., extra badges) to expand.
|
||||
# We use a selector that targets the first svg with a "cursor-pointer" class inside the trade offer card.
|
||||
toggle_selector = f"{trade_offer_selector} svg.cursor-pointer"
|
||||
try:
|
||||
toggle = page.query_selector(toggle_selector)
|
||||
if toggle:
|
||||
toggle.click()
|
||||
# Wait for the CSS animation to complete (600ms as in your template)
|
||||
page.wait_for_timeout(600)
|
||||
except Exception:
|
||||
# If the toggle is not found or clicking fails, proceed without expansion.
|
||||
pass
|
||||
|
||||
# Locate the element containing the trade offer and capture its screenshot.
|
||||
element = page.query_selector(trade_offer_selector)
|
||||
if not element:
|
||||
browser.close()
|
||||
raise Http404("Trade offer element not found on page")
|
||||
|
||||
png_bytes = element.screenshot(type="png", omit_background=True)
|
||||
browser.close()
|
||||
|
||||
return HttpResponse(png_bytes, content_type="image/png")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue