array( 'name' => __( 'Events', 'seathive' ), 'singular_name' => __( 'Event', 'seathive' ), 'add_new_item' => __( 'Add New Event', 'seathive' ), 'edit_item' => __( 'Edit Event', 'seathive' ), ), 'public' => true, 'show_in_menu' => 'seatt-main', 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ), 'show_in_rest' => true, 'menu_icon' => 'dashicons-tickets-alt', 'rewrite' => array( 'slug' => 'events' ), ) ); } /* --------------------------------------------------------------- * Event Settings meta box: event date, free/paid booking, and * default price per seat category (used as a reference — and as a * pre-fill — when building the chart's seat prices). * ------------------------------------------------------------- */ const BANNER_MAX_BYTES = 2097152; // 2 MB /** Banner guideline shown under the featured-image box. */ public static function banner_note_text() { return __( 'Recommended size: 1920 × 1080 px (16:9), JPG or WebP. Maximum file size 2 MB — larger files are rejected. Keep important text (event name, date, logo) in the centre, about 10% away from the top and bottom edges.', 'seathive' ); } public static function banner_too_big_message( $bytes ) { return sprintf( /* translators: %s: file size, e.g. 3.4 MB */ __( 'This image is %s. Event images can be at most 2 MB — please make it smaller (for example with squoosh.app or tinypng.com) and upload it again.', 'seathive' ), number_format_i18n( $bytes / 1048576, 1 ) . ' MB' ); } /** Uploads made from an event's edit screen (classic media modal or block editor). */ public function limit_banner_upload( $file ) { $post_id = intval( $_REQUEST['post_id'] ?? ( $_REQUEST['post'] ?? 0 ) ); // phpcs:ignore WordPress.Security.NonceVerification if ( ! $post_id || 'seatt_event' !== get_post_type( $post_id ) || empty( $file['size'] ) ) { return $file; } if ( 0 === strpos( (string) ( $file['type'] ?? '' ), 'image/' ) && (int) $file['size'] > self::BANNER_MAX_BYTES ) { $file['error'] = self::banner_too_big_message( (int) $file['size'] ); } return $file; } private static function attachment_bytes( $attachment_id ) { $path = $attachment_id ? get_attached_file( (int) $attachment_id ) : ''; return ( $path && file_exists( $path ) ) ? (int) filesize( $path ) : 0; } /** Block editor: picking a too-large library image as banner shows an error. */ public function limit_banner_rest( $prepared, $request ) { $media = $request->get_param( 'featured_media' ); if ( $media ) { $bytes = self::attachment_bytes( (int) $media ); if ( $bytes > self::BANNER_MAX_BYTES ) { return new WP_Error( 'seatt_banner_too_big', self::banner_too_big_message( $bytes ), array( 'status' => 400 ) ); } } return $prepared; } /** Classic editor / any other path: never store a too-large banner. */ public function limit_banner_meta( $check, $object_id, $meta_key, $meta_value ) { if ( '_thumbnail_id' !== $meta_key || 'seatt_event' !== get_post_type( $object_id ) ) { return $check; } return self::attachment_bytes( (int) $meta_value ) > self::BANNER_MAX_BYTES ? false : $check; } /** Classic editor: note under "Set featured image". */ public function banner_size_note( $html, $post_id ) { if ( 'seatt_event' !== get_post_type( $post_id ) ) { return $html; } return $html . '

' . esc_html( self::banner_note_text() ) . '

'; } /** Block editor: the same note under the Featured image panel. */ public function enqueue_media_for_event( $hook ) { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( $screen && 'seatt_event' === $screen->post_type && in_array( $hook, array( 'post.php', 'post-new.php' ), true ) ) { wp_enqueue_media(); wp_enqueue_script( 'jquery-ui-sortable' ); } } /** "Couldn't remove category X — it has sold seats." */ public function tickets_notice() { $m = get_transient( 'seatt_tickets_msg_' . get_current_user_id() ); if ( $m ) { delete_transient( 'seatt_tickets_msg_' . get_current_user_id() ); echo '

' . esc_html( $m ) . '

'; } } public function cats_kept_notice() { $kept = get_transient( 'seatt_cats_kept_' . get_current_user_id() ); if ( $kept ) { delete_transient( 'seatt_cats_kept_' . get_current_user_id() ); echo '

' . esc_html( sprintf( /* translators: %s: value */ __( 'These categories were kept because they have sold seats: %s. Move those seats to another category in the Chart Builder first.', 'seathive' ), implode( ', ', (array) $kept ) ) ) . '

'; } } public function banner_size_note_block_editor() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || 'seatt_event' !== $screen->post_type ) { return; } wp_register_script( 'seatt-banner-note', false, array( 'wp-hooks', 'wp-element' ), SEATT_VERSION, true ); wp_enqueue_script( 'seatt-banner-note' ); wp_add_inline_script( 'seatt-banner-note', '(function (hooks, el) { var note = ' . wp_json_encode( self::banner_note_text() ) . '; hooks.addFilter("editor.PostFeaturedImage", "seatwp/banner-note", function (Original) { return function (props) { return el.createElement(el.Fragment, null, el.createElement(Original, props), el.createElement("p", { className: "description", style: { marginTop: "8px", fontSize: "12px", color: "#757575" } }, note) ); }; }); })(window.wp.hooks, window.wp.element);' ); } /** * WordPress remembers each user's meta-box layout (drag & drop or the * ▲▼ buttons). If "Event Settings" ended up in the sidebar it becomes * a cramped column; this puts it back at the top of the main area. */ public function keep_settings_box_in_main( $order ) { if ( ! is_array( $order ) ) { return $order; } $box = 'seatt_event_settings'; foreach ( $order as $ctx => $list ) { $ids = array_filter( array_map( 'trim', explode( ',', (string) $list ) ), 'strlen' ); if ( 'normal' !== $ctx && in_array( $box, $ids, true ) ) { $order[ $ctx ] = implode( ',', array_diff( $ids, array( $box ) ) ); } } $normal = array_filter( array_map( 'trim', explode( ',', (string) ( $order['normal'] ?? '' ) ) ), 'strlen' ); $order['normal'] = implode( ',', array_merge( array( $box ), array_diff( $normal, array( $box ) ) ) ); return $order; } public function add_meta_boxes() { add_meta_box( 'seatt_event_cancel', __( 'Cancel event', 'seathive' ), array( $this, 'render_cancel_box' ), 'seatt_event', 'side', 'low' ); add_meta_box( 'seatt_event_settings', __( 'Event Settings', 'seathive' ), array( $this, 'render_meta_box' ), 'seatt_event', 'normal', 'high' ); add_meta_box( 'seatt_event_shortcode', __( 'Selling Page Shortcode', 'seathive' ), array( $this, 'render_shortcode_box' ), 'seatt_event', 'side', 'high' ); } /** * The shortcode a customer's booking page needs — shown right on * this event's own edit screen, ready to copy the moment the event * is saved, instead of it living on a separate "Sell Seats" admin * page the admin had to remember to go find (and which, before this, * doubled as an entirely separate in-wp-admin selling flow that * turned out to be more confusing than useful — this one box now * covers both what that page's original and reworked versions were * trying to do: hand over exactly the code needed, right where an * admin is already looking while setting the event up). */ public function render_shortcode_box( $post ) { // auto-draft/newly-created posts don't have a real ID a // shortcode could book against yet — WordPress only assigns one // once the post is actually saved (even as a Draft). if ( 'auto-draft' === $post->post_status ) { echo '

' . esc_html__( 'Save or publish this event first — the shortcode will appear here once it has an ID to point to.', 'seathive' ) . '

'; return; } $shortcode = '[seatt_booking event_id="' . $post->ID . '"]'; ?>

get_post_meta( $event_id, '_seatt_event_date', true ), 'booking_type' => get_post_meta( $event_id, '_seatt_booking_type', true ) ?: 'paid', 'price_standard' => get_post_meta( $event_id, '_seatt_price_standard', true ), 'price_vip' => get_post_meta( $event_id, '_seatt_price_vip', true ), 'price_reserved' => get_post_meta( $event_id, '_seatt_price_reserved', true ), 'price_standing' => get_post_meta( $event_id, '_seatt_price_standing', true ), ); } public function render_meta_box( $post ) { // One date of a multi-date event: its settings come from the main event. if ( class_exists( 'SEATT_Series' ) && SEATT_Series::render_child_box( $post ) ) { return; } wp_nonce_field( 'seatt_event_settings_save', 'seatt_event_settings_nonce' ); $s = self::get_settings( $post->ID ); $gateway = get_option( 'seatt_payment_gateway', 'manual' ); $gateway_labels = array( 'manual' => __( 'Cash', 'seathive' ), 'xendit' => 'Xendit', 'stripe' => 'Stripe', 'paypal' => 'PayPal', 'doku' => 'DOKU' ); ?>

–

ID, '_seatt_seatmap_banner', true ); $smb_url = $smb ? wp_get_attachment_image_url( $smb, 'medium_large' ) : ''; ?>

2097152) { window.alert(' . ( wp_json_encode( __( 'This image is larger than 2 MB. Please make it smaller and choose it again.', 'seathive' ) ) ) . '); return; } var url = (a.sizes && (a.sizes.medium_large || a.sizes.large || a.sizes.full) || a).url; $(\'#seatt_seatmap_banner\').val(a.id); $(\'#esm-smb-preview\').css({ display: \'block\', backgroundImage: \'url(\' + url + \')\' }); $(\'#esm-smb-remove\').show(); $(\'#esm-smb-pick\').text(' . ( wp_json_encode( __( 'Change image', 'seathive' ) ) ) . '); }); } frame.open(); }); $(\'#esm-smb-remove\').on(\'click\', function (e) { e.preventDefault(); $(\'#seatt_seatmap_banner\').val(\'\'); $(\'#esm-smb-preview\').hide(); $(this).hide(); $(\'#esm-smb-pick\').text(' . ( wp_json_encode( __( 'Choose image', 'seathive' ) ) ) . '); }); }); ' ); ?>

rich_editor( 'esmeventdescription', 'seatt_event_description', (string) get_post_meta( $post->ID, '_seatt_event_description', true ) ); ?>

rich_editor( 'esmeventterms', 'seatt_event_terms', (string) get_post_meta( $post->ID, '_seatt_event_terms', true ) ); ?>

' . esc_html( $gateway_labels[ $gateway ] ?? $gateway ) . '', '' . esc_html__( 'Payment Settings', 'seathive' ) . '' ); ?>

' . esc_html( $active_currency_code ) . '', esc_html( $active_currency_symbol ), '' . esc_html__( 'Settings → Store Setting', 'seathive' ) . '' ); ?>

