domma-cms 0.84.1 → 0.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +4 -4
- package/admin/js/lib/sidebar-renderer.js +3 -3
- package/admin/js/lib/themes.js +1 -1
- package/config/menus/admin-sidebar.json +21 -8
- package/config/plugins.json +0 -18
- package/package.json +2 -2
- package/plugins/_lib/mail/diagnostics.js +3 -2
- package/plugins/blog/CLAUDE.md +33 -0
- package/plugins/blog/lib/access.js +64 -0
- package/plugins/blog/lib/notices.js +38 -0
- package/plugins/blog/plugin.js +84 -26
- package/plugins/blog/plugin.json +173 -20
- package/plugins/blog/roles/blog-author.json +15 -2
- package/plugins/blog/tests/access.test.js +32 -0
- package/plugins/blog/tests/comment-notices.test.js +140 -0
- package/plugins/blog/tests/notices.test.js +20 -0
- package/plugins/blog/tests/public.test.js +3 -1
- package/plugins/free-tier.lock.json +63 -10
- package/plugins/mail-reader/plugin.js +12 -1
- package/plugins/mail-reader/plugin.json +23 -4
- package/plugins/shopping-cart/CLAUDE.md +93 -0
- package/plugins/shopping-cart/admin/css/index.css +224 -0
- package/plugins/shopping-cart/admin/lib/kit.js +190 -0
- package/plugins/shopping-cart/admin/templates/shop.html +1 -0
- package/plugins/shopping-cart/admin/views/orders.js +365 -0
- package/plugins/shopping-cart/admin/views/overview.js +214 -0
- package/plugins/shopping-cart/admin/views/products.js +547 -0
- package/plugins/shopping-cart/admin/views/settings.js +247 -0
- package/plugins/shopping-cart/admin/views/shop.js +130 -0
- package/plugins/shopping-cart/collections/shop-orders.json +20 -0
- package/plugins/shopping-cart/collections/shop-products.json +24 -0
- package/plugins/shopping-cart/config.js +52 -0
- package/plugins/shopping-cart/inject/body.html +2 -0
- package/plugins/shopping-cart/lib/extensions.js +96 -0
- package/plugins/shopping-cart/lib/payments.js +281 -0
- package/plugins/shopping-cart/lib/pricing.js +232 -0
- package/plugins/shopping-cart/lib/render.js +282 -0
- package/plugins/shopping-cart/lib/samples.js +34 -0
- package/plugins/shopping-cart/lib/shop.js +436 -0
- package/plugins/shopping-cart/lib/summary.js +127 -0
- package/plugins/shopping-cart/plugin.js +559 -0
- package/plugins/shopping-cart/plugin.json +52 -0
- package/plugins/shopping-cart/plugin.public.js +144 -0
- package/plugins/shopping-cart/public/cart.js +715 -0
- package/plugins/shopping-cart/public/lib/money.js +105 -0
- package/plugins/shopping-cart/public/lib/orders.js +122 -0
- package/plugins/shopping-cart/public/lib/products.js +235 -0
- package/plugins/shopping-cart/public/samples/candle-2.svg +16 -0
- package/plugins/shopping-cart/public/samples/candle.svg +16 -0
- package/plugins/shopping-cart/public/samples/mug-2.svg +14 -0
- package/plugins/shopping-cart/public/samples/mug.svg +14 -0
- package/plugins/shopping-cart/public/samples/notebook-2.svg +15 -0
- package/plugins/shopping-cart/public/samples/notebook.svg +15 -0
- package/plugins/shopping-cart/public/samples/print-2.svg +14 -0
- package/plugins/shopping-cart/public/samples/print.svg +14 -0
- package/plugins/shopping-cart/public/samples/tee-2.svg +12 -0
- package/plugins/shopping-cart/public/samples/tee.svg +12 -0
- package/plugins/shopping-cart/public/samples/tote-2.svg +13 -0
- package/plugins/shopping-cart/public/samples/tote.svg +13 -0
- package/plugins/shopping-cart/public/shop.css +415 -0
- package/plugins/shopping-cart/templates/shop.html +4 -0
- package/plugins/shopping-cart/tests/api.test.js +144 -0
- package/plugins/shopping-cart/tests/pure.test.js +210 -0
- package/scripts/setup.js +1 -0
- package/server/routes/api/notifications.js +4 -3
- package/server/services/notify.js +4 -2
- package/server/services/pluginScope.js +85 -0
- package/server/services/plugins.js +7 -2
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The storefront's HTML, rendered on the server so every page works (and is
|
|
3
|
+
* found by search engines) before any script runs; public/cart.js then adds
|
|
4
|
+
* the live parts - the basket, filtering, quantities, checkout.
|
|
5
|
+
*
|
|
6
|
+
* Every value that came from a person is escaped here. Product descriptions
|
|
7
|
+
* are the exception: an admin wrote them, and they go through the CMS's
|
|
8
|
+
* Markdown pipeline (sanitised) like any page.
|
|
9
|
+
*
|
|
10
|
+
* @module shopping-cart/lib/render
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {formatMoney} from '../public/lib/money.js';
|
|
14
|
+
import {available, priceRange, safeImage} from '../public/lib/products.js';
|
|
15
|
+
import {COUNTRIES, STATUS_PUBLIC, addressLines} from '../public/lib/orders.js';
|
|
16
|
+
|
|
17
|
+
export const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
18
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
19
|
+
|
|
20
|
+
/** JSON inside a <script> element, safe against `</script>` in any value. */
|
|
21
|
+
export const scriptJson = (v) => JSON.stringify(v).replace(/</g, '\\u003c')
|
|
22
|
+
.replace(new RegExp('\\u2028', 'g'), '\\u2028').replace(new RegExp('\\u2029', 'g'), '\\u2029');
|
|
23
|
+
|
|
24
|
+
/** Small inline icons - the storefront must not depend on an icon font being there. */
|
|
25
|
+
export const ICON = {
|
|
26
|
+
bag: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 7h12l-1 13H7L6 7z"/><path d="M9 7a3 3 0 0 1 6 0"/></svg>',
|
|
27
|
+
plus: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>',
|
|
28
|
+
search: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>',
|
|
29
|
+
check: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5L20 7"/></svg>',
|
|
30
|
+
lock: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>',
|
|
31
|
+
truck: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h11v10H3zM14 10h4l3 3v3h-7z"/><circle cx="7" cy="18" r="2"/><circle cx="17" cy="18" r="2"/></svg>',
|
|
32
|
+
back: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg>'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** "£24", "From £8", with the old price struck through when there is one. */
|
|
36
|
+
export function priceHtml(p, currency) {
|
|
37
|
+
const r = priceRange(p);
|
|
38
|
+
const from = r.min !== r.max ? '<span class="shop-from">From</span> ' : '';
|
|
39
|
+
const was = Number.isInteger(p.compareAt) && p.compareAt > r.min
|
|
40
|
+
? ` <s class="shop-was">${esc(formatMoney(p.compareAt, currency, {compact: true}))}</s>` : '';
|
|
41
|
+
return `${from}<span class="shop-now">${esc(formatMoney(r.min, currency, {compact: true}))}</span>${was}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Sold out, only a few left, on sale - what a card or a product page flags. */
|
|
45
|
+
export function flags(p, settings) {
|
|
46
|
+
const out = [];
|
|
47
|
+
const left = p.variants?.length
|
|
48
|
+
? (p.trackStock ? p.variants.reduce((s, v) => s + (Number(v.stock) || 0), 0) : Infinity)
|
|
49
|
+
: available(p);
|
|
50
|
+
if (left === 0) out.push({key: 'soldout', text: 'Sold out'});
|
|
51
|
+
else if (Number.isInteger(p.compareAt) && p.compareAt > p.price) out.push({key: 'sale', text: 'Sale'});
|
|
52
|
+
if (left > 0 && left !== Infinity && settings.showStockLeft && left <= (Number(settings.lowStockAt) || 0)) out.push({key: 'low', text: `Only ${left} left`});
|
|
53
|
+
if (p.featured && !out.length) out.push({key: 'featured', text: 'Featured'});
|
|
54
|
+
return {list: out, soldOut: left === 0};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* One product card. `data-*` carry what the basket needs to add it without
|
|
59
|
+
* another request: id, name, the picture, the one-option price.
|
|
60
|
+
*/
|
|
61
|
+
export function cardHtml(entry, settings, base) {
|
|
62
|
+
const p = entry.data;
|
|
63
|
+
const cur = settings.currency;
|
|
64
|
+
const img = safeImage(p.images?.[0]);
|
|
65
|
+
const alt = safeImage(p.images?.[1]);
|
|
66
|
+
const {list, soldOut} = flags(p, settings);
|
|
67
|
+
const href = `${base}/${encodeURIComponent(p.slug)}`;
|
|
68
|
+
const quick = !soldOut && !(p.variants?.length);
|
|
69
|
+
return `<article class="shop-card${soldOut ? ' is-soldout' : ''}" data-shop-card data-id="${esc(entry.id)}"
|
|
70
|
+
data-name="${esc(p.name)}" data-category="${esc(p.category || '')}" data-price="${p.price}" data-featured="${p.featured ? 1 : 0}"
|
|
71
|
+
data-search="${esc([p.name, p.summary, p.category, ...(p.tags || [])].join(' ').toLowerCase())}">
|
|
72
|
+
<a class="shop-card-media" href="${esc(href)}" tabindex="-1" aria-hidden="true">
|
|
73
|
+
${img ? `<img src="${esc(img)}" alt="" loading="lazy" decoding="async">` : '<span class="shop-card-noimg">' + ICON.bag + '</span>'}
|
|
74
|
+
${alt ? `<img class="shop-card-alt" src="${esc(alt)}" alt="" loading="lazy" decoding="async">` : ''}
|
|
75
|
+
${list.length ? `<span class="shop-flags">${list.map(f => `<span class="shop-flag is-${f.key}">${esc(f.text)}</span>`).join('')}</span>` : ''}
|
|
76
|
+
</a>
|
|
77
|
+
<div class="shop-card-body">
|
|
78
|
+
${p.category ? `<span class="shop-card-cat">${esc(p.category)}</span>` : ''}
|
|
79
|
+
<h3 class="shop-card-title"><a href="${esc(href)}">${esc(p.name)}</a></h3>
|
|
80
|
+
${p.summary ? `<p class="shop-card-summary">${esc(p.summary)}</p>` : ''}
|
|
81
|
+
<div class="shop-card-foot">
|
|
82
|
+
<span class="shop-price">${priceHtml(p, cur)}</span>
|
|
83
|
+
${quick
|
|
84
|
+
? `<button type="button" class="shop-quick" data-shop-add data-id="${esc(entry.id)}" data-name="${esc(p.name)}" data-image="${esc(img)}" aria-label="Add ${esc(p.name)} to basket">${ICON.plus}<span>Add</span></button>`
|
|
85
|
+
: `<a class="shop-quick is-link" href="${esc(href)}" aria-label="${soldOut ? 'Sold out' : 'Choose options for'} ${esc(p.name)}">${soldOut ? 'Sold out' : 'Choose'}</a>`}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</article>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function gridHtml(entries, settings, base, {columns} = {}) {
|
|
92
|
+
const cols = [2, 3, 4].includes(Number(columns)) ? Number(columns) : (settings.columns || 3);
|
|
93
|
+
return `<div class="shop-grid" style="--shop-cols:${cols}" data-shop-grid>${entries.map(e => cardHtml(e, settings, base)).join('\n')}</div>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The storefront: header, toolbar, grid. */
|
|
97
|
+
export function storefrontHtml({entries, settings, base, category = '', query = ''}) {
|
|
98
|
+
const cats = [...new Set(entries.map(e => e.data.category).filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
|
99
|
+
const chips = settings.showCategories && cats.length > 1
|
|
100
|
+
? `<div class="shop-chips" role="group" aria-label="Categories">
|
|
101
|
+
<button type="button" class="shop-chip${category ? '' : ' is-on'}" data-shop-cat="">All</button>
|
|
102
|
+
${cats.map(c => `<button type="button" class="shop-chip${c === category ? ' is-on' : ''}" data-shop-cat="${esc(c)}">${esc(c)}</button>`).join('')}
|
|
103
|
+
</div>` : '';
|
|
104
|
+
const search = settings.showSearch && entries.length > 4
|
|
105
|
+
? `<label class="shop-search">${ICON.search}<input type="search" placeholder="Search the shop" value="${esc(query)}" data-shop-search aria-label="Search the shop"></label>` : '';
|
|
106
|
+
const sort = entries.length > 4 ? `<select class="shop-sort" data-shop-sort aria-label="Sort">
|
|
107
|
+
<option value="">Featured</option><option value="price-asc">Price, low to high</option>
|
|
108
|
+
<option value="price-desc">Price, high to low</option><option value="name">Name</option></select>` : '';
|
|
109
|
+
const intro = settings.shopIntro ? `<p class="shop-intro">${esc(settings.shopIntro)}</p>` : '';
|
|
110
|
+
return `<header class="shop-hero">
|
|
111
|
+
<h1>${esc(settings.shopTitle || 'Shop')}</h1>${intro}
|
|
112
|
+
</header>
|
|
113
|
+
${chips || search || sort ? `<div class="shop-toolbar">${chips}<div class="shop-tools">${search}${sort}</div></div>` : ''}
|
|
114
|
+
${entries.length
|
|
115
|
+
? gridHtml(entries, settings, base)
|
|
116
|
+
: '<div class="shop-empty"><p>Nothing on sale just yet - come back soon.</p></div>'}
|
|
117
|
+
<p class="shop-none" data-shop-none hidden>Nothing matches. <button type="button" class="shop-linkbtn" data-shop-clear>Show everything</button></p>`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A product's page. */
|
|
121
|
+
export function productHtml({entry, settings, base, descriptionHtml, related}) {
|
|
122
|
+
const p = entry.data;
|
|
123
|
+
const cur = settings.currency;
|
|
124
|
+
const images = (p.images || []).map(safeImage).filter(Boolean);
|
|
125
|
+
const {list, soldOut} = flags(p, settings);
|
|
126
|
+
const variants = p.variants || [];
|
|
127
|
+
const firstOpen = variants.find(v => available(p, v.id) > 0);
|
|
128
|
+
const opts = variants.length ? `<fieldset class="shop-options">
|
|
129
|
+
<legend>${esc(p.optionName || 'Option')}</legend>
|
|
130
|
+
<div class="shop-option-list">
|
|
131
|
+
${variants.map(v => {
|
|
132
|
+
const left = available(p, v.id);
|
|
133
|
+
const price = Number.isInteger(v.price) ? v.price : p.price;
|
|
134
|
+
return `<label class="shop-option${left <= 0 ? ' is-out' : ''}">
|
|
135
|
+
<input type="radio" name="variant" value="${esc(v.id)}" data-price="${price}" data-left="${left === Infinity ? '' : left}"${left <= 0 ? ' disabled' : ''}${firstOpen?.id === v.id ? ' checked' : ''}>
|
|
136
|
+
<span>${esc(v.name)}${price !== p.price ? ` <small>${esc(formatMoney(price, cur, {compact: true}))}</small>` : ''}</span></label>`;
|
|
137
|
+
}).join('')}
|
|
138
|
+
</div>
|
|
139
|
+
</fieldset>` : '';
|
|
140
|
+
const left = variants.length ? (firstOpen ? available(p, firstOpen.id) : 0) : available(p);
|
|
141
|
+
const startPrice = firstOpen && Number.isInteger(firstOpen.price) ? firstOpen.price : p.price;
|
|
142
|
+
const gallery = images.length
|
|
143
|
+
? `<div class="shop-gallery" data-shop-gallery>
|
|
144
|
+
<div class="shop-gallery-main"><img src="${esc(images[0])}" alt="${esc(p.name)}" data-shop-main>
|
|
145
|
+
${list.length ? `<span class="shop-flags">${list.map(f => `<span class="shop-flag is-${f.key}">${esc(f.text)}</span>`).join('')}</span>` : ''}</div>
|
|
146
|
+
${images.length > 1 ? `<div class="shop-thumbs">${images.map((src, i) => `<button type="button" class="shop-thumb${i ? '' : ' is-on'}" data-src="${esc(src)}" aria-label="Picture ${i + 1}"><img src="${esc(src)}" alt="" loading="lazy"></button>`).join('')}</div>` : ''}
|
|
147
|
+
</div>`
|
|
148
|
+
: `<div class="shop-gallery"><div class="shop-gallery-main is-empty">${ICON.bag}</div></div>`;
|
|
149
|
+
|
|
150
|
+
return `<nav class="shop-crumbs" aria-label="Breadcrumb"><a href="${esc(base)}">${ICON.back}${esc(settings.shopTitle || 'Shop')}</a>${p.category ? ` <span>/</span> <a href="${esc(base)}?category=${encodeURIComponent(p.category)}">${esc(p.category)}</a>` : ''}</nav>
|
|
151
|
+
<div class="shop-product" data-shop-product data-id="${esc(entry.id)}" data-name="${esc(p.name)}" data-image="${esc(images[0] || '')}">
|
|
152
|
+
${gallery}
|
|
153
|
+
<div class="shop-buy">
|
|
154
|
+
${p.category ? `<span class="shop-card-cat">${esc(p.category)}</span>` : ''}
|
|
155
|
+
<h1 class="shop-product-title">${esc(p.name)}</h1>
|
|
156
|
+
<p class="shop-product-price"><span class="shop-now" data-shop-price>${esc(formatMoney(startPrice, cur))}</span>${Number.isInteger(p.compareAt) && p.compareAt > startPrice ? ` <s class="shop-was">${esc(formatMoney(p.compareAt, cur))}</s>` : ''}</p>
|
|
157
|
+
${p.summary ? `<p class="shop-product-summary">${esc(p.summary)}</p>` : ''}
|
|
158
|
+
<form class="shop-buy-form" data-shop-buy>
|
|
159
|
+
${opts}
|
|
160
|
+
<div class="shop-buy-row">
|
|
161
|
+
<div class="shop-qty" data-shop-qty>
|
|
162
|
+
<button type="button" data-step="-1" aria-label="One fewer">−</button>
|
|
163
|
+
<input type="number" name="qty" value="1" min="1" max="${left === Infinity ? 999 : Math.max(1, left)}" inputmode="numeric" aria-label="Quantity">
|
|
164
|
+
<button type="button" data-step="1" aria-label="One more">+</button>
|
|
165
|
+
</div>
|
|
166
|
+
<button type="submit" class="shop-add-btn" data-shop-addbtn${soldOut ? ' disabled' : ''}>${ICON.bag}<span>${soldOut ? 'Sold out' : 'Add to basket'}</span></button>
|
|
167
|
+
</div>
|
|
168
|
+
<p class="shop-stock" data-shop-stock>${soldOut ? '' : left !== Infinity && settings.showStockLeft && left <= (Number(settings.lowStockAt) || 0) ? `Only ${left} left` : ''}</p>
|
|
169
|
+
</form>
|
|
170
|
+
<ul class="shop-assure">
|
|
171
|
+
${p.shippable !== false ? `<li>${ICON.truck}<span>${Number(settings.freeShippingOver) > 0 ? `Free delivery over ${esc(formatMoney(settings.freeShippingOver, cur, {compact: true}))}` : esc(settings.shippingLabel || 'Delivery')}${settings.allowCollection ? ` · or ${esc(String(settings.collectionLabel || 'collect').toLowerCase())}` : ''}</span></li>` : `<li>${ICON.check}<span>Nothing to ship - yours as soon as it is paid</span></li>`}
|
|
172
|
+
<li>${ICON.lock}<span>Secure checkout</span></li>
|
|
173
|
+
</ul>
|
|
174
|
+
${descriptionHtml ? `<div class="shop-description">${descriptionHtml}</div>` : ''}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
${related.length ? `<section class="shop-related"><h2>You may also like</h2>${gridHtml(related, settings, base, {columns: 4})}</section>` : ''}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The basket and checkout, one page. The script fills it from the basket in the browser. */
|
|
181
|
+
export function cartHtml({settings, base}) {
|
|
182
|
+
const countries = (settings.countries || ['GB']).map(c => `<option value="${esc(c)}">${esc(COUNTRIES[c] || c)}</option>`).join('');
|
|
183
|
+
return `<nav class="shop-crumbs" aria-label="Breadcrumb"><a href="${esc(base)}">${ICON.back}Keep shopping</a></nav>
|
|
184
|
+
<h1 class="shop-co-title">Your basket</h1>
|
|
185
|
+
<div class="shop-checkout" data-shop-checkout>
|
|
186
|
+
<div class="shop-co-main">
|
|
187
|
+
<div class="shop-co-lines" data-co-lines><p class="shop-muted">Loading your basket…</p></div>
|
|
188
|
+
<form class="shop-co-form" data-co-form novalidate hidden>
|
|
189
|
+
<section class="shop-co-step">
|
|
190
|
+
<h2><span class="shop-step">1</span> Your details</h2>
|
|
191
|
+
<div class="shop-fields">
|
|
192
|
+
<label class="shop-field is-wide"><span>Name</span><input name="name" autocomplete="name" required maxlength="120"></label>
|
|
193
|
+
<label class="shop-field"><span>Email</span><input name="email" type="email" autocomplete="email" required maxlength="200"></label>
|
|
194
|
+
<label class="shop-field"><span>Phone <em>optional</em></span><input name="phone" type="tel" autocomplete="tel" maxlength="40"></label>
|
|
195
|
+
</div>
|
|
196
|
+
</section>
|
|
197
|
+
<section class="shop-co-step" data-co-delivery hidden>
|
|
198
|
+
<h2><span class="shop-step">2</span> Delivery</h2>
|
|
199
|
+
<div class="shop-choice-row" data-co-collect hidden>
|
|
200
|
+
<label class="shop-choice"><input type="radio" name="delivery" value="ship" checked><span>${ICON.truck} Deliver to me</span></label>
|
|
201
|
+
<label class="shop-choice"><input type="radio" name="delivery" value="collect"><span>${ICON.bag} ${esc(settings.collectionLabel || 'Collect in person')}</span></label>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="shop-methods" data-co-services hidden></div>
|
|
204
|
+
<div class="shop-fields" data-co-address>
|
|
205
|
+
<label class="shop-field is-wide"><span>Address</span><input name="line1" autocomplete="address-line1" maxlength="160"></label>
|
|
206
|
+
<label class="shop-field is-wide"><span class="sr-only">Address line 2</span><input name="line2" autocomplete="address-line2" maxlength="160" placeholder="Flat, building (optional)"></label>
|
|
207
|
+
<label class="shop-field"><span>Town or city</span><input name="city" autocomplete="address-level2" maxlength="100"></label>
|
|
208
|
+
<label class="shop-field"><span>County / state <em>optional</em></span><input name="region" autocomplete="address-level1" maxlength="100"></label>
|
|
209
|
+
<label class="shop-field"><span>Postcode</span><input name="postcode" autocomplete="postal-code" maxlength="20"></label>
|
|
210
|
+
<label class="shop-field"><span>Country</span><select name="country" autocomplete="country">${countries}</select></label>
|
|
211
|
+
</div>
|
|
212
|
+
</section>
|
|
213
|
+
<section class="shop-co-step">
|
|
214
|
+
<h2><span class="shop-step" data-co-paystep>3</span> Payment</h2>
|
|
215
|
+
<div class="shop-methods" data-co-methods></div>
|
|
216
|
+
<label class="shop-field is-wide"><span>A note with your order <em>optional</em></span><textarea name="note" rows="2" maxlength="1000"></textarea></label>
|
|
217
|
+
${settings.termsUrl ? `<label class="shop-terms"><input type="checkbox" name="terms" required> <span>I agree to the <a href="${esc(settings.termsUrl)}" target="_blank" rel="noopener">terms of sale</a></span></label>` : ''}
|
|
218
|
+
<p class="shop-co-error" data-co-error role="alert" hidden></p>
|
|
219
|
+
<button type="submit" class="shop-pay-btn" data-co-pay>${ICON.lock}<span>Pay</span></button>
|
|
220
|
+
<p class="shop-secure">${ICON.lock} Payment happens on a secure page run by the payment provider. We never see your card.</p>
|
|
221
|
+
</section>
|
|
222
|
+
</form>
|
|
223
|
+
</div>
|
|
224
|
+
<aside class="shop-co-summary" data-co-summary hidden>
|
|
225
|
+
<h2>Summary</h2>
|
|
226
|
+
<div data-co-totals></div>
|
|
227
|
+
<form class="shop-code" data-co-code hidden>
|
|
228
|
+
<input name="code" placeholder="Discount code" autocomplete="off" maxlength="60" aria-label="Discount code">
|
|
229
|
+
<button type="submit">Apply</button>
|
|
230
|
+
</form>
|
|
231
|
+
</aside>
|
|
232
|
+
</div>`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A shopper's receipt. */
|
|
236
|
+
export function receiptHtml({order, settings, base, problem = ''}) {
|
|
237
|
+
const cur = order.currency;
|
|
238
|
+
const status = order.status;
|
|
239
|
+
const waiting = status === 'pending';
|
|
240
|
+
const steps = [
|
|
241
|
+
{key: 'placed', label: 'Order placed', done: true},
|
|
242
|
+
{key: 'paid', label: status === 'awaiting' ? 'Awaiting payment' : 'Paid', done: ['paid', 'fulfilled'].includes(status)},
|
|
243
|
+
{key: 'sent', label: order.delivery === 'collect' ? 'Ready to collect' : order.delivery === 'none' ? 'Delivered' : 'Sent', done: status === 'fulfilled'}
|
|
244
|
+
];
|
|
245
|
+
const closed = status === 'cancelled' || status === 'refunded';
|
|
246
|
+
const lines = order.lines.map(l => `<li class="shop-r-line">
|
|
247
|
+
${l.image ? `<img src="${esc(safeImage(l.image))}" alt="" loading="lazy">` : `<span class="shop-r-noimg">${ICON.bag}</span>`}
|
|
248
|
+
<span class="shop-r-name">${esc(l.name)}${l.variantName ? `<small>${esc(l.optionName || 'Option')}: ${esc(l.variantName)}</small>` : ''}</span>
|
|
249
|
+
<span class="shop-r-qty">× ${l.qty}</span>
|
|
250
|
+
<span class="shop-r-amt">${esc(formatMoney(l.lineTotal, cur))}</span></li>`).join('');
|
|
251
|
+
const totals = `<dl class="shop-totals">
|
|
252
|
+
<div><dt>Subtotal</dt><dd>${esc(formatMoney(order.subtotal, cur))}</dd></div>
|
|
253
|
+
${order.adjustments.map(a => `<div class="${a.included ? 'is-included' : a.amount < 0 ? 'is-discount' : ''}"><dt>${esc(a.label)}</dt><dd>${a.amount === 0 && a.stage === 'shipping' ? 'Free' : esc(formatMoney(a.amount, cur))}</dd></div>`).join('')}
|
|
254
|
+
<div class="is-total"><dt>Total</dt><dd>${esc(formatMoney(order.total, cur))}</dd></div>
|
|
255
|
+
</dl>`;
|
|
256
|
+
const addr = addressLines(order.address);
|
|
257
|
+
const t = order.tracking;
|
|
258
|
+
return `<div class="shop-receipt${closed ? ' is-closed' : ''}" data-shop-receipt data-status="${esc(status)}" data-order="${esc(order.id)}">
|
|
259
|
+
<div class="shop-r-head">
|
|
260
|
+
<span class="shop-r-badge${waiting ? ' is-waiting' : ''}">${waiting ? '<span class="shop-spinner"></span>' : ICON.check}</span>
|
|
261
|
+
<div>
|
|
262
|
+
<p class="shop-muted">Order #${esc(order.number)}</p>
|
|
263
|
+
<h1>${closed ? esc(STATUS_PUBLIC[status]) : waiting ? 'Confirming your payment…' : `Thank you${order.customer?.name ? `, ${esc(order.customer.name.split(' ')[0])}` : ''}!`}</h1>
|
|
264
|
+
<p>${waiting ? 'This takes a moment. You can keep this page open.'
|
|
265
|
+
: status === 'awaiting' ? 'Your order is in. It is sent as soon as your payment arrives.'
|
|
266
|
+
: closed ? '' : `A record of your order is below${order.email ? ` - we will write to <strong>${esc(order.email)}</strong> if anything changes` : ''}.`}</p>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
${problem === 'paypal' && waiting ? '<p class="shop-co-error">PayPal did not confirm the payment. Nothing has been taken - <a href="' + esc(base) + '/cart">try again from your basket</a>.</p>' : ''}
|
|
270
|
+
${status === 'awaiting' && settings.offlineInstructions ? `<div class="shop-r-note"><strong>${esc(settings.offlineLabel || 'How to pay')}</strong><p>${esc(settings.offlineInstructions)}</p><p>Please quote <strong>#${esc(order.number)}</strong> with your payment - ${esc(formatMoney(order.total, cur))}.</p></div>` : ''}
|
|
271
|
+
${closed ? '' : `<ol class="shop-r-steps">${steps.map(s => `<li class="${s.done ? 'is-done' : ''}"><span></span>${esc(s.label)}</li>`).join('')}</ol>`}
|
|
272
|
+
${t ? `<div class="shop-r-note">${ICON.truck} <strong>${esc(t.carrier || 'Tracking')}</strong> ${esc(t.number || '')} ${t.url ? `<a href="${esc(t.url)}" target="_blank" rel="noopener">Track it</a>` : ''}</div>` : ''}
|
|
273
|
+
<div class="shop-r-grid">
|
|
274
|
+
<section><h2>Items</h2><ul class="shop-r-lines">${lines}</ul>${totals}</section>
|
|
275
|
+
<section class="shop-r-side">
|
|
276
|
+
${addr.length ? `<h2>${order.delivery === 'collect' ? 'Collection' : 'Delivering to'}</h2><address>${addr.map(esc).join('<br>')}</address>` : order.delivery === 'collect' ? `<h2>Collection</h2><p>${esc(settings.collectionLabel || 'Collect in person')}</p>` : ''}
|
|
277
|
+
<h2>Placed</h2><p>${esc(new Date(order.placedAt).toLocaleString('en-GB', {dateStyle: 'long', timeStyle: 'short'}))}</p>
|
|
278
|
+
<p><a class="shop-linkbtn" href="${esc(base)}">Continue shopping</a> · <button type="button" class="shop-linkbtn" data-shop-print>Print</button></p>
|
|
279
|
+
</section>
|
|
280
|
+
</div>
|
|
281
|
+
</div>`;
|
|
282
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample products - so a new shop can be seen working before its own stock is
|
|
3
|
+
* photographed. Pictures are shipped with the plugin (public/samples/). They
|
|
4
|
+
* are ordinary products once added: edit them, or delete them.
|
|
5
|
+
*
|
|
6
|
+
* @module shopping-cart/lib/samples
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const pic = (n) => [`/plugins/shopping-cart/public/samples/${n}.svg`, `/plugins/shopping-cart/public/samples/${n}-2.svg`];
|
|
10
|
+
|
|
11
|
+
/** Prices in minor units. */
|
|
12
|
+
export const SAMPLES = [
|
|
13
|
+
{name: 'Stoneware Mug', category: 'Kitchen', price: 1800, images: pic('mug'), featured: true, trackStock: true, stock: 24, weight: 420,
|
|
14
|
+
summary: 'Hand-thrown, glazed in deep slate. Holds a proper cup of tea.',
|
|
15
|
+
description: 'Thrown on the wheel and glazed by hand, so no two are quite alike.\n\n- 350 ml\n- Dishwasher and microwave safe\n- Made in Yorkshire'},
|
|
16
|
+
{name: 'Canvas Tote', category: 'Accessories', price: 1500, compareAt: 2000, images: pic('tote'), weight: 180,
|
|
17
|
+
summary: 'Heavy organic cotton with a stitched globe. Carries a week of shopping.',
|
|
18
|
+
description: 'Twelve-ounce organic cotton canvas, long handles, an inside pocket for keys.'},
|
|
19
|
+
{name: 'Fig & Cedar Candle', category: 'Home', price: 2400, images: pic('candle'), trackStock: true, weight: 380, optionName: 'Size',
|
|
20
|
+
variants: [{name: 'Small · 20 h', price: 1600, stock: 12}, {name: 'Large · 45 h', price: 2400, stock: 2}],
|
|
21
|
+
summary: 'Soy wax, cotton wick, the smell of a late-summer orchard.',
|
|
22
|
+
description: 'Hand-poured soy wax in a reusable glass. Burn for two hours at a time for an even pool.'},
|
|
23
|
+
{name: 'Linen Notebook', category: 'Stationery', price: 1200, images: pic('notebook'), weight: 260, optionName: 'Paper',
|
|
24
|
+
variants: [{name: 'Lined'}, {name: 'Dotted'}, {name: 'Plain'}],
|
|
25
|
+
summary: 'A5, 192 pages of 100 gsm paper that fountain pens love.',
|
|
26
|
+
description: 'Lay-flat binding, a ribbon marker and a pocket at the back.'},
|
|
27
|
+
{name: 'Sunset Tee', category: 'Clothing', price: 2200, images: pic('tee'), featured: true, trackStock: true, weight: 200, optionName: 'Size',
|
|
28
|
+
variants: [{name: 'S', stock: 6}, {name: 'M', stock: 9}, {name: 'L', stock: 4}, {name: 'XL', stock: 0}],
|
|
29
|
+
summary: 'Soft organic cotton, screen-printed by hand.',
|
|
30
|
+
description: 'A relaxed fit in 180 gsm organic cotton. Wash cool, inside out.'},
|
|
31
|
+
{name: '"Dusk" Art Print', category: 'Prints', price: 3500, images: pic('print'), weight: 300,
|
|
32
|
+
summary: 'Giclée print on 310 gsm cotton rag, A3, signed.',
|
|
33
|
+
description: 'Printed with archival pigment inks and signed on the back. Frame not included.'}
|
|
34
|
+
];
|