diff --git a/backend/src/config/storefrontAssetPaths.ts b/backend/src/config/storefrontAssetPaths.ts index 8135603..7f698dc 100644 --- a/backend/src/config/storefrontAssetPaths.ts +++ b/backend/src/config/storefrontAssetPaths.ts @@ -4,9 +4,13 @@ export const storefrontImgUrlPrefix = '/shop/assets/img'; export const storefrontCssUrlPrefix = '/shop/assets/css'; +export const storefrontJsUrlPrefix = '/shop/assets/js'; + export const getStorefrontPublicDir = (): string => path.join(__dirname, '..', 'modules', 'storefrontCore', 'public'); export const getStorefrontImgDir = (): string => path.join(getStorefrontPublicDir(), 'img'); export const getStorefrontCssDir = (): string => path.join(getStorefrontPublicDir(), 'css'); + +export const getStorefrontJsDir = (): string => path.join(getStorefrontPublicDir(), 'js'); diff --git a/backend/src/main.ts b/backend/src/main.ts index 4f5f5ec..2f3ab93 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -8,8 +8,10 @@ import path from 'node:path'; import { getStorefrontCssDir, getStorefrontImgDir, + getStorefrontJsDir, storefrontCssUrlPrefix, - storefrontImgUrlPrefix + storefrontImgUrlPrefix, + storefrontJsUrlPrefix } from './config/storefrontAssetPaths'; import { getPublicUploadsDir, publicUploadsUrlPrefix } from './config/uploadPaths'; import { AppModule } from './AppModule'; @@ -42,6 +44,10 @@ async function bootstrap() { prefix: storefrontCssUrlPrefix }); + app.useStaticAssets(getStorefrontJsDir(), { + prefix: storefrontJsUrlPrefix + }); + app.use(cookieParser()); if (nodeEnv === NodeEnv.Production) { diff --git a/backend/src/modules/storefrontCore/public/css/storefront.css b/backend/src/modules/storefrontCore/public/css/storefront.css index 7921f9e..c19dd59 100644 --- a/backend/src/modules/storefrontCore/public/css/storefront.css +++ b/backend/src/modules/storefrontCore/public/css/storefront.css @@ -159,6 +159,26 @@ box-sizing: border-box; } +.sf-js-only { + display: none; +} + +.js .sf-js-only { + display: block; +} + +.js .sf-js-only.sf-copy-btn { + display: inline-flex; +} + +.sf-nojs-only { + display: block; +} + +.js .sf-nojs-only { + display: none; +} + html { scroll-padding-top: var(--sf-sticky-header-scroll-padding); } @@ -629,6 +649,10 @@ a:hover { border-color 0.1s; } +.sf-btn.sf-js-only { + display: none; +} + .sf-btn:hover { color: var(--sf-accent); background: var(--sf-accent-subtle); @@ -1302,6 +1326,64 @@ a:hover { font-size: 0.9rem; } +.sf-copy-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--sf-space-2); +} + +.sf-copy-row .sf-payment-amount { + margin: 0; +} + +.sf-copy-row--inline { + display: inline-flex; + vertical-align: baseline; +} + +.sf-copy-row--address { + flex-wrap: nowrap; + align-items: center; + margin-bottom: var(--sf-space-4); +} + +.sf-copy-row__value { + flex: 1 1 auto; + min-width: 0; + margin: 0; + word-break: break-all; +} + +.sf-copy-btn { + flex-shrink: 0; + min-width: 1.375rem; + min-height: 1.375rem; + padding: 0.125rem; +} + +.sf-copy-btn__icon { + display: block; + line-height: 0; +} + +.sf-copy-btn__icon svg { + width: 0.875rem; + height: 0.875rem; +} + +.sf-copy-btn__icon--success { + display: none; +} + +.sf-copy-btn--copied .sf-copy-btn__icon--copy { + display: none; +} + +.sf-copy-btn--copied .sf-copy-btn__icon--success { + display: block; +} + .sf-order-status { font-weight: 600; text-transform: capitalize; diff --git a/backend/src/modules/storefrontCore/public/js/copy.js b/backend/src/modules/storefrontCore/public/js/copy.js new file mode 100644 index 0000000..66d61ce --- /dev/null +++ b/backend/src/modules/storefrontCore/public/js/copy.js @@ -0,0 +1,52 @@ +(() => { + const copiedAriaLabel = 'Copied'; + const resetMs = 2000; + + const copyText = async text => { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + + return; + } + + const textarea = document.createElement('textarea'); + + textarea.value = text; + textarea.setAttribute('readonly', ''); + textarea.style.position = 'fixed'; + textarea.style.left = '-9999px'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + }; + + const handleCopyClick = async event => { + const button = event.currentTarget; + const text = button.dataset.copyText; + + if (!text) { + return; + } + + const defaultAriaLabel = button.dataset.copyAriaLabel ?? 'Copy'; + + try { + await copyText(text); + + button.classList.add('sf-copy-btn--copied'); + button.setAttribute('aria-label', copiedAriaLabel); + + window.setTimeout(() => { + button.classList.remove('sf-copy-btn--copied'); + button.setAttribute('aria-label', defaultAriaLabel); + }, resetMs); + } catch (error) { + console.error('Copy to clipboard failed', error); + } + }; + + for (const button of document.querySelectorAll('[data-action="copy"]')) { + button.addEventListener('click', handleCopyClick); + } +})(); diff --git a/backend/src/modules/storefrontCore/public/js/js-enabled.js b/backend/src/modules/storefrontCore/public/js/js-enabled.js new file mode 100644 index 0000000..62a1cdc --- /dev/null +++ b/backend/src/modules/storefrontCore/public/js/js-enabled.js @@ -0,0 +1 @@ +document.documentElement.classList.replace('no-js', 'js'); diff --git a/backend/src/modules/storefrontCore/utils/registerStorefrontPartials.ts b/backend/src/modules/storefrontCore/utils/registerStorefrontPartials.ts index 78d1fbf..5c7eae1 100644 --- a/backend/src/modules/storefrontCore/utils/registerStorefrontPartials.ts +++ b/backend/src/modules/storefrontCore/utils/registerStorefrontPartials.ts @@ -12,6 +12,7 @@ const STOREFRONT_PARTIALS = [ { name: 'cart-line-item', file: 'cart-line-item.hbs' }, { name: 'checkout-line-item', file: 'checkout-line-item.hbs' }, { name: 'confirm-action-details', file: 'confirm-action-details.hbs' }, + { name: 'copy-button', file: 'copy-button.hbs' }, { name: 'dismiss-button', file: 'dismiss-button.hbs' }, { name: 'order-line-item', file: 'order-line-item.hbs' }, { name: 'order-digital-delivery', file: 'order-digital-delivery.hbs' }, diff --git a/backend/src/modules/storefrontCore/views/checkout.hbs b/backend/src/modules/storefrontCore/views/checkout.hbs index 8e6ca82..c7fe3ff 100644 --- a/backend/src/modules/storefrontCore/views/checkout.hbs +++ b/backend/src/modules/storefrontCore/views/checkout.hbs @@ -45,3 +45,5 @@ + + diff --git a/backend/src/modules/storefrontCore/views/layouts/shop-minimal.hbs b/backend/src/modules/storefrontCore/views/layouts/shop-minimal.hbs index f44f3ec..a8e1771 100644 --- a/backend/src/modules/storefrontCore/views/layouts/shop-minimal.hbs +++ b/backend/src/modules/storefrontCore/views/layouts/shop-minimal.hbs @@ -1,6 +1,7 @@ html-theme-attributes}} >
diff --git a/backend/src/modules/storefrontCore/views/layouts/shop.hbs b/backend/src/modules/storefrontCore/views/layouts/shop.hbs index fd20b11..2d6dcde 100644 --- a/backend/src/modules/storefrontCore/views/layouts/shop.hbs +++ b/backend/src/modules/storefrontCore/views/layouts/shop.hbs @@ -1,6 +1,7 @@ html-theme-attributes}} > diff --git a/backend/src/modules/storefrontCore/views/order.hbs b/backend/src/modules/storefrontCore/views/order.hbs index e26b290..9823215 100644 --- a/backend/src/modules/storefrontCore/views/order.hbs +++ b/backend/src/modules/storefrontCore/views/order.hbs @@ -69,3 +69,5 @@ {{/if}} + + diff --git a/backend/src/modules/storefrontCore/views/partials/copy-button.hbs b/backend/src/modules/storefrontCore/views/partials/copy-button.hbs new file mode 100644 index 0000000..d9b5eb1 --- /dev/null +++ b/backend/src/modules/storefrontCore/views/partials/copy-button.hbs @@ -0,0 +1,20 @@ + diff --git a/backend/src/modules/storefrontCore/views/partials/invoice-payment.hbs b/backend/src/modules/storefrontCore/views/partials/invoice-payment.hbs index 60ad578..25c4492 100644 --- a/backend/src/modules/storefrontCore/views/partials/invoice-payment.hbs +++ b/backend/src/modules/storefrontCore/views/partials/invoice-payment.hbs @@ -3,7 +3,7 @@ {{/if}} {{#if showProminentAmount}} -{{expectedTotalCrypto}} {{paymentLabel}}
+{{instructionAmountCrypto}} {{paymentLabel}}
{{/if}} {{#if showReceivedTotal}} @@ -23,7 +23,13 @@ {{#if showInstruction}}{{instructionPrefix}} - {{instructionAmountCrypto}} {{paymentLabel}} + + {{instructionAmountCrypto}} {{paymentLabel}} + {{> copy-button + copyText=instructionAmountCrypto + ariaLabel='Copy payment amount' + }} + {{instructionSuffix}} {{#if showExpiry}} Payment expires in @@ -40,7 +46,13 @@ {{#if showAddress}}
{{#if showQr}}Address{{else}}Payment address{{/if}}
-{{paymentAddress}}
+{{paymentAddress}}
+ {{> copy-button + copyText=paymentAddress + ariaLabel='Copy payment address' + }} +