ID, '_rh_tm_instagram', true );
$location = (string) get_post_meta( $post->ID, '_rh_tm_location', true );
$media_ids = get_post_meta( $post->ID, '_rh_tm_media_ids', true );
$media_ids = is_array( $media_ids ) ? array_filter( array_map( 'absint', $media_ids ) ) : array();
// Back-compat: fold a legacy single media id into the list.
if ( empty( $media_ids ) ) {
$legacy = (int) get_post_meta( $post->ID, '_rh_tm_media_id', true );
if ( $legacy ) { $media_ids = array( $legacy ); }
}
$poster_id = (int) get_post_meta( $post->ID, '_rh_tm_poster_id', true );
$poster_url = $poster_id ? wp_get_attachment_image_url( $poster_id, 'medium' ) : '';
$pinned = (int) get_post_meta( $post->ID, '_rh_tm_pinned', true );
$product_id = (int) get_post_meta( $post->ID, '_rh_tm_product', true );
$scope = TestimonialType::sanitizeScope( get_post_meta( $post->ID, '_rh_tm_scope', true ) );
$via_insta = (int) get_post_meta( $post->ID, '_rh_tm_via_instagram', true );
$verified = (int) get_post_meta( $post->ID, '_rh_tm_verified', true );
$scopes = array(
'all' => __( 'همهجا — اسلایدر نظرات و صفحهٔ محصول', 'rahkar-toolkit' ),
'product' => __( 'فقط صفحهٔ محصول', 'rahkar-toolkit' ),
'rail' => __( 'فقط اسلایدر نظرات', 'rahkar-toolkit' ),
);
?>
.rh-tm-mb .rh-tm-row { margin: 14px 0; }
.rh-tm-mb label.rh-tm-lbl { display:block; font-weight:var(--wp--custom--fw--strong); margin-bottom:6px; }
.rh-tm-mb input[type=text], .rh-tm-mb select { width: 100%; max-width: 420px; }
.rh-tm-mb .rh-tm-file { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.rh-tm-mb .rh-tm-filename { color:#555; }
.rh-tm-mb .rh-tm-poster-prev img { max-width:120px; height:auto; border-radius:6px; margin-top:6px; display:block; }
.rh-tm-mb .rh-tm-hint { color:#777; font-size:12px; margin-top:4px; }
.rh-tm-mb .rh-tm-media-prev { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
.rh-tm-mb .rh-tm-thumb img { width:60px; height:60px; object-fit:cover; border-radius:6px; display:block; background:#eee; }
( function () {
/* ── Media: multiple, append to the existing selection ── */
var mediaBtn = document.querySelector( '.rh-tm-pick-media' );
var mediaClear = document.querySelector( '.rh-tm-clear-media' );
var idsInput = document.getElementById( 'rh-tm-media-ids' );
var mediaPrev = document.querySelector( '.rh-tm-media-prev' );
var mediaFrame = null;
function currentIds() { return idsInput.value ? idsInput.value.split( ',' ).filter( Boolean ) : []; }
function thumbUrl( a ) { return ( a.sizes && a.sizes.thumbnail && a.sizes.thumbnail.url ) || a.icon || a.url; }
function addThumb( a ) {
var s = document.createElement( 'span' );
s.className = 'rh-tm-thumb';
s.innerHTML = '
';
mediaPrev.appendChild( s );
}
mediaBtn && mediaBtn.addEventListener( 'click', function ( e ) {
e.preventDefault();
mediaFrame = wp.media( {
title: ,
button: { text: },
multiple: 'add',
} );
mediaFrame.on( 'select', function () {
var ids = currentIds();
mediaFrame.state().get( 'selection' ).toJSON().forEach( function ( a ) {
if ( ids.indexOf( String( a.id ) ) === -1 ) { ids.push( String( a.id ) ); addThumb( a ); }
} );
idsInput.value = ids.join( ',' );
if ( ids.length && mediaClear ) { mediaClear.style.display = ''; }
} );
mediaFrame.open();
} );
mediaClear && mediaClear.addEventListener( 'click', function ( e ) {
e.preventDefault();
idsInput.value = '';
mediaPrev.innerHTML = '';
mediaClear.style.display = 'none';
} );
/* ── Poster: single image ── */
var posBtn = document.querySelector( '.rh-tm-pick-poster' );
var posClear = document.querySelector( '.rh-tm-clear-poster' );
var posInput = document.getElementById( 'rh-tm-poster-id' );
var posPrev = document.querySelector( '.rh-tm-poster-prev' );
var posFrame = null;
posBtn && posBtn.addEventListener( 'click', function ( e ) {
e.preventDefault();
if ( posFrame ) { posFrame.open(); return; }
posFrame = wp.media( {
title: ,
library: { type: 'image' },
multiple: false,
} );
posFrame.on( 'select', function () {
var a = posFrame.state().get( 'selection' ).first().toJSON();
posInput.value = a.id;
var url = ( a.sizes && a.sizes.medium && a.sizes.medium.url ) || a.url;
posPrev.innerHTML = url ? '
' : '';
if ( posClear ) { posClear.style.display = ''; }
} );
posFrame.open();
} );
posClear && posClear.addEventListener( 'click', function ( e ) {
e.preventDefault();
posInput.value = '';
posPrev.innerHTML = '';
posClear.style.display = 'none';
} );
} )();
post_type ) {
return;
}
wp_enqueue_media();
// WooCommerce's enhanced select powers the AJAX product search field. Both
// handles are registered by WC itself; we only ask for them on this one
// screen, so no other admin page pays for select2.
if ( class_exists( 'WooCommerce' ) ) {
wp_enqueue_script( 'wc-enhanced-select' );
wp_enqueue_style( 'woocommerce_admin_styles' );
}
}
/**
* Persist what the moderator changed.
*
* @param int $post_id Testimonial id.
* @param WP_Post $post The post object.
*
* @internal Hooked callback.
*/
public function save( $post_id, $post ): void {
if ( ! isset( $_POST['rh_testimonial_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['rh_testimonial_nonce'] ) ), 'rh_testimonial_save' ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
$instagram = isset( $_POST['rh_tm_instagram'] ) ? sanitize_text_field( wp_unslash( $_POST['rh_tm_instagram'] ) ) : '';
$location = isset( $_POST['rh_tm_location'] ) ? sanitize_text_field( wp_unslash( $_POST['rh_tm_location'] ) ) : '';
$ids_raw = isset( $_POST['rh_tm_media_ids'] ) ? sanitize_text_field( wp_unslash( $_POST['rh_tm_media_ids'] ) ) : '';
$media_ids = array_values( array_filter( array_map( 'absint', explode( ',', $ids_raw ) ) ) );
$poster_id = isset( $_POST['rh_tm_poster_id'] ) ? absint( $_POST['rh_tm_poster_id'] ) : 0;
// Pinned is written on EVERY save, 0 or 1 — never deleted. That keeps the
// "rest" query a cheap `= 0` compare for everything saved from here on.
$pinned = ! empty( $_POST['rh_tm_pinned'] ) ? 1 : 0;
// Product: accept only a real, existing product ID. A stale or hand-typed
// value would otherwise sit in the map and render a section on nothing.
$product_id = isset( $_POST['rh_tm_product'] ) ? absint( $_POST['rh_tm_product'] ) : 0;
if ( $product_id > 0 && 'product' !== get_post_type( $product_id ) ) {
$product_id = 0;
}
$scope = TestimonialType::sanitizeScope( isset( $_POST['rh_tm_scope'] ) ? sanitize_key( wp_unslash( $_POST['rh_tm_scope'] ) ) : '' );
/*
* «خریدار سایت» is re-derived only when the product changes. A
* moderator cannot tick it — it comes from the order table — and an
* unrelated edit must not drop a badge the customer earned when they
* submitted (the order may since have been archived).
*/
$previous_product = (int) get_post_meta( $post_id, '_rh_tm_product', true );
if ( $product_id !== $previous_product ) {
if ( $product_id && Purchases::bought( (int) $post->post_author, $product_id ) ) {
update_post_meta( $post_id, '_rh_tm_verified', 1 );
} else {
delete_post_meta( $post_id, '_rh_tm_verified' );
}
}
update_post_meta( $post_id, '_rh_tm_instagram', $instagram );
update_post_meta( $post_id, '_rh_tm_location', $location );
update_post_meta( $post_id, '_rh_tm_media_ids', $media_ids );
update_post_meta( $post_id, '_rh_tm_poster_id', $media_ids ? $poster_id : 0 );
update_post_meta( $post_id, '_rh_tm_pinned', $pinned );
// Absent means «everywhere» — the state of every testimonial saved
// before this setting existed, so the rail's exclusion stays a cheap
// NOT EXISTS / != compare.
if ( 'all' === $scope ) {
delete_post_meta( $post_id, '_rh_tm_scope' );
} else {
update_post_meta( $post_id, '_rh_tm_scope', $scope );
}
if ( ! empty( $_POST['rh_tm_via_instagram'] ) ) {
update_post_meta( $post_id, '_rh_tm_via_instagram', 1 );
} else {
delete_post_meta( $post_id, '_rh_tm_via_instagram' );
}
if ( $product_id ) {
update_post_meta( $post_id, '_rh_tm_product', $product_id );
} else {
// Deleted, not set to 0: the product map query uses EXISTS, so an
// unassigned testimonial must carry no row at all.
delete_post_meta( $post_id, '_rh_tm_product' );
}
// Drop legacy single-media meta so it can't shadow the new list.
delete_post_meta( $post_id, '_rh_tm_media_id' );
delete_post_meta( $post_id, '_rh_tm_media_type' );
}
}