ID, $active_currency_symbol ); ?>
ID, $active_currency_symbol ); ?>
the way the old code did. var activeCurrencyCode = ' . ( wp_json_encode( $active_currency_code ) ) . '; var priceInputs = document.querySelectorAll(\'.esm-price-input\'); function formatIdr(digitsOnly) { return digitsOnly.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \'.\'); } function rawInputFor(displayInput) { return document.querySelector(\'input.esm-price-raw[name="\' + displayInput.dataset.target + \'"]\'); } // Keeps the HIDDEN field (the one actually named/submitted) // in sync with the visible, human-formatted one on every // keystroke — rather than trying to clean the value up in a // \'submit\' event handler right before sending. That worked // for a normal HTML form post, but this screen is the // Gutenberg block editor: WordPress saves it via a hidden // legacy-meta-box form that gets submitted PROGRAMMATICALLY // (form.submit() in JS), and calling .submit() that way never // fires the form\'s \'submit\' event at all — so the cleanup // silently never ran, and "150.000" (meant as 150,000) went // to the server exactly as typed, where PHP\'s floatval() // read the "." as a decimal point and saved plain 150. function syncRaw(displayInput) { var raw = rawInputFor(displayInput); if (!raw) return; raw.value = activeCurrencyCode === \'IDR\' ? displayInput.value.replace(/\\D/g, \'\') : displayInput.value.replace(/[^\\d.]/g, \'\'); } function reformatAll() { priceInputs.forEach(function (input) { if (activeCurrencyCode === \'IDR\') { input.value = formatIdr(input.value.replace(/\\D/g, \'\')); } else { input.value = input.value.replace(/[^\\d.]/g, \'\'); } syncRaw(input); }); } // Initial display pass — the DB stores a plain number either // way, so a saved IDR value like "50000" becomes "50.000" here. reformatAll(); priceInputs.forEach(function (input) { input.addEventListener(\'input\', function () { if (activeCurrencyCode === \'IDR\') { var caretFromEnd = this.value.length - this.selectionStart; this.value = formatIdr(this.value.replace(/\\D/g, \'\')); var pos = Math.max(0, this.value.length - caretFromEnd); this.setSelectionRange(pos, pos); } syncRaw(this); }); }); })(); ' ); ?> $f ) { if ( 'system' === $slug ) { continue; } // Unquoted family names: they survive wp_kses_post's // style filtering and are valid CSS as-is. $fonts[] = $f['label'] . '=' . str_replace( '"', '', $f['stack'] ); } } $google = array(); if ( function_exists( 'seatt_display_fonts' ) ) { foreach ( seatt_display_fonts() as $f ) { if ( ! empty( $f['google'] ) ) { $google[] = 'family=' . $f['google']; } } } wp_editor( $content, $editor_id, array( 'textarea_name' => $name, 'textarea_rows' => 10, 'media_buttons' => false, 'teeny' => false, 'quicktags' => array( 'buttons' => 'strong,em,ul,ol,li,link,close' ), 'tinymce' => array( 'toolbar1' => 'formatselect,fontselect,fontsizeselect,bold,italic,underline,forecolor,bullist,numlist,alignleft,aligncenter,alignright,link,unlink,removeformat,undo,redo', 'toolbar2' => '', 'block_formats' => __( 'Paragraph', 'seathive' ) . '=p;' . __( 'Heading', 'seathive' ) . ' 2=h2;' . __( 'Heading', 'seathive' ) . ' 3=h3;' . __( 'Heading', 'seathive' ) . ' 4=h4', 'font_formats' => implode( ';', $fonts ), 'fontsize_formats' => '12px 14px 16px 18px 20px 24px 28px 32px', 'wpautop' => true, ) + ( $google ? array( 'content_css' => 'https://fonts.googleapis.com/css2?' . implode( '&', $google ) . '&display=swap' ) : array() ), ) ); } public function render_cancel_box( $post ) { if ( SEATT_Event_Cancel::is_cancelled( $post->ID ) ) { echo '

' . esc_html__( 'This event is cancelled.', 'seathive' ) . '

'; echo '

' . esc_html( sprintf( /* translators: %s: value */ __( 'Cancelled on %s', 'seathive' ), mysql2date( 'j M Y H:i', get_post_meta( $post->ID, '_seatt_cancelled_at', true ) ) ) ) . '

'; echo '

' . esc_html__( 'Manage refunds', 'seathive' ) . '

'; return; } ?>

ID ) . '); add(\'reason\', document.getElementById(\'esm-cancel-reason\').value); add(\'_wpnonce\', ' . ( wp_json_encode( wp_create_nonce( 'seatt_cancel_event_' . $post->ID ) ) ) . '); document.body.appendChild(f); f.submit(); }); ' ); ?> '_seatt_event_time', 'seatt_event_end_time' => '_seatt_event_end_time' ) as $f => $k ) { $t = sanitize_text_field( wp_unslash( $_POST[ $f ] ?? '' ) ); update_post_meta( $post_id, $k, preg_match( '/^\d{2}:\d{2}$/', $t ) ? $t : '' ); } } if ( isset( $_POST['seatt_event_date'] ) ) { update_post_meta( $post_id, '_seatt_event_date', sanitize_text_field( wp_unslash( $_POST['seatt_event_date'] ) ) ); } if ( isset( $_POST['seatt_hold_minutes'] ) ) { $hold = absint( wp_unslash( $_POST['seatt_hold_minutes'] ) ); update_post_meta( $post_id, '_seatt_hold_minutes', $hold > 0 ? max( SEATT_DB::HOLD_MINUTES_MIN, min( SEATT_DB::HOLD_MINUTES_MAX, $hold ) ) : '' ); } if ( isset( $_POST['seatt_max_seats'] ) ) { $max = (int) $_POST['seatt_max_seats']; update_post_meta( $post_id, '_seatt_max_seats', $max > 0 ? min( 100, $max ) : '' ); update_post_meta( $post_id, '_seatt_max_seats_scope', 'person' === sanitize_key( wp_unslash( $_POST['seatt_max_seats_scope'] ?? '' ) ) ? 'person' : 'order' ); } if ( isset( $_POST['seatt_earlybird_value'] ) ) { update_post_meta( $post_id, '_seatt_earlybird_value', max( 0, (float) $_POST['seatt_earlybird_value'] ) ); update_post_meta( $post_id, '_seatt_earlybird_type', 'amount' === sanitize_key( wp_unslash( $_POST['seatt_earlybird_type'] ?? '' ) ) ? 'amount' : 'percent' ); $until = sanitize_text_field( wp_unslash( $_POST['seatt_earlybird_until'] ?? '' ) ); update_post_meta( $post_id, '_seatt_earlybird_until', preg_match( '/^\d{4}-\d{2}-\d{2}$/', $until ) ? $until : '' ); } if ( isset( $_POST['seatt_booking_type'] ) ) { $type = 'free' === sanitize_key( wp_unslash( $_POST['seatt_booking_type'] ) ) ? 'free' : 'paid'; update_post_meta( $post_id, '_seatt_booking_type', $type ); } if ( isset( $_POST['seatt_sales_mode'] ) ) { $tmsg = SEATT_Tickets::save( $post_id, sanitize_key( wp_unslash( $_POST['seatt_sales_mode'] ) ), wp_unslash( $_POST['seatt_ticket_types'] ?? '[]' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON; every field is sanitized in SEATT_Tickets::clean() if ( $tmsg ) { set_transient( 'seatt_tickets_msg_' . get_current_user_id(), $tmsg, 120 ); } } if ( isset( $_POST['seatt_categories'] ) && ! SEATT_Tickets::is_ticket_mode( $post_id ) ) { $res = SEATT_Categories::save( $post_id, wp_unslash( $_POST['seatt_categories'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON; every field is sanitized in SEATT_Categories::save() if ( ! empty( $res['kept'] ) ) { set_transient( 'seatt_cats_kept_' . get_current_user_id(), $res['kept'], 120 ); } } foreach ( array( 'seatt_price_reserved', 'seatt_price_standing' ) as $field ) { if ( isset( $_POST[ $field ] ) ) { update_post_meta( $post_id, '_' . $field, floatval( $_POST[ $field ] ) ); } } /** * Saves the extra Event Settings sections (nonce already checked). * * @param int $post_id The event. */ do_action( 'seatt_event_settings_save', $post_id ); } /* --------------------------------------------------------------- * Events list: Date column + dark mode toggle * ------------------------------------------------------------- */ public function add_columns( $columns ) { $new = array(); foreach ( $columns as $key => $label ) { $new[ $key ] = $label; if ( 'title' === $key ) { $new['seatt_event_date'] = __( 'Event Date', 'seathive' ); $new['seatt_booking_type'] = __( 'Booking', 'seathive' ); } } return $new; } public function render_column( $column, $post_id ) { if ( 'seatt_event_date' === $column ) { $date = get_post_meta( $post_id, '_seatt_event_date', true ); echo $date ? esc_html( mysql2date( 'j M Y', $date ) ) : '—'; } elseif ( 'seatt_booking_type' === $column ) { $type = get_post_meta( $post_id, '_seatt_booking_type', true ) ?: 'paid'; echo 'free' === $type ? '' . esc_html__( 'Free', 'seathive' ) . '' : '' . esc_html__( 'Paid', 'seathive' ) . ''; } } public function sortable_columns( $columns ) { $columns['seatt_event_date'] = 'seatt_event_date'; return $columns; } public function sort_by_event_date( $query ) { if ( ! is_admin() || ! $query->is_main_query() ) { return; } if ( 'seatt_event_date' === $query->get( 'orderby' ) ) { $query->set( 'meta_key', '_seatt_event_date' ); $query->set( 'orderby', 'meta_value' ); } } public function enqueue( $hook ) { global $post_type; if ( 'seatt_event' !== $post_type || ! in_array( $hook, array( 'edit.php', 'post.php', 'post-new.php' ), true ) ) { return; } // esm-shared.css/js used to also carry this screen's dark-mode // toggle — dark mode is now Chart Builder-only, so neither is // needed here anymore. } }