diff --git a/assets/css/paybutton-admin.css b/assets/css/paybutton-admin.css index 9211426..d5deeac 100644 --- a/assets/css/paybutton-admin.css +++ b/assets/css/paybutton-admin.css @@ -59,6 +59,142 @@ border: none; } +/* ------------------------------ + Button Generator Page Styles +------------------------------ */ +.pb-generator-container { + display: flex; + gap: 2rem; + } + +.pb-generator-form { + flex: 1; + max-width: 400px; +} + +.pb-generator-form label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.pb-generator-input { + width: 100%; + box-sizing: border-box; + padding: 0.5rem; + margin-bottom: 1rem; +} + +.pb-generator-preview { + flex: 1; +} + +.pb-generator-preview #pbGenPreview { + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + padding: 1rem; + min-height: 80px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + display: flex; + justify-content: center; + align-items: center; + font-size: 1rem; + color: #333; + transition: box-shadow 0.3s ease, border-color 0.3s ease; +} + +.pb-generator-preview #pbGenPreview:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + border-color: #0074C2; +} + +#pbGenShortcode { + font-family: monospace; + font-size: 0.8rem; + line-height: 1.5; + color: #333; + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 4px; + padding: 1rem; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + width: 100%; + resize: vertical; + word-break: break-all; +} + +#pbGenToValidationResult{ + margin-top: 0px; +} + +/* Grouped Colors */ +.pb-generator-colors { + display: flex; + gap: 1rem; + align-items: center; + margin-top: 1rem; +} + +.pb-generator-colors .pb-generator-color { + display: flex; + flex-direction: column; + align-items: center; +} + +.pb-generator-widget { + display: inline-flex; + align-items: center; + gap: 0.5rem; + margin-top: 1rem; +} + +.pb-generator-widget input[type="checkbox"] { + margin-left: 0.7rem; +} + +/* Simple custom tooltip styling */ +.pbTooltip { + position: absolute; + cursor: help; + margin-left: 0.3rem; +} + +.pbTooltip::before { + padding: 5px 3px; +} + +.pbTooltip::after { + content: ''; + left: 100%; + top: 50%; + transform: translateY(-50%); + opacity: 0; + transition: opacity 0.2s ease-in-out; + pointer-events: none; + z-index: 999999; /* ensure it stays above WP admin menu */ + max-width: 300px; + background: #0074C2; + color: #fff; + padding: 5px 2px; + border-radius: 3px; + white-space: normal; + overflow-wrap: break-word; + text-align: center; +} + +.pbTooltip:hover::after { + content: attr(data-tooltip); + opacity: 1; + padding: 5px 10px; +} + +.shortcode-note { + font-size: 0.9em; + color: #555; + margin-top: 0.5rem; + font-style: italic; +} /* ------------------------------ Paywall Settings page styles ------------------------------ */ @@ -85,4 +221,47 @@ table.widefat.fixed.striped td { ------------------------------ */ .pb-paragraph-margin-top { margin-top: 1rem; +} + +/* ------------------------------ + Utility - Clipboard +------------------------------ */ +.shortcode-container { + position: relative; + margin-bottom: 1rem; +} + +.shortcode-container textarea { + width: 100%; + box-sizing: border-box; +} + +/* The overlay covers the textarea but is hidden by default */ +.copy-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(66, 123, 236, 0.7); + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + color: #f6f7f7; + cursor: pointer; + transition: opacity 0.3s ease; + opacity: 0; + pointer-events: none; +} + +/* Show the overlay when hovering over the container */ +.shortcode-container:hover .copy-overlay { + opacity: 1; + pointer-events: auto; +} + +.copy-overlay .overlay-text { + text-align: center; + font-weight:bold; } \ No newline at end of file diff --git a/assets/js/addressValidator.bundle.js b/assets/js/addressValidator.bundle.js index 69ad8d4..3e67f71 100644 --- a/assets/js/addressValidator.bundle.js +++ b/assets/js/addressValidator.bundle.js @@ -749,7 +749,7 @@ var cashaddrExports = requireCashaddr(); - // src/addressValidator.js + window.cashaddrExports = cashaddrExports; document.addEventListener('DOMContentLoaded', () => { // Find the wallet address input field by its ID. @@ -771,8 +771,8 @@ addressInput.addEventListener('input', () => { const address = addressInput.value.trim(); if (address === "") { - resultSpan.textContent = '❌ Invalid address'; - resultSpan.style.color = 'red'; + resultSpan.textContent = ''; + resultSpan.style.color = ''; if (saveButton) saveButton.disabled = true; return; } @@ -793,4 +793,4 @@ addressInput.dispatchEvent(new Event('input')); }); -})(); +})(); \ No newline at end of file diff --git a/assets/js/paybutton-generator.js b/assets/js/paybutton-generator.js new file mode 100644 index 0000000..3e78009 --- /dev/null +++ b/assets/js/paybutton-generator.js @@ -0,0 +1,261 @@ +/* File: assets/js/paybutton-generator.js +*/ +(function($) { + "use strict"; + + /* ========================================================================== + ADMIN WALLET ADDRESS VALIDATION + ========================================================================== + */ + if ($('#pbGenTo').length) { + + const $toField = $('#pbGenTo'); + let $validationMsg; + + if (!$('#pbGenToValidationResult').length) { + $toField.after('

