From b27c0748a6e2e7dceed116cb42ab276d949800c8 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Wed, 13 May 2026 13:01:11 -0400 Subject: [PATCH] feat: live pay-by countdown on offer + auction pages (JS-enhanced) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When JS is available, the buyer now sees a live countdown next to the absolute pay-by date on both the accepted-offer page and the auction-won page. Without JS, the existing static "You have until " copy still renders — the countdown element is .js-only. Offer (/o/{id}): - offer_page view now exposes pay_deadline_timestamp_ms alongside the human string. - offer.j2 adds a [data-pay-deadline] span next to the deadline copy on both the buyer (pay-now) and seller (awaiting-payment) sides. - offer.js scans for [data-pay-deadline] every second and writes the formatted remaining time into .offer-pay-countdown-value. Reuses the same fmtRemaining shape as auction.js (Nd Nh Nm / Nh Nm Ns / Nm Ns). Auction (/a/{id}): - _serialize_auction adds payment_deadline_timestamp, user_is_winner, and is_settled. - auction.j2 renders a new "You won this auction!" well when state is ENDED, the current user is the winner, the auction isn't SETTLED, and payment_deadline_timestamp is set. The well carries the pay button + a countdown that auction.js fills in. The human deadline is also resolved client-side (toLocaleString) so the buyer sees it in their own timezone. - auction.js adds tickPayCountdown() in addition to the existing tickCountdown() that counts auction end. Test fixture _accepted_offer now sets offer.accepted_timestamp (the fixture bypasses accept_offer() which would set it for free). test_accepted_offer_renders_pay_countdown_for_buyer locks in the markup. --- make_post_sell/static/js/auction.js | 27 ++++++++++++++++++ make_post_sell/static/js/offer.js | 37 +++++++++++++++++++++++++ make_post_sell/templates/auction.j2 | 14 ++++++++++ make_post_sell/templates/offer.j2 | 12 ++++++-- make_post_sell/tests/test_functional.py | 19 +++++++++++++ make_post_sell/views/auction.py | 8 ++++++ make_post_sell/views/offer.py | 2 ++ 7 files changed, 117 insertions(+), 2 deletions(-) diff --git a/make_post_sell/static/js/auction.js b/make_post_sell/static/js/auction.js index ff8c1d9..f81c6ae 100644 --- a/make_post_sell/static/js/auction.js +++ b/make_post_sell/static/js/auction.js @@ -114,9 +114,36 @@ }); } + // Pay-by countdown (post-end): same fmtRemaining; written into + // #auction-pay-countdown if present. The human deadline string is + // also resolved locally from the same data attribute so the user + // sees a consistent wall-clock + countdown in their own timezone. + const payCountdownEl = document.getElementById("auction-pay-countdown"); + const payDeadlineHumanEl = document.getElementById( + "auction-pay-deadline-human" + ); + + function tickPayCountdown() { + if (!payCountdownEl) return; + const raw = payCountdownEl.getAttribute("data-pay-deadline"); + if (!raw) return; + const deadline = parseInt(raw, 10); + if (isNaN(deadline)) return; + payCountdownEl.textContent = fmtRemaining(deadline - Date.now()); + if (payDeadlineHumanEl && payDeadlineHumanEl.textContent === "computing…") { + try { + payDeadlineHumanEl.textContent = new Date(deadline).toLocaleString(); + } catch (e) { + /* leave the placeholder */ + } + } + } + // Countdown ticks locally every second. setInterval(tickCountdown, 1000); + setInterval(tickPayCountdown, 1000); tickCountdown(); + tickPayCountdown(); // Live state: prefer a bounded SSE feed (the server closes it after // ~25s; EventSource reconnects on its own), and fall back to polling diff --git a/make_post_sell/static/js/offer.js b/make_post_sell/static/js/offer.js index e0c6cc3..69bcbb9 100644 --- a/make_post_sell/static/js/offer.js +++ b/make_post_sell/static/js/offer.js @@ -118,9 +118,46 @@ }); } + /* Pay-by deadline countdown. + * + * Any element carrying [data-pay-deadline] (an absolute ms timestamp) + * with a child .offer-pay-countdown-value will tick once a second + * showing how long the buyer has left to pay. When the window + * elapses the offer auto-expires server-side; we reflect that + * locally as "expired — refresh to update." + */ + function fmtRemaining(ms) { + if (ms <= 0) return "expired"; + var totalSeconds = Math.floor(ms / 1000); + var days = Math.floor(totalSeconds / 86400); + var hours = Math.floor((totalSeconds % 86400) / 3600); + var minutes = Math.floor((totalSeconds % 3600) / 60); + var seconds = totalSeconds % 60; + if (days > 0) return days + "d " + hours + "h " + minutes + "m"; + if (hours > 0) return hours + "h " + minutes + "m " + seconds + "s"; + if (minutes > 0) return minutes + "m " + seconds + "s"; + return seconds + "s"; + } + + function tickPayCountdown() { + var nodes = document.querySelectorAll("[data-pay-deadline]"); + if (!nodes.length) return; + var now = Date.now(); + Array.prototype.forEach.call(nodes, function (node) { + var raw = node.getAttribute("data-pay-deadline"); + var deadline = parseInt(raw, 10); + if (isNaN(deadline)) return; + var value = node.querySelector(".offer-pay-countdown-value"); + if (!value) return; + value.textContent = fmtRemaining(deadline - now); + }); + } + function init() { wire(); watchOfferState(); + tickPayCountdown(); + setInterval(tickPayCountdown, 1000); } if (document.readyState === "loading") { diff --git a/make_post_sell/templates/auction.j2 b/make_post_sell/templates/auction.j2 index 06c2267..6d975a0 100644 --- a/make_post_sell/templates/auction.j2 +++ b/make_post_sell/templates/auction.j2 @@ -40,6 +40,20 @@ {% endif %} + {% if is_ended and user_is_winner and not is_settled and payment_deadline_timestamp %} +
+

You won this auction!

+

+ Complete payment by computing… + · computing… remaining, + or the auction will be released to the next-highest bidder. +

+
+ +
+
+ {% endif %} + {% if is_active %} {% if user_is_seller %}
diff --git a/make_post_sell/templates/offer.j2 b/make_post_sell/templates/offer.j2 index c4c97cf..37c4eb7 100644 --- a/make_post_sell/templates/offer.j2 +++ b/make_post_sell/templates/offer.j2 @@ -107,7 +107,11 @@ {% if pay_deadline_human %} -

You have until {{ pay_deadline_human }} to complete payment, after which this offer auto-expires.

+

You have until {{ pay_deadline_human }} to complete payment, after which this offer auto-expires. + {% if pay_deadline_timestamp_ms %} + · computing… remaining + {% endif %} +

{% endif %}
@@ -118,7 +122,11 @@

Awaiting payment

You accepted {{ buyer_name }}’s offer at ${{ "%.2f"|format(current_amount) }}. We emailed them a one-time checkout link — this offer can be redeemed only once.

{% if pay_deadline_human %} -

Buyer must pay by {{ pay_deadline_human }}; the offer auto-expires after that.

+

Buyer must pay by {{ pay_deadline_human }}; the offer auto-expires after that. + {% if pay_deadline_timestamp_ms %} + · computing… remaining + {% endif %} +

{% endif %}

If they need it again, share this same page: