* only submitting its OWN fields, switching to another tab and saving * would submit an empty value for every field on the tab you're NOT * looking at, silently wiping it back to blank/default. Keeping * everything in one form/one settings group sidesteps that entirely. */ class SEATT_Settings { /** * Sample prices in the Display previews are written in rupiah-sized * numbers (150,000). Scale them so a USD or EUR store sees $35, not * $150,000.00, and a yen or rupee store sees something in between. */ private static function preview_price( $amount ) { $code = strtoupper( (string) get_option( 'seatt_currency_code', 'IDR' ) ); $large = array( 'IDR', 'VND', 'LAK', 'KHR', 'MMK', 'KRW', 'UZS', 'IRR', 'COP', 'CLP', 'PYG', 'UGX', 'TZS' ); $mid = array( 'JPY', 'INR', 'PHP', 'THB', 'RUB', 'TWD', 'PKR', 'BDT', 'LKR', 'NPR', 'NGN', 'KES', 'ARS', 'EGP', 'HUF', 'ISK' ); if ( in_array( $code, $large, true ) ) { return $amount; } return in_array( $code, $mid, true ) ? $amount / 100 : round( $amount / 4285.7142857 ); } public function __construct() { add_action( 'admin_menu', array( $this, 'register_menu' ) ); add_action( 'admin_init', array( $this, 'register_settings' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); } public function enqueue( $hook ) { $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- which screen is open if ( 'seatt-settings' !== $page ) { return; } wp_enqueue_style( 'seatt-shared', SEATT_PLUGIN_URL . 'admin/css/esm-shared.css', array(), seatt_asset_ver( 'admin/css/esm-shared.css' ) ); } public function register_menu() { add_submenu_page( 'seatt-main', __( 'Settings', 'seathive' ), __( 'Settings', 'seathive' ), 'manage_options', 'seatt-settings', array( $this, 'render_page' ) ); } public function register_settings() { // Store Setting register_setting( 'seatt_settings', 'seatt_currency_code', array( 'sanitize_callback' => function ( $v ) { $v = strtoupper( sanitize_text_field( (string) $v ) ); return array_key_exists( $v, seatt_currency_list() ) ? $v : 'IDR'; } ) ); register_setting( 'seatt_settings', 'seatt_currency_symbol', array( 'sanitize_callback' => 'sanitize_text_field' ) ); register_setting( 'seatt_settings', 'seatt_show_tax_in_cart', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ) ); register_setting( 'seatt_settings', 'seatt_tax_rate', array( 'type' => 'number', 'sanitize_callback' => function ( $v ) { return max( 0, (float) str_replace( ',', '.', (string) $v ) ); } ) ); register_setting( 'seatt_settings', 'seatt_fee_type', array( 'sanitize_callback' => function ( $v ) { return in_array( $v, array( 'none', 'percent', 'per_ticket' ), true ) ? $v : 'none'; } ) ); register_setting( 'seatt_settings', 'seatt_fee_value', array( 'type' => 'number', 'sanitize_callback' => function ( $v ) { return max( 0, (float) $v ); } ) ); register_setting( 'seatt_settings', 'seatt_fee_label', array( 'sanitize_callback' => 'sanitize_text_field' ) ); register_setting( 'seatt_settings', 'seatt_reminder_enabled', array( 'sanitize_callback' => function ( $v ) { return $v ? 1 : 0; } ) ); register_setting( 'seatt_settings', 'seatt_reminder_days', array( 'sanitize_callback' => function ( $v ) { return max( 0, min( 14, (int) $v ) ); } ) ); register_setting( 'seatt_settings', 'seatt_reminder_template', array( 'sanitize_callback' => 'sanitize_textarea_field' ) ); // Payment Setting (unchanged from before — just relocated into this tabbed page) register_setting( 'seatt_settings', 'seatt_display_event', array( 'sanitize_callback' => array( 'SEATT_Display_Extra', 'sanitize_event' ) ) ); register_setting( 'seatt_settings', 'seatt_display_tickets', array( 'sanitize_callback' => array( 'SEATT_Display_Extra', 'sanitize_tickets' ) ) ); register_setting( 'seatt_settings', 'seatt_display_checkout', array( 'sanitize_callback' => array( 'SEATT_Display_Extra', 'sanitize_checkout' ) ) ); register_setting( 'seatt_settings', 'seatt_display_receipt', array( 'sanitize_callback' => array( 'SEATT_Display_Extra', 'sanitize_receipt' ) ) ); register_setting( 'seatt_settings', 'seatt_display_mytickets', array( 'sanitize_callback' => array( 'SEATT_Display_Extra', 'sanitize_mytickets' ) ) ); register_setting( 'seatt_settings', SEATT_Door_Theme::OPT, array( 'sanitize_callback' => array( 'SEATT_Door_Theme', 'sanitize' ) ) ); register_setting( 'seatt_settings', 'seatt_payment_gateway', array( 'sanitize_callback' => function ( $v ) { return in_array( $v, array( 'manual', 'xendit', 'stripe', 'doku', 'paypal', 'mollie' ), true ) ? $v : 'manual'; } ) ); register_setting( 'seatt_settings', 'seatt_mollie_api_key', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_mollie_currency', array( 'sanitize_callback' => function ( $v ) { $v = strtoupper( sanitize_text_field( $v ) ); return ( '' === $v || SEATT_Payment::mollie_supports( $v ) ) ? $v : ''; } ) ); register_setting( 'seatt_settings', 'seatt_mollie_rate', array( 'sanitize_callback' => function ( $v ) { return max( 0, (float) str_replace( ',', '.', (string) $v ) ); } ) ); register_setting( 'seatt_settings', 'seatt_max_seats_per_order', array( 'type' => 'integer', 'sanitize_callback' => function ( $v ) { return max( 1, min( 100, (int) $v ) ); } ) ); register_setting( 'seatt_settings', 'seatt_cash_expiry_hours', array( 'type' => 'integer', 'sanitize_callback' => function ( $v ) { return max( 0, min( 720, (int) $v ) ); } ) ); // 'manual' | 'xendit' | 'stripe' | 'paypal' register_setting( 'seatt_settings', 'seatt_xendit_secret_key', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_xendit_callback_token', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); // "Verification Token" from Xendit dashboard register_setting( 'seatt_settings', 'seatt_stripe_secret_key', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_stripe_webhook_secret', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); // "Signing secret" from the Stripe Dashboard webhook endpoint register_setting( 'seatt_settings', 'seatt_doku_client_id', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_doku_secret_key', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_doku_production', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ) ); register_setting( 'seatt_settings', 'seatt_paypal_client_id', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_paypal_client_secret', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_paypal_production', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ) ); register_setting( 'seatt_settings', 'seatt_paypal_currency', array( 'sanitize_callback' => function ( $v ) { $v = strtoupper( sanitize_text_field( $v ) ); return ( '' === $v || SEATT_Payment::paypal_supports( $v ) ) ? $v : ''; } ) ); register_setting( 'seatt_settings', 'seatt_paypal_rate', array( 'sanitize_callback' => function ( $v ) { return max( 0, (float) str_replace( ',', '.', (string) $v ) ); } ) ); // Email // SMTP. The password is registered like any other option, but // wp-config's SEATT_SMTP_PASSWORD takes precedence at read time — // see SEATT_SMTP::password() for why that is the better place. register_setting( 'seatt_settings', 'seatt_smtp_enabled', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ) ); register_setting( 'seatt_settings', 'seatt_smtp_preset', array( 'sanitize_callback' => 'sanitize_key' ) ); register_setting( 'seatt_settings', 'seatt_smtp_host', array( 'sanitize_callback' => 'sanitize_text_field' ) ); register_setting( 'seatt_settings', 'seatt_smtp_port', array( 'type' => 'integer', 'sanitize_callback' => 'absint' ) ); register_setting( 'seatt_settings', 'seatt_smtp_encryption', array( 'sanitize_callback' => function ( $v ) { return in_array( $v, array( 'tls', 'ssl', 'none' ), true ) ? $v : 'tls'; } ) ); register_setting( 'seatt_settings', 'seatt_smtp_auth', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ) ); register_setting( 'seatt_settings', 'seatt_smtp_username', array( 'sanitize_callback' => 'sanitize_text_field' ) ); register_setting( 'seatt_settings', 'seatt_smtp_password', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_smtp_from_email', array( 'sanitize_callback' => 'sanitize_email' ) ); register_setting( 'seatt_settings', 'seatt_smtp_from_name', array( 'sanitize_callback' => 'sanitize_text_field' ) ); register_setting( 'seatt_settings', 'seatt_send_receipt_email', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ) ); register_setting( 'seatt_settings', 'seatt_receipt_email_text', array( 'sanitize_callback' => 'wp_kses_post' ) ); // Client Order Placed Email register_setting( 'seatt_settings', 'seatt_client_order_placed_enabled', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ) ); register_setting( 'seatt_settings', 'seatt_client_order_placed_subject', array( 'sanitize_callback' => 'sanitize_text_field' ) ); register_setting( 'seatt_settings', 'seatt_client_order_placed_content', array( 'sanitize_callback' => 'wp_kses_post' ) ); // Display — one option holding every event's theme plus a // 'default'. Arrives from the tab as JSON in a single hidden // field, so the whole map saves in the same round trip as every // other tab (see this class's header note about one form). register_setting( 'seatt_settings', 'seatt_display_themes', array( 'type' => 'array', 'sanitize_callback' => array( $this, 'sanitize_display_themes' ), 'default' => array(), ) ); } /** * Rebuilds the theme map from scratch on every save rather than * merging into what's already stored — that way an event deleted in * the tab actually disappears instead of lingering forever. */ public function sanitize_display_themes( $input ) { // The tab posts discrete seatt_display_themes[key][prop] fields, so // this arrives as an array. Anything else means a truncated or // tampered request, and blanking every event's theme would be a // destructive way to react to that — keep what's stored instead. if ( ! is_array( $input ) ) { return seatt_get_display_themes(); } $clean = array(); foreach ( $input as $key => $theme ) { if ( ! is_array( $theme ) ) { continue; } // Keys are either the literal 'default' or a real event ID. if ( 'default' === $key ) { $clean['default'] = seatt_sanitize_display_theme( $theme ); continue; } $event_id = intval( $key ); if ( $event_id > 0 && 'seatt_event' === get_post_type( $event_id ) ) { $clean[ $event_id ] = seatt_sanitize_display_theme( $theme ); } } return $clean; } public function render_page() { if ( ! current_user_can( 'manage_options' ) ) return; $gateway = get_option( 'seatt_payment_gateway', 'manual' ); $webhook_url = rest_url( 'seatt/v1/payment-webhook' ); $currency = get_option( 'seatt_currency_code', 'IDR' ); $currencies = seatt_currency_list(); $active_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- which tab to show ?>

