From 135a30f1cdee3e8ec656ed062c989d78fedc5f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 20 Aug 2026 14:58:00 -0600 Subject: [PATCH] Refactor markup and styles to re-use existing code where possible --- app/components/modal.gjs | 22 +- app/components/photo-gallery.gjs | 2 +- app/components/zap-photo-modal.gjs | 350 ++++++++---------- app/styles/app.css | 326 +++++----------- .../components/zap-photo-modal-test.gjs | 56 ++- 5 files changed, 290 insertions(+), 466 deletions(-) diff --git a/app/components/modal.gjs b/app/components/modal.gjs index e673e05..37bc88e 100644 --- a/app/components/modal.gjs +++ b/app/components/modal.gjs @@ -30,6 +30,10 @@ export default class Modal extends Component { return config.environment === 'test'; } + get shouldPortal() { + return !this.isTesting && !this.args.inline; + } + get destinationElement() { return document.getElementById('modal-portal') || document.body; } @@ -48,15 +52,7 @@ export default class Modal extends Component { } } diff --git a/app/components/photo-gallery.gjs b/app/components/photo-gallery.gjs index 4da2e5b..9b7748d 100644 --- a/app/components/photo-gallery.gjs +++ b/app/components/photo-gallery.gjs @@ -180,7 +180,7 @@ export default class PhotoGallery extends Component { e.target.closest('.carousel-nav-btn') || e.target.closest('.close-btn') || e.target.closest('.photo-gallery-header') || - e.target.closest('.zap-photo-modal-overlay') + e.target.closest('.modal-overlay') ) { return; } diff --git a/app/components/zap-photo-modal.gjs b/app/components/zap-photo-modal.gjs index 4d3df46..845739f 100644 --- a/app/components/zap-photo-modal.gjs +++ b/app/components/zap-photo-modal.gjs @@ -7,6 +7,7 @@ import { task } from 'ember-concurrency'; import { eq } from 'ember-truth-helpers'; import qrCode from '../modifiers/qr-code'; import Icon from './icon'; +import Modal from './modal'; const SLIDER_MIN_SATS = 10; const SLIDER_MAX_SATS = 100_000; @@ -210,11 +211,6 @@ export default class ZapPhotoModal extends Component { } } - @action - stopPropagation(e) { - e.stopPropagation(); - } - _startReceiptSubscription(zapRequest) { this._cleanupReceiptSub(); this._zapRequest = zapRequest; @@ -291,215 +287,189 @@ export default class ZapPhotoModal extends Component { } } diff --git a/app/styles/app.css b/app/styles/app.css index 5a51211..6fe69d5 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -2427,259 +2427,113 @@ button.create-place { border-radius: 10px; } -/* Zap Photo Modal */ -.zap-photo-modal-overlay { - position: fixed; - inset: 0; - background: rgb(0 0 0 / 70%); - z-index: 10000; - display: flex; - align-items: center; - justify-content: center; - padding: 1rem; +/* Generic modal heading reset (shared by all modals) */ +.modal-content h2, +.modal-content h3 { + margin-top: 0; } +/* Zap Photo Modal — scoped, nested styles */ .zap-photo-modal { - background: white; - border-radius: 12px; - box-shadow: 0 8px 32px rgb(0 0 0 / 30%); - width: 100%; - max-width: 420px; - max-height: 90vh; - overflow-y: auto; - display: flex; - flex-direction: column; -} - -.zap-photo-modal-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1rem 1.25rem; - border-bottom: 1px solid var(--divider-color); -} - -.zap-photo-modal-header h3 { - margin: 0; -} - -.zap-modal-close { - background: none; - border: none; - padding: 0.25rem; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - color: #898989; -} - -.zap-modal-close:hover { - color: var(--body-text-color); -} - -.zap-modal-status { - padding: 0.75rem 1.25rem; - border-bottom: 1px solid var(--divider-color); - min-height: 2.5rem; - display: flex; - flex-direction: column; - gap: 0.4rem; -} - -.zap-modal-loading { - font-size: 0.85rem; - color: #898989; -} - -.zap-modal-address { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.zap-modal-badge { - background: #f7b500; - color: white; - font-size: 0.7rem; - font-weight: 600; - padding: 2px 8px; - border-radius: 10px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.zap-modal-address-text { - font-family: monospace; - font-size: 0.8rem; - color: #555; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.zap-modal-error-msg { - color: var(--danger-color); - font-size: 0.85rem; - padding: 0.5rem 0.75rem; - background: rgb(234 67 53 / 8%); - border-radius: 4px; -} - -.zap-modal-body { - padding: 1.25rem; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.zap-modal-center { - align-items: center; text-align: center; -} -.zap-amount-display { - display: flex; - align-items: baseline; - justify-content: center; - gap: 0.4rem; -} + & h2 { + margin-bottom: 1rem; + } -.zap-amount-value { - font-size: 2rem; - font-weight: 700; - color: #f7b500; -} + & .amount-display { + display: flex; + align-items: baseline; + justify-content: center; + gap: 0.4rem; + } -.zap-amount-unit { - font-size: 0.9rem; - color: #898989; -} + & .amount-value { + font-size: 2rem; + font-weight: 700; + color: var(--default-list-color); + } -.zap-slider { - width: 100%; - accent-color: #f7b500; - cursor: pointer; -} + & .amount-unit { + font-size: 0.9rem; + color: var(--body-text-color); + opacity: 0.7; + } -.zap-slider-ticks { - display: flex; - justify-content: space-between; - font-size: 0.7rem; - color: #aaa; - padding: 0 0.25rem; -} + & .zap-slider { + width: 100%; + accent-color: var(--default-list-color); + cursor: pointer; + } -.zap-min-max { - font-size: 0.75rem; - color: #898989; - text-align: center; -} + & .slider-ticks { + display: flex; + justify-content: space-between; + font-size: 0.7rem; + color: var(--body-text-color); + opacity: 0.5; + padding: 0 0.25rem; + } -.zap-message-field { - display: flex; - flex-direction: column; - gap: 0.3rem; -} + & .min-max { + font-size: 0.75rem; + color: var(--body-text-color); + opacity: 0.7; + } -.zap-message-field label { - font-size: 0.8rem; - color: #898989; -} + & .lightning-address { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + } -.zap-message-field textarea { - width: 100%; - border: 1px solid #ddd; - border-radius: 4px; - padding: 0.5rem; - font-family: inherit; - font-size: 0.9rem; - resize: vertical; -} + & .lightning-badge { + background: var(--default-list-color); + color: white; + font-size: 0.7rem; + font-weight: 600; + padding: 2px 8px; + border-radius: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; + } -.zap-message-field textarea:focus { - outline: none; - border-color: var(--link-color); -} + & .lightning-address-text { + font-family: monospace; + font-size: 0.8rem; + color: var(--body-text-color); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } -.zap-modal-actions { - display: flex; - gap: 0.5rem; - justify-content: flex-end; -} + & .centered { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + } -.zap-modal-hint { - font-size: 0.8rem; - color: #898989; - text-align: center; -} + & .success { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + } -.zap-spinner { - width: 32px; - height: 32px; - border: 3px solid #eee; - border-top-color: #f7b500; - border-radius: 50%; - animation: zap-spin 0.8s linear infinite; -} + & .success-icon { + font-size: 3rem; + } -@keyframes zap-spin { - to { - transform: rotate(360deg); + & .success h4 { + margin: 0; + font-size: 1.2rem; + } + + & .success p { + margin: 0; + font-size: 0.9rem; + color: var(--body-text-color); } } - -.zap-pay-prompt { - text-align: center; - font-size: 0.9rem; -} - -.zap-qr-container { - display: flex; - justify-content: center; -} - -.zap-qr-container canvas { - border-radius: 8px; - background: white; - padding: 8px; -} - -.zap-qr-container a { - display: block; -} - -.zap-webln-btn { - width: 100%; -} - -.zap-open-wallet-link { - width: 100%; - text-align: center; - text-decoration: none; -} - -.zap-invoice-details { - display: flex; - justify-content: center; - font-size: 0.8rem; -} - -.zap-success { - gap: 0.5rem; -} - -.zap-success-icon { - font-size: 3rem; -} - -.zap-success h4 { - margin: 0; - font-size: 1.2rem; -} - -.zap-success p { - margin: 0; - font-size: 0.9rem; - color: #555; -} diff --git a/tests/integration/components/zap-photo-modal-test.gjs b/tests/integration/components/zap-photo-modal-test.gjs index 4c58aae..f08a13e 100644 --- a/tests/integration/components/zap-photo-modal-test.gjs +++ b/tests/integration/components/zap-photo-modal-test.gjs @@ -42,11 +42,11 @@ module('Integration | Component | zap-photo-modal', function (hooks) { ); assert.dom('.zap-slider').exists('slider is rendered'); - assert.dom('.zap-amount-value').exists('amount is displayed'); + assert.dom('.zap-amount').exists('amount is displayed'); assert - .dom('.zap-modal-address-text') + .dom('.lightning-address-text') .hasText('user@example.com', 'lightning address is shown'); - assert.dom('.zap-modal-badge').hasText('Lightning', 'badge is shown'); + assert.dom('.lightning-badge').hasText('Lightning', 'badge is shown'); }); test('it shows error when user has no lightning address', async function (assert) { @@ -60,10 +60,10 @@ module('Integration | Component | zap-photo-modal', function (hooks) { ); assert - .dom('.zap-modal-error-msg') + .dom('.alert-error') .exists('error message is shown when no lightning address'); assert - .dom('.zap-modal-actions .btn-primary') + .dom('.edit-actions .btn-primary') .isDisabled('zap button is disabled when no address'); }); @@ -80,11 +80,9 @@ module('Integration | Component | zap-photo-modal', function (hooks) { ); + assert.dom('.alert-error').exists('error shown when allowsNostr is false'); assert - .dom('.zap-modal-error-msg') - .exists('error shown when allowsNostr is false'); - assert - .dom('.zap-modal-actions .btn-primary') + .dom('.edit-actions .btn-primary') .isDisabled('zap button is disabled'); }); @@ -103,10 +101,10 @@ module('Integration | Component | zap-photo-modal', function (hooks) { ); assert - .dom('.zap-modal-actions .btn-primary') + .dom('.edit-actions .btn-primary') .isNotDisabled('zap button is enabled when endpoint is valid'); - await click('.zap-modal-actions .btn-primary'); + await click('.edit-actions .btn-primary'); assert.ok(zapSpy.calledOnce, 'nostrZap.zap was called'); assert.deepEqual( @@ -120,9 +118,9 @@ module('Integration | Component | zap-photo-modal', function (hooks) { 'zap amount is 1000 sats in millisats' ); - await waitFor('.zap-qr-container'); + await waitFor('.qr-code-container'); - assert.dom('.zap-qr-container').exists('QR code is shown'); + assert.dom('.qr-code-container').exists('QR code is shown'); assert.dom('.zap-pay-prompt').exists('payment prompt is shown'); assert.notOk(closed, 'modal was not closed by clicking the Zap button'); }); @@ -137,9 +135,9 @@ module('Integration | Component | zap-photo-modal', function (hooks) { ); - await fillIn('.zap-message-field textarea', 'Great photo!'); + await fillIn('.form-group textarea', 'Great photo!'); - await click('.zap-modal-actions .btn-primary'); + await click('.edit-actions .btn-primary'); assert.ok(zapSpy.calledOnce, 'nostrZap.zap was called'); assert.strictEqual( @@ -159,10 +157,10 @@ module('Integration | Component | zap-photo-modal', function (hooks) { ); - await click('.zap-modal-actions .btn-primary'); - await waitFor('.zap-webln-btn'); + await click('.edit-actions .btn-primary'); + await waitFor('.webln-pay-btn'); - assert.dom('.zap-webln-btn').exists('WebLN button is shown when available'); + assert.dom('.webln-pay-btn').exists('WebLN button is shown when available'); }); test('it shows success after WebLN payment', async function (assert) { @@ -175,9 +173,9 @@ module('Integration | Component | zap-photo-modal', function (hooks) { ); - await click('.zap-modal-actions .btn-primary'); - await waitFor('.zap-webln-btn'); - await click('.zap-webln-btn'); + await click('.edit-actions .btn-primary'); + await waitFor('.webln-pay-btn'); + await click('.webln-pay-btn'); await waitFor('.zap-success'); @@ -193,10 +191,10 @@ module('Integration | Component | zap-photo-modal', function (hooks) { ); - await click('.zap-modal-actions .btn-primary'); - await waitFor('.zap-qr-container'); + await click('.edit-actions .btn-primary'); + await waitFor('.qr-code-container'); - assert.dom('.zap-qr-container').exists('QR code is shown'); + assert.dom('.qr-code-container').exists('QR code is shown'); assert.ok( this.nostrZap._receiptCallback, @@ -227,7 +225,7 @@ module('Integration | Component | zap-photo-modal', function (hooks) { ); - await click('.zap-modal-close'); + await click('.close-modal-btn'); assert.ok(closed, 'onClose was called'); }); @@ -245,7 +243,7 @@ module('Integration | Component | zap-photo-modal', function (hooks) { ); - await click('.zap-modal-actions .btn-outline'); + await click('.edit-actions .btn-outline'); assert.ok(closed, 'onClose was called from Cancel button'); }); @@ -268,7 +266,7 @@ module('Integration | Component | zap-photo-modal', function (hooks) { assert.notOk(closed, 'onClose was not called when clicking inside modal'); }); - test('it does not close when clicking the overlay background', async function (assert) { + test('it closes when clicking the overlay background', async function (assert) { let closed = false; this.handleClose = () => { closed = true; @@ -281,8 +279,8 @@ module('Integration | Component | zap-photo-modal', function (hooks) { ); - await click('.zap-photo-modal-overlay'); + await click('.modal-overlay'); - assert.notOk(closed, 'onClose was not called when clicking overlay'); + assert.ok(closed, 'onClose was called when clicking overlay'); }); });