Skip to main content

max / makenotwork

10.3 KB · 224 lines History Blame Raw
1 {% extends "base.html" %}
2
3 {% block title %}Cart ({{ total_items }}) - Makenot.work{% endblock %}
4 {% block body_attrs %} class="padded-page"{% endblock %}
5
6 {% block content %}
7 {% include "partials/site_header.html" %}
8
9 <div class="container">
10 <h1 class="page-title">Your Cart</h1>
11
12 {% if checkout_status == "partial" %}
13 <div class="callout callout--solid-warning mb-5">
14 <strong>Some purchases completed.</strong> Part of your checkout succeeded, but one or more creators could not be processed. The items below are still in your cart; you can try checking out again.
15 </div>
16 {% endif %}
17
18 {% if seller_groups.is_empty() %}
19 <div class="content-section cart-empty">
20 <p class="muted mb-4">Your cart is empty.</p>
21 <p class="cart-empty-hint">Add items from the Discover page or from your wishlist.</p>
22 <a href="/discover" class="btn-primary">Browse Discover</a>
23 </div>
24 {% else %}
25
26 {% if seller_groups.len() > 1 %}
27 <div class="cart-multi-bar">
28 <div>
29 <strong>{{ total_items }} items from {{ seller_groups.len() }} creators</strong>
30 <span class="cart-multi-bar-note">You'll check out with each creator in sequence.</span>
31 </div>
32 <form action="/stripe/checkout/cart/all" method="post" class="cart-multi-bar-form">
33 {% if let Some(token) = csrf_token %}<input type="hidden" name="_csrf" value="{{ token }}">{% endif %}
34 <label class="cart-share-label">
35 <input type="checkbox" name="share_contact" value="true"> Share email
36 </label>
37 <button type="submit" class="btn-primary" data-loading-text="Redirecting to Stripe...">Checkout All</button>
38 </form>
39 </div>
40 {% endif %}
41
42 {% for group in seller_groups %}
43 <div class="content-section cart-group">
44 <h2 class="cart-group-title">
45 <a href="/u/{{ group.seller_username }}">{{ group.seller_username }}</a>
46 </h2>
47 <p class="cart-group-count">{{ group.item_count }} item{% if group.item_count != 1 %}s{% endif %}</p>
48
49 <div class="scroll-x">
50 <table class="data-table minw-400">
51 <thead>
52 <tr>
53 <th>Item</th>
54 <th>Type</th>
55 <th class="text-right">Price</th>
56 <th></th>
57 </tr>
58 </thead>
59 <tbody>
60 {% for item in group.items %}
61 <tr id="cart-row-{{ item.item_id }}">
62 <td><a href="/i/{{ item.item_id }}">{{ item.title }}</a></td>
63 <td><span class="badge">{{ item.item_type }}</span></td>
64 <td class="text-right">
65 {% if item.pwyw_enabled %}
66 <div class="cart-pwyw-cell">
67 <span class="cart-pwyw-symbol">$</span>
68 <input type="number"
69 class="pwyw-cart-input cart-pwyw-input input--xs w-80 input--numeric"
70 data-item-id="{{ item.item_id }}"
71 value="{{ item.effective_price_input_value() }}"
72 min="{{ item.pwyw_min_dollars() }}"
73 step="0.01">
74 </div>
75 {% else if item.is_free() %}Free
76 {% else %}{{ item.effective_price_display() }}
77 {% endif %}
78 </td>
79 <td>
80 <button class="btn-secondary small cart-row-btn"
81 hx-delete="/api/cart/{{ item.item_id }}"
82 hx-target="#cart-row-{{ item.item_id }}"
83 hx-swap="outerHTML"
84 hx-confirm="Remove this item from your cart?">Remove</button>
85 </td>
86 </tr>
87 {% endfor %}
88 </tbody>
89 </table>
90 </div>
91
92 <div class="cart-group-summary">
93 <div>
94 <div class="cart-subtotal">
95 Subtotal: {{ group.subtotal_display() }}
96 </div>
97 <div class="cart-fee-line">
98 Platform fee: $0.00 &middot; Stripe processing: ~2.9% + $0.30
99 </div>
100 {% if group.savings_cents > 0 %}
101 <div class="cart-savings">
102 Buying together saves {{ group.seller_username }} ~{{ group.savings_display() }} in processing fees vs. separate purchases.
103 </div>
104 {% endif %}
105 </div>
106
107 {% if group.stripe_ready %}
108 <form action="/stripe/checkout/cart" method="post">
109 {% if let Some(token) = csrf_token %}<input type="hidden" name="_csrf" value="{{ token }}">{% endif %}
110 <input type="hidden" name="seller_id" value="{{ group.seller_id }}">
111 <div class="cart-promo-row">
112 <input type="text" name="promo_code" placeholder="Promo code (optional)"
113 autocomplete="off"
114 class="cart-promo-input input--sm w-180">
115 </div>
116 <label class="cart-checkout-share">
117 <input type="checkbox" name="share_contact" value="true">
118 Share my email with {{ group.seller_username }}
119 </label>
120 <button type="submit" class="btn-primary" data-loading-text="Redirecting to Stripe...">Checkout ({{ group.item_count }} items)</button>
121 </form>
122 {% else %}
123 <div class="cart-stripe-not-ready">
124 <p>This creator hasn't set up payments yet. You can keep these items in your cart for later, or remove them.</p>
125 <button type="button" class="btn-secondary small"
126 data-seller-name="{{ group.seller_username }}"
127 onclick="removeCartGroup(this)">Remove all from {{ group.seller_username }}</button>
128 </div>
129 {% endif %}
130 </div>
131 </div>
132 {% endfor %}
133
134 {% endif %}
135
136 {% if !wishlist_suggestions.is_empty() %}
137 <div class="content-section cart-wishlist">
138 <h2 class="section-header">From your wishlist</h2>
139 <p class="section-lead text-sm dimmed">Add wishlisted items to your cart to buy them together.</p>
140 <div class="scroll-x">
141 <table class="data-table minw-400">
142 <thead>
143 <tr>
144 <th>Item</th>
145 <th>Creator</th>
146 <th class="text-right">Price</th>
147 <th></th>
148 </tr>
149 </thead>
150 <tbody>
151 {% for item in wishlist_suggestions %}
152 <tr id="wish-row-{{ item.item_id }}">
153 <td><a href="/i/{{ item.item_id }}">{{ item.title }}</a></td>
154 <td><a href="/u/{{ item.creator }}">{{ item.creator }}</a></td>
155 <td class="text-right">
156 {{ item.price_display() }}
157 </td>
158 <td>
159 <button class="btn-primary small cart-row-btn"
160 hx-post="/api/cart/{{ item.item_id }}"
161 hx-on::after-request="if(event.detail.successful) this.closest('tr').classList.add('is-faded'); this.textContent='Added'">Add to Cart</button>
162 </td>
163 </tr>
164 {% endfor %}
165 </tbody>
166 </table>
167 </div>
168 </div>
169 {% endif %}
170 </div>
171
172 <script>
173 (function() {
174 document.querySelectorAll('.pwyw-cart-input').forEach(function(input) {
175 var debounce;
176 input.addEventListener('change', function() {
177 clearTimeout(debounce);
178 var itemId = this.dataset.itemId;
179 var dollars = parseFloat(this.value) || 0;
180 var cents = Math.round(dollars * 100);
181 debounce = setTimeout(function() {
182 fetch('/api/cart/' + itemId, {
183 method: 'PUT',
184 headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
185 body: JSON.stringify({ amount_cents: cents })
186 }).then(function(r) {
187 if (!r.ok) return r.json().then(function(d) { showToast(d.error || 'Invalid amount'); });
188 }).catch(function() { showToast('Failed to update amount'); });
189 }, 300);
190 });
191 });
192 })();
193
194 window.removeCartGroup = function(btn) {
195 var name = btn.dataset.sellerName || 'this creator';
196 if (!confirm('Remove all items from ' + name + '?')) return;
197 var group = btn.closest('.cart-group');
198 if (!group) return;
199 var rows = group.querySelectorAll('tr[id^="cart-row-"]');
200 if (rows.length === 0) return;
201 btn.disabled = true;
202 var headers = csrfHeaders();
203 var pending = rows.length;
204 var failed = false;
205 function finish() {
206 if (--pending !== 0) return;
207 if (failed) {
208 showToast('Some items could not be removed. Refreshing.');
209 window.location.reload();
210 } else {
211 window.location.reload();
212 }
213 }
214 rows.forEach(function(row) {
215 var id = row.id.replace('cart-row-', '');
216 fetch('/api/cart/' + id, { method: 'DELETE', headers: headers })
217 .then(function(r) { if (!r.ok) failed = true; })
218 .catch(function() { failed = true; })
219 .finally(finish);
220 });
221 };
222 </script>
223 {% endblock %}
224