Shopping Bag
No products in the cart.
'shianboon@hotmail.com', // who gets every new review (comma-separate for more) // Claude API key for AI captions. Best: put define( 'TYRELL_CLAUDE_KEY', 'sk-ant-...' ); in wp-config.php. // Leave both empty to skip AI captions (everything else still works). 'claude_key' => defined( 'TYRELL_CLAUDE_KEY' ) ? TYRELL_CLAUDE_KEY : '', 'claude_model' => 'claude-opus-5-5', 'max_photos' => 4, 'max_mb' => 10, // per photo // slug => name, Google review link. Online orders get a random branch's Google link. 'branches' => array( 'bukit-mertajam' => array( 'Bukit Mertajam', 'https://g.page/r/CQ7lktDgGnFgEBM/review', 'Penang' ), 'all-seasons' => array( 'All Seasons Place (Air Itam)', 'https://g.page/r/CawizdPBt1pkEBM/review', 'Penang' ), 'summerskye' => array( 'Summerskye Square (Bayan Lepas)', 'https://g.page/r/CcOPL_DcnDYREBM/review', 'Penang' ), 'sri-petaling' => array( 'Sri Petaling', 'https://g.page/r/CUOPmfjrLGfsEBM/review', 'Klang Valley' ), 'kota-damansara' => array( 'Kota Damansara', 'https://g.page/r/Ccuwly_e5seaEBM/review', 'Klang Valley' ), 'mahkota-cheras' => array( 'Bandar Mahkota Cheras', 'https://g.page/r/CZ_rgHMgsu-ZEBM/review', 'Klang Valley' ), 'klang-bbt2' => array( 'Klang Bandar Bukit Tinggi 2', 'https://g.page/r/Ca1a0XKKP4CXEBM/review', 'Klang Valley' ), ), ); } /* ============ END OF SETTINGS ============ */ /* ---------- Storage: private "Reviews" list in WP Admin ---------- */ add_action( 'init', function () { register_post_type( 'tyrell_review', array( 'labels' => array( 'name' => 'Customer Reviews', 'singular_name' => 'Customer Review', 'menu_name' => 'Reviews 好评' ), 'public' => false, 'show_ui' => true, 'show_in_menu' => true, 'menu_icon' => 'dashicons-star-filled', 'menu_position' => 26, 'supports' => array( 'title' ), 'capability_type' => 'post', 'capabilities' => array( 'create_posts' => 'do_not_allow' ), 'map_meta_cap' => true, ) ); } ); function tyrell_review_branch_name( $slug ) { $b = tyrell_review_settings()['branches']; return 'online' === $slug ? 'Online order 网站订单' : ( isset( $b[ $slug ] ) ? $b[ $slug ][0] : $slug ); } /* ---------- Form handler ---------- */ add_action( 'admin_post_nopriv_tyrell_review', 'tyrell_review_handle' ); add_action( 'admin_post_tyrell_review', 'tyrell_review_handle' ); function tyrell_review_fail( $back, $code ) { wp_safe_redirect( add_query_arg( 'err', $code, $back ) . '#tyrv-form' ); exit; } function tyrell_review_handle() { $s = tyrell_review_settings(); $back = isset( $_POST['back'] ) ? esc_url_raw( wp_unslash( $_POST['back'] ) ) : home_url( '/review/' ); $back = wp_validate_redirect( remove_query_arg( array( 'err', 'done' ), $back ), home_url( '/review/' ) ); // Spam guards: hidden field, signed timestamp (works with page caching, unlike nonces), rate limit. if ( ! empty( $_POST['website'] ) ) { tyrell_review_fail( $back, 'spam' ); } $ts = isset( $_POST['ts'] ) ? (int) $_POST['ts'] : 0; $sig = isset( $_POST['sig'] ) ? sanitize_text_field( wp_unslash( $_POST['sig'] ) ) : ''; if ( ! hash_equals( wp_hash( 'tyrv' . $ts ), $sig ) || time() - $ts < 3 || time() - $ts > DAY_IN_SECONDS ) { tyrell_review_fail( $back, 'expired' ); } $ip_key = 'tyrv_ip_' . md5( isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '' ); $hits = (int) get_transient( $ip_key ); if ( $hits >= 5 ) { tyrell_review_fail( $back, 'limit' ); } $branch = isset( $_POST['branch'] ) ? sanitize_key( $_POST['branch'] ) : ''; $rating = isset( $_POST['rating'] ) ? (int) $_POST['rating'] : 0; $text = isset( $_POST['review'] ) ? trim( sanitize_textarea_field( wp_unslash( $_POST['review'] ) ) ) : ''; $name = isset( $_POST['display_name'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['display_name'] ) ) ) : ''; $contact = isset( $_POST['contact'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['contact'] ) ) ) : ''; $c_mkt = ! empty( $_POST['consent_marketing'] ); $c_wa = ! empty( $_POST['consent_whatsapp'] ); if ( 'online' !== $branch && ! isset( $s['branches'][ $branch ] ) ) { tyrell_review_fail( $back, 'branch' ); } if ( $rating < 1 || $rating > 5 ) { tyrell_review_fail( $back, 'rating' ); } if ( mb_strlen( $text ) < 5 || mb_strlen( $text ) > 2000 ) { tyrell_review_fail( $back, 'text' ); } if ( '' === $name || mb_strlen( $name ) > 60 || mb_strlen( $contact ) < 5 || mb_strlen( $contact ) > 40 ) { tyrell_review_fail( $back, 'contact' ); } $post_id = wp_insert_post( array( 'post_type' => 'tyrell_review', 'post_status' => 'private', 'post_title' => sprintf( '%s · %s · %d★', $name, tyrell_review_branch_name( $branch ), $rating ), ), true ); if ( is_wp_error( $post_id ) ) { tyrell_review_fail( $back, 'save' ); } update_post_meta( $post_id, '_tyrv_branch', $branch ); update_post_meta( $post_id, '_tyrv_rating', $rating ); update_post_meta( $post_id, '_tyrv_text', $text ); update_post_meta( $post_id, '_tyrv_name', $name ); update_post_meta( $post_id, '_tyrv_contact', $contact ); update_post_meta( $post_id, '_tyrv_consent_marketing', $c_mkt ? 1 : 0 ); update_post_meta( $post_id, '_tyrv_consent_whatsapp', $c_wa ? 1 : 0 ); update_post_meta( $post_id, '_tyrv_status', $rating <= 3 ? 'follow-up' : 'new' ); $photos = tyrell_review_save_photos( $post_id, $s ); update_post_meta( $post_id, '_tyrv_photos', $photos ); if ( $rating >= 4 && $c_mkt && $s['claude_key'] ) { $ai = tyrell_review_ai_captions( $s, $branch, $rating, $text, $name, count( $photos ) ); if ( $ai ) { update_post_meta( $post_id, '_tyrv_ai', $ai ); } } tyrell_review_email( $post_id, $s ); set_transient( $ip_key, $hits + 1, HOUR_IN_SECONDS ); // Thank-you page data (the review text is shown back so the customer can paste it on Google). $token = wp_generate_password( 20, false ); set_transient( 'tyrv_done_' . $token, array( 'text' => $text, 'branch' => $branch ), 2 * HOUR_IN_SECONDS ); wp_safe_redirect( add_query_arg( 'done', $token, $back ) ); exit; } /* Photos: validate, strip EXIF/GPS by re-saving, add to Media Library attached to the review. */ function tyrell_review_save_photos( $post_id, $s ) { if ( empty( $_FILES['photos'] ) || ! is_array( $_FILES['photos']['name'] ) ) { return array(); } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; $f = $_FILES['photos']; $ids = array(); $ok = array( 'jpg|jpeg|jpe' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', 'heic' => 'image/heic' ); foreach ( $f['name'] as $i => $orig ) { if ( count( $ids ) >= $s['max_photos'] ) { break; } if ( UPLOAD_ERR_OK !== $f['error'][ $i ] || $f['size'][ $i ] > $s['max_mb'] * MB_IN_BYTES ) { continue; } $file = array( 'name' => sprintf( 'review-%d-%d.%s', $post_id, $i + 1, strtolower( pathinfo( $orig, PATHINFO_EXTENSION ) ) ), 'type' => $f['type'][ $i ], 'tmp_name' => $f['tmp_name'][ $i ], 'error' => 0, 'size' => $f['size'][ $i ], ); $up = wp_handle_upload( $file, array( 'test_form' => false, 'mimes' => $ok ) ); if ( empty( $up['file'] ) ) { continue; } // Re-encode the original: this drops all EXIF (GPS location, phone model, time). $ed = wp_get_image_editor( $up['file'] ); if ( ! is_wp_error( $ed ) ) { if ( method_exists( $ed, 'maybe_exif_rotate' ) ) { $ed->maybe_exif_rotate(); } $ed->resize( 2000, 2000, false ); $ed->set_quality( 88 ); $saved = $ed->save( $up['file'] ); if ( is_wp_error( $saved ) ) { wp_delete_file( $up['file'] ); continue; } } else { // Cannot re-encode (e.g. HEIC without Imagick): don't keep a file that may carry GPS. wp_delete_file( $up['file'] ); continue; } $att = wp_insert_attachment( array( 'post_mime_type' => $up['type'], 'post_title' => 'Customer review photo #' . $post_id, 'post_status' => 'inherit', ), $up['file'], $post_id ); if ( ! is_wp_error( $att ) && $att ) { add_filter( 'wp_image_maybe_exif_rotate', '__return_false' ); wp_update_attachment_metadata( $att, wp_generate_attachment_metadata( $att, $up['file'] ) ); remove_filter( 'wp_image_maybe_exif_rotate', '__return_false' ); update_post_meta( $att, '_tyrv_photo', 1 ); $ids[] = (int) $att; } } return $ids; } /* Optional AI captions through the Claude API (raw HTTP: no SDK inside a WPCode snippet). */ function tyrell_review_ai_captions( $s, $branch, $rating, $text, $name, $n_photos ) { $where = 'online' === $branch ? 'an online order on tyrelljewelry.com.my' : 'the ' . tyrell_review_branch_name( $branch ) . ' branch'; $prompt = "You write social media posts for Tyrell Jewelry, a Malaysian gold jewellery and gold bar retailer with 7 branches (3 in Penang, 4 in Klang Valley) and an online shop. " . "A real customer left this review. Turn it into marketing copy.\n\n" . "Customer display name: {$name}\nBought at: {$where}\nRating: {$rating}/5\nPhotos attached: {$n_photos}\nReview: \"{$text}\"\n\n" . "Rules: quote or closely paraphrase only what the customer actually said; never invent details, prices, products or claims. " . "Write in Malaysian Chinese mixed with common English words, warm and semi-casual, like the shop talks to its customers. " . "Never mention the customer's phone, address or anything that shows where they keep their gold.\n\n" . "Return exactly three sections with these headings:\n" . "FACEBOOK / INSTAGRAM: 4-7 lines with a few emoji and 4-6 hashtags including #TyrellJewelry\n" . "TIKTOK: one short line with 3 hashtags\n" . "WEBSITE: one-line testimonial in the form ★★★★★ \"quote\" — Name, Branch"; $res = wp_remote_post( 'https://api.anthropic.com/v1/messages', array( 'timeout' => 60, 'headers' => array( 'content-type' => 'application/json', 'x-api-key' => $s['claude_key'], 'anthropic-version' => '2026-10-21', 'anthropic-beta' => 'server-side-fallback-2026-07-01', ), 'body' => wp_json_encode( array( 'model' => $s['claude_model'], 'max_tokens' => 4000, 'output_config' => array( 'effort' => 'low' ), 'fallbacks' => 'default', 'messages' => array( array( 'role' => 'user', 'content' => $prompt ) ), ) ), ) ); if ( is_wp_error( $res ) || 200 !== wp_remote_retrieve_response_code( $res ) ) { return ''; } $body = json_decode( wp_remote_retrieve_body( $res ), true ); if ( empty( $body['content'] ) || ( isset( $body['stop_reason'] ) && 'refusal' === $body['stop_reason'] ) ) { return ''; } $out = ''; foreach ( $body['content'] as $block ) { if ( isset( $block['type'], $block['text'] ) && 'text' === $block['type'] ) { $out .= $block['text']; } } return trim( $out ); } /* Email to the owner */ function tyrell_review_email( $post_id, $s ) { $m = function ( $k ) use ( $post_id ) { return get_post_meta( $post_id, '_tyrv_' . $k, true ); }; $rating = (int) $m( 'rating' ); $stars = str_repeat( '★', $rating ) . str_repeat( '☆', 5 - $rating ); $branch = tyrell_review_branch_name( $m( 'branch' ) ); $subj = ( $rating <= 3 ? '⚠ 低分 请跟进 | ' : '' ) . "New review {$stars} {$branch} · " . $m( 'name' ); $h = '
Low rating: please contact the customer. No captions are made for this review.
'; } $h .= '' . nl2br( esc_html( $m( 'text' ) ) ) . '
'; $h .= '| Name 名字 | ' . esc_html( $m( 'name' ) ) . ' |
| Phone / order | ' . esc_html( $m( 'contact' ) ) . ' |
| Marketing consent | ' . ( $m( 'consent_marketing' ) ? '✅ Yes, can post' : '❌ No, do NOT post' ) . ' |
| WhatsApp promos | ' . ( $m( 'consent_whatsapp' ) ? 'Yes' : 'No' ) . ' |
Photos (' . count( $photos ) . ')
';
foreach ( $photos as $att ) {
$thumb = wp_get_attachment_image_url( $att, 'medium' );
$full = wp_get_attachment_url( $att );
$h .= '';
}
$h .= '
' . esc_html( $ai ) . ''; } $h .= '
' . esc_html( str_repeat( '★', (int) $m( 'rating' ) ) ) . '
'; echo '' . esc_html( tyrell_review_branch_name( $m( 'branch' ) ) ) . ' · ' . esc_html( $m( 'name' ) ) . ' · ' . esc_html( $m( 'contact' ) ) . '
'; echo '' . nl2br( esc_html( $m( 'text' ) ) ) . ''; echo '
Marketing consent: ' . ( $m( 'consent_marketing' ) ? '✅ yes' : '❌ no, do not post' ) . ' · WhatsApp promos: ' . ( $m( 'consent_whatsapp' ) ? 'yes' : 'no' ) . '
'; foreach ( (array) $m( 'photos' ) as $att ) { echo '' . wp_get_attachment_image( $att, 'thumbnail', false, array( 'style' => 'margin:4px;border-radius:6px' ) ) . ''; } echo ''; echo ''; } add_action( 'save_post_tyrell_review', function ( $id ) { if ( ! isset( $_POST['tyrv_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['tyrv_nonce'] ) ), 'tyrv_save' ) || ! current_user_can( 'edit_post', $id ) ) { return; } if ( isset( $_POST['tyrv_ai'] ) ) { update_post_meta( $id, '_tyrv_ai', sanitize_textarea_field( wp_unslash( $_POST['tyrv_ai'] ) ) ); } if ( isset( $_POST['tyrv_status'] ) ) { update_post_meta( $id, '_tyrv_status', sanitize_key( $_POST['tyrv_status'] ) ); } } ); /* Deleting a review also deletes its photos */ add_action( 'before_delete_post', function ( $id ) { if ( 'tyrell_review' === get_post_type( $id ) ) { foreach ( (array) get_post_meta( $id, '_tyrv_photos', true ) as $att ) { wp_delete_attachment( (int) $att, true ); } } } ); /* ---------- Front end ---------- */ add_shortcode( 'tyrell_review', function () { $s = tyrell_review_settings(); ob_start(); echo ''; $done = isset( $_GET['done'] ) ? sanitize_key( $_GET['done'] ) : ''; $data = $done ? get_transient( 'tyrv_done_' . $done ) : false; if ( $data ) { $slug = $data['branch']; if ( ! isset( $s['branches'][ $slug ] ) ) { $keys = array_keys( $s['branches'] ); $slug = $keys[ array_rand( $keys ) ]; // online order: random branch (Boon, 2026-11-03) } $g = $s['branches'][ $slug ][1]; ?>Thank you for your review
可以帮我们也在 Google 留言吗?
Would you share it on Google too? We copy your review for you: open Google, long-press → Paste, pick the stars and post. About 10 seconds.
分享您的购买体验 · Share your experience with us
Showing 1–12 of 21 resultsSorted by latest