'); + } + $validationMsg = $('#pbGenToValidationResult'); + + $toField.on('input', function() { + const address = $toField.val().trim(); + + if (!address) { + $validationMsg.text('').css('color', ''); + return; + } + + const valid = window.cashaddrExports && window.cashaddrExports.isValidCashAddress(address); + if (typeof window.cashaddrExports === 'undefined') { + console.error('[PayButton] addressValidator is missing or not loaded!'); + } + + if (valid) { + $validationMsg.text('✅ Valid address').css('color', 'green'); + } else { + $validationMsg.text('❌ Invalid address').css('color', 'red'); + } + }); + + // Trigger input event on page load to validate pre-set value (from Paywall Settings). + $toField.trigger('input'); + } + + /* ========================================================================== + BUTTON GENERATOR LOGIC + ========================================================================== + */ + + let previousAddress = ''; + + if ($('#pbGenTo').length) { + function updateGenerator() { + // Define default values for required options + const defaults = { + to:"", + text: "🧋 Buy me a coffee", + currency: "XEC", + animation: "slide", + successText: "Thank You", + primary: "#0074C2", + secondary: "#ffffff", + tertiary: "#231F20" + }; + + const toVal = ($('#pbGenTo').val() || "").trim() || defaults.to; + + // If the TO field is empty, reset the currency dropdown to show all options. + if (!toVal) { + $('#pbGenCurrency').html(''); + } + + // If the wallet address is invalid, clear preview and shortcode and exit. + if (!toVal || !window.cashaddrExports.isValidCashAddress(toVal)) { + const previewContainer = document.getElementById('pbGenPreview'); + if (previewContainer) { + previewContainer.innerHTML = 'Please enter a valid wallet address to preview'; + } + $('#pbGenShortcode').val(''); + $('.pb-generator-preview').find('h3:contains("SHORTCODE"), p.shortcode-note, #pbGenShortcode').hide(); + $('.shortcode-container .copy-btn').hide(); + return; + } else { + $('.pb-generator-preview').find('h3:contains("SHORTCODE"), p.shortcode-note, #pbGenShortcode').show(); + $('.shortcode-container .copy-btn').show(); + } + + const addressChanged = toVal !== previousAddress; + if (addressChanged) { + previousAddress = toVal; + } + + // Set primary color based on address type only if the address has changed, to preserve user modifications + try { + const decoded = window.cashaddrExports.decodeCashAddress(toVal); + const $currency = $('#pbGenCurrency'); + const currentCurrency = $currency.val(); + const prefix = decoded.prefix ? decoded.prefix.toLowerCase() : ""; + if (addressChanged) { + if (prefix === 'bitcoincash') { + $('#pbGenPrimary').val("#4BC846"); + } else if (prefix === 'ecash') { + $('#pbGenPrimary').val("#0074C2"); + } else { + $('#pbGenPrimary').val(defaults.primary); + } + } + if (prefix === 'bitcoincash') { + // Remove XEC option from crypto choices. + $currency.find('option[value="XEC"]').remove(); + if ($currency.find('option[value="BCH"]').length === 0) { + $currency.append(''); + } + } else if (prefix === 'ecash') { + // Remove BCH option from crypto choices. + $currency.find('option[value="BCH"]').remove(); + if ($currency.find('option[value="XEC"]').length === 0) { + $currency.append(''); + } + if (!currentCurrency || currentCurrency === "BCH") { + $currency.val("XEC"); + } + } else { + $currency.val(defaults.currency); + } + } catch (e) { + // In case decoding fails, revert to default values. + if (addressChanged) { + $('#pbGenPrimary').val(defaults.primary); + } + $('#pbGenCurrency').val(defaults.currency); + } + + const amountVal = ($('#pbGenAmount').val() || "").trim(); + const currencyVal = ($('#pbGenCurrency').val() || "").trim() || defaults.currency; + const textVal = ($('#pbGenText').val() || "").trim() || defaults.text; + const hoverVal = ($('#pbGenHover').val() || "").trim(); + const successVal = ($('#pbGenSuccessText').val() || "").trim() || defaults.successText; + const animationVal = ($('#pbGenAnimation').val() || "").trim() || defaults.animation; + const goalVal = ($('#pbGenGoal').val() || "").trim(); + const primaryVal = ($('#pbGenPrimary').val() || "").trim() || defaults.primary; + const secondaryVal = ($('#pbGenSecondary').val() || "").trim() || defaults.secondary; + const tertiaryVal = ($('#pbGenTertiary').val() || "").trim() || defaults.tertiary; + const widgetChecked = $('#pbGenWidget').is(':checked'); + + // Build PB's config object + let config = {}; + if(toVal !== '') { config.to = toVal; } + if(amountVal !== '') { config.amount = parseFloat(amountVal); } + config.currency = currencyVal; + if(textVal !== '') { config.text = textVal; } + if(hoverVal !== '') { config.hoverText = hoverVal; } + config.successText = successVal; + config.animation = animationVal; + if(goalVal !== '') { config.goalAmount = parseFloat(goalVal); } + config.theme = { + palette: { + primary: primaryVal, + secondary: secondaryVal, + tertiary: tertiaryVal + } + }; + config.widget = widgetChecked; + + // Get the preview container + const previewContainer = document.getElementById('pbGenPreview'); + if (previewContainer) { + // Remove all child nodes + while (previewContainer.firstChild) { + previewContainer.removeChild(previewContainer.firstChild); + } + // Create a new inner container for rendering the button + const innerContainer = document.createElement('div'); + previewContainer.appendChild(innerContainer); + + // Render the button using the local paybutton.js + if (typeof PayButton !== 'undefined') { + if (widgetChecked) { + PayButton.renderWidget(innerContainer, config); + } else { + PayButton.render(innerContainer, config); + } + } else { + previewContainer.innerHTML = '