%


Xendit

Stripe

DOKU

' . esc_html( sprintf( /* translators: %s: configured currency code */ __( 'This store is set to %s. DOKU Checkout only settles in IDR, so payments will be refused until the currency is changed in Store Setting.', 'seathive' ), strtoupper( $currency ) ) ) . ''; } else { esc_html_e( 'DOKU Checkout settles in IDR only, which matches this store.', 'seathive' ); } ?>

Mollie

' . esc_html( $m_cur ?: $m_store ) . '' ); ?>

 

PayPal

' . esc_html( $pp_cur ?: $store_cur ) . '' ); ?>

 




define( 'SEATT_SMTP_PASSWORD', 'your-app-password' );

'seatt_event', 'numberposts' => -1 ) ); // The whole map goes to the browser and comes back as // one JSON field, so switching events in the dropdown // never loses edits made to another event before saving. $seed = array( 'default' => isset( $themes['default'] ) ? array_merge( $tdefaults, $themes['default'] ) : $tdefaults ); foreach ( $all_events as $ev ) { $seed[ $ev->ID ] = isset( $themes[ $ev->ID ] ) ? array_merge( $seed['default'], $themes[ $ev->ID ] ) : null; // null = "inherits the default" } // Legacy rows may still hold a full CSS stack here. foreach ( $seed as $k => $s_theme ) { if ( is_array( $s_theme ) ) { $seed[ $k ]['font'] = seatt_display_normalize_font( $s_theme['font'] ); } } ?>

A1A2 A3A4 A5A6
B1B2 B3B4 B5B6
C1C2 C3C4 C5C6

SEATT_Display_Extra::get( 'tickets' ), 'checkout' => SEATT_Display_Extra::get( 'checkout' ), 'event' => SEATT_Display_Extra::get( 'event' ), 'receipt' => SEATT_Display_Extra::get( 'receipt' ), 'mytickets' => SEATT_Display_Extra::get( 'mytickets' ) ); $seatt_dx_color = function ( $area, $key, $label, $help = '' ) use ( $seatt_dx ) { $name = SEATT_Display_Extra::OPTS[ $area ] . '[' . $key . ']'; echo '
' . '' . ( $help ? '' . esc_html( $help ) . '' : '' ) . '
'; }; $seatt_dx_toggle = function ( $area, $key, $label ) use ( $seatt_dx ) { $name = SEATT_Display_Extra::OPTS[ $area ] . '[' . $key . ']'; echo ''; }; $seatt_dx_text = function ( $area, $key, $label, $placeholder ) use ( $seatt_dx ) { $name = SEATT_Display_Extra::OPTS[ $area ] . '[' . $key . ']'; echo '
' . esc_html__( 'Empty = "Secure payment".', 'seathive' ) . '
'; }; $seatt_dx_radius = function ( $area, $label ) use ( $seatt_dx ) { $name = SEATT_Display_Extra::OPTS[ $area ] . '[radius]'; echo '
'; }; ?> > 16, g = (n >> 8) & 255, b = n & 255; return \'rgb(\' + Math.round(r + (255 - r) * amt) + \',\' + Math.round(g + (255 - g) * amt) + \',\' + Math.round(b + (255 - b) * amt) + \')\'; } function val(area, key) { var i = document.querySelector(\'.esm-dx-in[data-area="\' + area + \'"][data-key="\' + key + \'"]:not([type=hidden])\'); if (!i) return \'\'; return i.type === \'checkbox\' ? i.checked : i.value; } function paint() { var t = document.querySelector(\'[data-preview="tickets"]\'); if (t) { t.style.setProperty(\'--pv-acc\', val(\'tickets\', \'accent\')); t.style.setProperty(\'--pv-on\', val(\'tickets\', \'ontext\')); t.style.setProperty(\'--pv-page\', val(\'tickets\', \'page\')); t.style.setProperty(\'--pv-card\', val(\'tickets\', \'card\')); t.style.setProperty(\'--pv-radius\', val(\'tickets\', \'radius\') + \'px\'); t.querySelector(\'.pv-timer\').style.display = val(\'tickets\', \'timer\') ? \'\' : \'none\'; t.querySelector(\'.pv-trust\').style.display = val(\'tickets\', \'trust\') ? \'\' : \'none\'; } var tt = document.querySelector(\'[data-preview="tickets"] .pv-trust-t\'); if (tt) tt.textContent = val(\'tickets\', \'trusttext\') || SEATT_DX_TRUST; var c = document.querySelector(\'[data-preview="checkout"]\'); if (c) { var own = val(\'checkout\', \'custom\'); c.style.setProperty(\'--pv-acc\', own ? val(\'checkout\', \'accent\') : SEATT_DX_BUY[0]); c.style.setProperty(\'--pv-on\', own ? val(\'checkout\', \'ontext\') : SEATT_DX_BUY[1]); c.style.setProperty(\'--pv-text\', own ? val(\'checkout\', \'text\') : \'#1f2328\'); c.style.setProperty(\'--pv-card\', own ? val(\'checkout\', \'card\') : \'#ffffff\'); c.style.setProperty(\'--pv-back\', val(\'checkout\', \'back\')); c.style.setProperty(\'--pv-radius\', val(\'checkout\', \'radius\') + \'px\'); document.querySelectorAll(\'.esm-dx-in[data-area="checkout"]:not([data-key=custom]):not([data-key=back]):not([data-key=radius]):not([data-key=timer]):not([data-key=trust]):not([data-key=trusttext])\').forEach(function (i) { i.closest(\'.esm-dx-field\').style.opacity = own ? \'\' : \'.45\'; }); c.querySelector(\'.pv-timer\').style.display = val(\'checkout\', \'timer\') ? \'\' : \'none\'; c.querySelector(\'.pv-trust\').style.display = val(\'checkout\', \'trust\') ? \'\' : \'none\'; c.querySelector(\'.pv-trust-t\').textContent = val(\'checkout\', \'trusttext\') || SEATT_DX_TRUST; } var e = document.querySelector(\'[data-preview="event"]\'); if (e) { e.style.setProperty(\'--pv-acc\', val(\'event\', \'accent\')); e.style.setProperty(\'--pv-tabtext\', val(\'event\', \'tabtext\')); e.style.setProperty(\'--pv-tabhover\', val(\'event\', \'tabhover\')); e.style.setProperty(\'--pv-herobg\', val(\'event\', \'herobg\')); e.style.setProperty(\'--pv-herotext\', val(\'event\', \'herotext\')); e.style.setProperty(\'--pv-radius\', val(\'event\', \'radius\') + \'px\'); e.dataset.hero = val(\'event\', \'hero\'); e.querySelector(\'.pv-share\').style.display = val(\'event\', \'share\') ? \'\' : \'none\'; e.querySelector(\'.pv-terms\').style.display = val(\'event\', \'terms\') ? \'\' : \'none\'; } var r = document.querySelector(\'[data-preview="receipt"]\'); if (r) { var a = val(\'receipt\', \'accent\'); r.style.setProperty(\'--pv-acc\', a); r.style.setProperty(\'--pv-soft\', mix(a, .88)); r.querySelector(\'.pv-banner\').style.display = val(\'receipt\', \'banner\') ? \'\' : \'none\'; r.querySelector(\'.pv-pdf\').style.display = val(\'receipt\', \'pdf\') ? \'\' : \'none\'; r.querySelector(\'.pv-cal\').style.display = val(\'receipt\', \'calendar\') ? \'\' : \'none\'; r.querySelector(\'.pv-note\').textContent = val(\'receipt\', \'note\'); } var m = document.querySelector(\'[data-preview="mytickets"]\'); if (m) { var ma = val(\'mytickets\', \'accent\'); m.style.setProperty(\'--pv-acc\', ma); m.style.setProperty(\'--pv-on\', val(\'mytickets\', \'ontext\')); m.style.setProperty(\'--pv-soft\', mix(ma, .88)); m.style.setProperty(\'--pv-page\', val(\'mytickets\', \'page\')); m.style.setProperty(\'--pv-card\', val(\'mytickets\', \'card\')); m.style.setProperty(\'--pv-text\', val(\'mytickets\', \'text\')); m.style.setProperty(\'--pv-radius\', val(\'mytickets\', \'radius\') + \'px\'); } document.querySelectorAll(\'.esm-dx-in[data-key="radius"]\').forEach(function (i) { var o = i.parentNode.querySelector(\'output\'); if (o) o.textContent = i.value + \'px\'; }); } document.querySelectorAll(\'.esm-dx-in\').forEach(function (i) { i.addEventListener(\'input\', paint); i.addEventListener(\'change\', paint); }); paint(); })(); ' ); ?>
' . ( esc_js( __( 'Sending…', 'seathive' ) ) ) . '\'; fetch(' . ( wp_json_encode( esc_url_raw( rest_url( 'seatt/v1/smtp-test' ) ) ) ) . ', { method: \'POST\', headers: { \'Content-Type\': \'application/json\', \'X-WP-Nonce\': ' . ( wp_json_encode( wp_create_nonce( 'wp_rest' ) ) ) . ' }, credentials: \'same-origin\', body: JSON.stringify({ to: to }) }).then(function (res) { return res.json().then(function (data) { return { ok: res.ok, data: data }; }); }).then(function (r) { testBtn.disabled = false; if (r.ok) { out.innerHTML = \'