Error: paybutton.js not loaded.

'; + } + } + + // Generate shortcode with config attribute + const shortcode = `[paybutton config='${JSON.stringify(config)}'][/paybutton]`; + $('#pbGenShortcode').val(shortcode); + } + + // Bind updates on input and change events + $('#pbGenTo, #pbGenAmount, #pbGenCurrency, #pbGenText, #pbGenAnimation, #pbGenGoal, #pbGenHover, #pbGenPrimary, #pbGenSecondary, #pbGenTertiary, #pbGenSuccessText, #pbGenWidget') + .on('input change', updateGenerator); + + // Initialize the generator on page load + updateGenerator(); + } + + /* ========================================================================== + SHORTCODE RENDERING LOGIC ON THE FRONT-END + ========================================================================== + */ + function renderPayButtonShortcodes() { + const containers = document.querySelectorAll('.paybutton-shortcode-container'); + containers.forEach(container => { + const configStr = container.getAttribute('data-config'); + try { + const config = JSON.parse(configStr); + if (typeof PayButton !== 'undefined') { + if (config.widget) { + PayButton.renderWidget(container, config); + } else { + PayButton.render(container, config); + } + } else { + container.innerHTML = '

Error: paybutton.js not loaded.

'; + } + } catch (err) { + console.error('Invalid PayButton config JSON:', err); + } + }); + } + + // Run shortcode rendering on DOMContentLoaded + document.addEventListener('DOMContentLoaded', renderPayButtonShortcodes); + +})(jQuery); + +/* ========================================================================== + CLIPBOARD FUNCTIONALITY + ========================================================================== +*/ +document.addEventListener('DOMContentLoaded', function() { + const copyOverlay = document.querySelector('.copy-overlay'); + if (copyOverlay) { + copyOverlay.addEventListener('click', function() { + const targetSelector = copyOverlay.getAttribute('data-target'); + const targetElement = document.getElementById(targetSelector.replace('#', '')); + if (targetElement && navigator.clipboard) { + navigator.clipboard.writeText(targetElement.value) + .then(function() { + const overlayText = copyOverlay.querySelector('.overlay-text'); + if (overlayText) { + overlayText.textContent = 'Copied to clipboard!'; + } + copyOverlay.classList.add('copied'); + setTimeout(function() { + if (overlayText) { + overlayText.textContent = 'Click to copy!'; + } + copyOverlay.classList.remove('copied'); + }, 1000); + }) + .catch(function(err) { + console.error('Failed to copy text: ', err); + }); + } + }); + } +}); \ No newline at end of file diff --git a/includes/class-paybutton-admin.php b/includes/class-paybutton-admin.php index 5025627..174e111 100644 --- a/includes/class-paybutton-admin.php +++ b/includes/class-paybutton-admin.php @@ -33,6 +33,15 @@ public function add_admin_menus() { 100 ); + add_submenu_page( + 'paybutton', + 'Button Generator', + 'Button Generator', + 'manage_options', + 'paybutton-generator', + array( $this, 'button_generator_page' ) + ); + add_submenu_page( 'paybutton', 'Paywall Settings', @@ -110,6 +119,35 @@ public function enqueue_admin_scripts( $hook_suffix ) { true ); } + + // Only load the generator JS on the PayButton Generator page + if ( $hook_suffix === 'paybutton_page_paybutton-generator' ) { + + // Enqueue the bundled address validator script + wp_enqueue_script( + 'address-validator', + PAYBUTTON_PLUGIN_URL . 'assets/js/addressValidator.bundle.js', + array(), + '2.0.0', + true + ); + + wp_enqueue_script( + 'paybutton-core', + PAYBUTTON_PLUGIN_URL . 'assets/js/paybutton.js', + array('address-validator'), + '1.0', + true + ); + + wp_enqueue_script( + 'paybutton-generator', + PAYBUTTON_PLUGIN_URL . 'assets/js/paybutton-generator.js', + array('jquery','paybutton-core','address-validator'), + '1.0', + true + ); + } } /** @@ -134,12 +172,19 @@ private function load_admin_template( $template_name, $args = array() ) { */ public function dashboard_page() { $args = array( - 'generate_button_url' => 'https://paybutton.org/#button-generator', + 'generate_button_url' => esc_url( admin_url( 'admin.php?page=paybutton-generator' ) ), 'paywall_settings_url' => esc_url( admin_url( 'admin.php?page=paybutton-paywall' ) ) ); $this->load_admin_template( 'dashboard', $args ); } + public function button_generator_page() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + $this->load_admin_template( 'paybutton-generator' ); + } + /** * Output the Paywall Settings page. */ diff --git a/includes/class-paybutton-public.php b/includes/class-paybutton-public.php index b7212da..8b8f4ed 100644 --- a/includes/class-paybutton-public.php +++ b/includes/class-paybutton-public.php @@ -30,6 +30,7 @@ public function __construct() { add_shortcode( 'paybutton_profile', array( $this, 'profile_shortcode' ) ); add_filter( 'comments_open', array( $this, 'filter_comments_open' ), 999, 2 ); add_action( 'pre_get_comments', array( $this, 'filter_comments_query' ), 999 ); + add_shortcode( 'paybutton', [ $this, 'paybutton_generator_shortcode' ] ); } /** @@ -85,6 +86,15 @@ public function enqueue_public_assets() { true ); + // Load a new JS file to render the [paybutton] shortcodes + wp_enqueue_script( + 'paybutton-generator', + PAYBUTTON_PLUGIN_URL . 'assets/js/paybutton-generator.js', + array('paybutton-core'), + '1.0', + true + ); + /** * Localizes the 'paybutton-cashtab-login' script with variables needed for AJAX interactions. * @@ -108,6 +118,27 @@ public function enqueue_public_assets() { ) ); } + /** + * Renders the [paybutton][/paybutton] shortcode + */ + public function paybutton_generator_shortcode( $atts, $content = null ) { + // Provide a default empty JSON if not supplied + $atts = shortcode_atts( [ 'config' => '{}' ], $atts ); + $decoded = json_decode( $atts['config'], true ); + if ( ! is_array( $decoded ) ) { + $decoded = []; + } + + // Safely encode the config for a data attribute + $encodedConfig = esc_attr( wp_json_encode( $decoded ) ); + + ob_start(); + ?> +
+
- - Add Simple PayButton + + PayButton Generator
- + Paywall Settings
diff --git a/templates/admin/paybutton-generator.php b/templates/admin/paybutton-generator.php new file mode 100644 index 0000000..033c267 --- /dev/null +++ b/templates/admin/paybutton-generator.php @@ -0,0 +1,142 @@ + + + +
+

Button Generator

+

Build your custom PayButton and begin accepting payments!

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ +
+

PREVIEW

+
+ Enter an address +
+

SHORTCODE

+

+ Click the shortcode below to copy it, then paste it anywhere you'd like the PayButton to appear on your site. +

+
+ +
+ Click to copy! +
+
+
+
+
\ No newline at end of file