\' + ' . ( wp_json_encode( __( 'Sent. Check the inbox — and the spam folder, since that is where an unauthenticated sender usually lands.', 'seathive' ) ) ) . ' + \'

\'; } else { // The mail server\'s own words, not a generic // failure: "could not authenticate" and // "connection refused" need entirely // different fixes. var msg = (r.data && r.data.message) ? r.data.message : ' . ( wp_json_encode( __( 'Sending failed.', 'seathive' ) ) ) . '; out.innerHTML = \'

\' + ' . ( wp_json_encode( __( 'Not sent.', 'seathive' ) ) ) . ' + \' \' + String(msg).replace(/[&<>"]/g, function (c) { return { \'&\': \'&\', \'<\': \'<\', \'>\': \'>\', \'"\': \'"\' }[c]; }) + \'

\'; } }).catch(function () { testBtn.disabled = false; out.innerHTML = \'

\' + ' . ( wp_json_encode( __( 'Could not reach the site to run the test.', 'seathive' ) ) ) . ' + \'

\'; }); }); } /* ================= Display tab ================= */ var seedEl = document.getElementById(\'esm-display-seed\'); if (!seedEl) return; var fieldWrap = document.getElementById(\'esm-display-fields\'); var DEFAULTS = ' . ( wp_json_encode( seatt_display_defaults() ) ) . '; // slug -> CSS stack, so the preview can show the real typeface // without any stack ever being posted back. var FONT_STACKS = ' . ( wp_json_encode( wp_list_pluck( seatt_display_fonts(), 'stack' ) ) ) . '; var PRESETS = [ { name: \'' . ( esc_js( __( 'WordPress', 'seathive' ) ) ) . '\', bg: \'#ffffff\', text: \'#1f2328\', accent: \'#2271b1\', btntext: \'#ffffff\' }, { name: \'' . ( esc_js( __( 'Midnight', 'seathive' ) ) ) . '\', bg: \'#14161a\', text: \'#eceff3\', accent: \'#5b9dd9\', btntext: \'#0b0d10\' }, { name: \'' . ( esc_js( __( 'Warm sand', 'seathive' ) ) ) . '\', bg: \'#faf6f0\', text: \'#3a3229\', accent: \'#b4653a\', btntext: \'#ffffff\' }, { name: \'' . ( esc_js( __( 'Forest', 'seathive' ) ) ) . '\', bg: \'#f4f8f4\', text: \'#1e2f22\', accent: \'#2f7d4f\', btntext: \'#ffffff\' }, { name: \'' . ( esc_js( __( 'Aubergine', 'seathive' ) ) ) . '\', bg: \'#f8f5fb\', text: \'#2c1f38\', accent: \'#7b3fa0\', btntext: \'#ffffff\' } ]; var themes; try { themes = JSON.parse(seedEl.getAttribute(\'data-seed\') || \'{}\') || {}; } catch (e) { themes = {}; } if (!themes[\'default\']) themes[\'default\'] = Object.assign({}, DEFAULTS); // Themes saved before a colour existed get its default. Object.keys(themes).forEach(function (k) { themes[k] = Object.assign({}, DEFAULTS, themes[k]); }); var eventSel = document.getElementById(\'esm-display-event\'); var inheritRow = document.getElementById(\'esm-display-inherit-row\'); var inheritBox = document.getElementById(\'esm-display-inherit\'); var editor = document.getElementById(\'esm-display-editor\'); var scopeNote = document.getElementById(\'esm-display-scope-note\'); var app = document.getElementById(\'esm-preview-app\'); var current = \'default\'; /* ---- colour maths: identical to the PHP in seatt_display_css_vars() ---- */ function hex2rgb(h) { h = String(h).replace(\'#\', \'\'); if (h.length === 3) h = h[0]+h[0]+h[1]+h[1]+h[2]+h[2]; return { r: parseInt(h.slice(0,2),16), g: parseInt(h.slice(2,4),16), b: parseInt(h.slice(4,6),16) }; } function rgb2hex(r, g, b) { return \'#\' + [r,g,b].map(function (v) { return Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, \'0\'); }).join(\'\'); } function luminance(hex) { var c = hex2rgb(hex); var a = [c.r, c.g, c.b].map(function (v) { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }); return 0.2126*a[0] + 0.7152*a[1] + 0.0722*a[2]; } function contrast(a, b) { var l1 = luminance(a), l2 = luminance(b); return (Math.max(l1,l2) + 0.05) / (Math.min(l1,l2) + 0.05); } function mix(a, b, amount) { var x = hex2rgb(a), y = hex2rgb(b); return rgb2hex(x.r + (y.r-x.r)*amount, x.g + (y.g-x.g)*amount, x.b + (y.b-x.b)*amount); } function isHex(v) { return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(v); } /** The theme currently being edited (default, or an event override). */ function active() { return (current === \'default\') ? themes[\'default\'] : themes[current]; } function paint() { var t = (current === \'default\' || themes[current]) ? active() : themes[\'default\']; var s = app.style; s.setProperty(\'--esm-bg\', t.bg); s.setProperty(\'--esm-text\', t.text); s.setProperty(\'--esm-muted\', mix(t.text, t.bg, 0.42)); s.setProperty(\'--esm-border\', mix(t.text, t.bg, 0.86)); s.setProperty(\'--esm-surface\', mix(t.bg, t.text, 0.035)); s.setProperty(\'--esm-accent\', t.accent); s.setProperty(\'--esm-accent-hover\', luminance(t.accent) < 0.12 ? mix(t.accent, \'#ffffff\', 0.22) : mix(t.accent, \'#000000\', 0.18)); s.setProperty(\'--esm-accent-text\', t.btntext); s.setProperty(\'--esm-waitlist\', t.waitlist); s.setProperty(\'--swp-buy-bg\', t.buybg); s.setProperty(\'--swp-buy-text\', t.buytext); s.setProperty(\'--esm-font\', FONT_STACKS[t.font] || FONT_STACKS[\'system\']); s.setProperty(\'--esm-radius\', parseInt(t.radius, 10) + \'px\'); loadGoogleFont(t.font); warn(\'esm-warn-text\', contrast(t.text, t.bg), \'' . ( esc_js( __( 'Text on the background', 'seathive' ) ) ) . '\'); warn(\'esm-warn-btn\', contrast(t.btntext, t.accent), \'' . ( esc_js( __( 'Button text on the button', 'seathive' ) ) ) . '\'); writeFields(); } /** * State goes into ordinary named inputs — * seatt_display_themes[42][bg] — rather than one JSON field. * Every posted value is then a bare hex colour, a lowercase * slug or an integer, which sails past the mod_security rules * that were killing the connection when the payload carried * quoted font stacks inside quoted JSON. * * Rebuilt from scratch each time rather than patched: an * event switched back to "inherit" must vanish from the post * entirely, since sanitize_display_themes() treats absence as * "no override". */ function writeFields() { var html = \'\'; Object.keys(themes).forEach(function (key) { var t = themes[key]; if (!t) return; // inheriting: post nothing [\'bg\', \'text\', \'accent\', \'btntext\', \'buybg\', \'buytext\', \'waitlist\', \'font\', \'radius\'].forEach(function (prop) { html += \'\'; }); }); fieldWrap.innerHTML = html; } function warn(id, ratio, label) { var el = document.getElementById(id); el.classList.remove(\'esm-show\', \'esm-bad\'); if (ratio >= 4.5) return; el.classList.add(\'esm-show\'); if (ratio < 2.5) { el.classList.add(\'esm-bad\'); el.textContent = label + \': ' . ( esc_js( __( 'unreadable at this pairing', 'seathive' ) ) ) . ' (\' + ratio.toFixed(1) + \':1). ' . ( esc_js( __( 'Pick a much lighter or much darker colour.', 'seathive' ) ) ) . '\'; } else { el.textContent = label + \': ' . ( esc_js( __( 'hard to read', 'seathive' ) ) ) . ' (\' + ratio.toFixed(1) + \':1, ' . ( esc_js( __( 'aim for 4.5:1', 'seathive' ) ) ) . ').\'; } } // Admin-side only, so the seller sees the real typeface while // choosing. The buyer page loads its own copy, and only when a // Google font is actually selected. var loadedFonts = {}; function loadGoogleFont(slug) { // Lookup by iteration rather than a selector: a legacy value // carrying a full font stack would make an invalid selector // and throw, taking the whole tab\'s JS down with it. var fam = \'\'; for (var i = 0; i < fontSel.options.length; i++) { if (fontSel.options[i].value === slug) { fam = fontSel.options[i].dataset.google; break; } } if (!fam || loadedFonts[fam]) return; loadedFonts[fam] = true; var l = document.createElement(\'link\'); l.rel = \'stylesheet\'; l.href = \'https://fonts.googleapis.com/css2?family=\' + fam + \'&display=swap\'; document.head.appendChild(l); } function syncControls() { var t = (current === \'default\' || themes[current]) ? active() : themes[\'default\']; [[\'bg\',\'esm-c-bg\',\'esm-t-bg\'], [\'text\',\'esm-c-text\',\'esm-t-text\'], [\'accent\',\'esm-c-accent\',\'esm-t-accent\'], [\'btntext\',\'esm-c-btntext\',\'esm-t-btntext\'], [\'buybg\',\'esm-c-buybg\',\'esm-t-buybg\'], [\'buytext\',\'esm-c-buytext\',\'esm-t-buytext\'], [\'waitlist\',\'esm-c-waitlist\',\'esm-t-waitlist\'] ].forEach(function (row) { document.getElementById(row[1]).value = t[row[0]]; document.getElementById(row[2]).value = String(t[row[0]]).toUpperCase(); }); fontSel.value = t.font; radius.value = parseInt(t.radius, 10); document.getElementById(\'esm-radius-val\').textContent = parseInt(t.radius, 10) + \'px\'; describeFont(); } function bindColor(key, colorId, textId) { var picker = document.getElementById(colorId), text = document.getElementById(textId); picker.addEventListener(\'input\', function () { active()[key] = picker.value; text.value = picker.value.toUpperCase(); paint(); }); text.addEventListener(\'input\', function () { var v = text.value.trim(); if (v[0] !== \'#\') v = \'#\' + v; if (!isHex(v)) return; // mid-typing: leave it alone rather than fighting the cursor active()[key] = v; picker.value = (v.length === 4) ? \'#\' + v[1]+v[1]+v[2]+v[2]+v[3]+v[3] : v; paint(); }); text.addEventListener(\'blur\', function () { text.value = String(active()[key]).toUpperCase(); picker.value = active()[key]; }); } var fontSel = document.getElementById(\'esm-f-font\'); var radius = document.getElementById(\'esm-f-radius\'); function describeFont() { var opt = fontSel.options[fontSel.selectedIndex]; document.getElementById(\'esm-font-desc\').textContent = (opt && opt.dataset.google) ? \'' . ( esc_js( __( 'Downloaded from Google Fonts on each visit — roughly 20–40 KB added to the booking page.', 'seathive' ) ) ) . '\' : \'' . ( esc_js( __( "No extra file is downloaded — this uses a font the visitor's device already has.", 'seathive' ) ) ) . '\'; } bindColor(\'bg\', \'esm-c-bg\', \'esm-t-bg\'); bindColor(\'text\', \'esm-c-text\', \'esm-t-text\'); bindColor(\'accent\', \'esm-c-accent\', \'esm-t-accent\'); bindColor(\'btntext\', \'esm-c-btntext\', \'esm-t-btntext\'); bindColor(\'buybg\', \'esm-c-buybg\', \'esm-t-buybg\'); bindColor(\'buytext\', \'esm-c-buytext\', \'esm-t-buytext\'); bindColor(\'waitlist\', \'esm-c-waitlist\', \'esm-t-waitlist\'); fontSel.addEventListener(\'change\', function () { active().font = fontSel.value; describeFont(); paint(); }); radius.addEventListener(\'input\', function () { active().radius = parseInt(radius.value, 10); document.getElementById(\'esm-radius-val\').textContent = radius.value + \'px\'; paint(); }); var presetWrap = document.getElementById(\'esm-display-presets\'); PRESETS.forEach(function (p) { var b = document.createElement(\'button\'); b.type = \'button\'; b.innerHTML = \'\' + p.name; b.addEventListener(\'click\', function () { var t = active(); t.bg = p.bg; t.text = p.text; t.accent = p.accent; t.btntext = p.btntext; syncControls(); paint(); }); presetWrap.appendChild(b); }); document.getElementById(\'esm-display-reset\').addEventListener(\'click\', function () { var t = active(); Object.keys(DEFAULTS).forEach(function (k) { t[k] = DEFAULTS[k]; }); syncControls(); paint(); }); // A null entry means "this event inherits the default" — kept // distinct from a copy of the default\'s values, so later edits // to the default keep flowing through to it. function refreshScope() { var isDefault = (current === \'default\'); inheritRow.style.display = isDefault ? \'none\' : \'\'; document.getElementById(\'esm-pv-event-name\').textContent = isDefault ? \'' . ( esc_js( __( 'Your event', 'seathive' ) ) ) . '\' : eventSel.options[eventSel.selectedIndex].text; if (isDefault) { scopeNote.textContent = \'' . ( esc_js( __( 'Applies to every event that has no theme of its own.', 'seathive' ) ) ) . '\'; editor.style.display = \'\'; } else { var inherits = !themes[current]; inheritBox.checked = inherits; editor.style.display = inherits ? \'none\' : \'\'; scopeNote.textContent = inherits ? \'' . ( esc_js( __( 'This event follows the default theme.', 'seathive' ) ) ) . '\' : \'' . ( esc_js( __( 'This event has its own theme, overriding the default.', 'seathive' ) ) ) . '\'; } syncControls(); paint(); } eventSel.addEventListener(\'change\', function () { current = this.value === \'default\' ? \'default\' : this.value; refreshScope(); }); inheritBox.addEventListener(\'change\', function () { if (this.checked) { themes[current] = null; } else { themes[current] = Object.assign({}, themes[\'default\']); } refreshScope(); }); document.querySelectorAll(\'.esm-device-toggle button\').forEach(function (b) { b.addEventListener(\'click\', function () { document.querySelectorAll(\'.esm-device-toggle button\').forEach(function (x) { x.classList.remove(\'esm-on\'); }); b.classList.add(\'esm-on\'); document.getElementById(\'esm-stage-inner\').classList.toggle(\'esm-mobile\', b.dataset.w === \'mobile\'); }); }); refreshScope(); })(); ' ); ?>