фунгк

<?php

/*

Plugin Name: SpiritTrade Digital Modules

Description: Единый плагин для формы постов, карты базы знаний, древа категорий, Summer Menu API и AL-Тиндер.

Version: 3.1.1

Author: Spirit Trade

*/

 

// ==============================================================================

// 0. ГЛОБАЛЬНЫЕ КОНСТАНТЫ И НАСТРОЙКИ

// ==============================================================================

define( ‘ST_TG_BOT_TOKEN’, ‘7675966976:AAFmAeowI9eGA5siDvsUy9kPr_SHZIOBI38’ );

define( ‘ST_TG_CHAT_ID’, ‘-1002640050542’ );

define( ‘ST_ADMIN_PIN’, ’16’ );

 

// ==============================================================================

// 1. ИНИЦИАЛИЗАЦИЯ БАЗЫ ДАННЫХ И ПОДКЛЮЧЕНИЕ СКРИПТОВ

// ==============================================================================

 

add_action( ‘wp_enqueue_scripts’, ‘st_enqueue_global_assets’ );

function st_enqueue_global_assets() {

    if ( ! is_admin() ) {

        wp_enqueue_style( ‘dashicons’ );

        wp_enqueue_style( ‘editor-buttons’ );

        wp_enqueue_style( ‘tinymce-css’, includes_url( ‘css/editor.min.css’ ) );

        wp_enqueue_script( ‘jquery’ );

        

        // Подключаем тяжелые скрипты редактора только если они нужны

        if ( shortcode_exists( ‘frontend_post_form’ ) || shortcode_exists( ‘al_profile_setup’ ) ) {

            wp_enqueue_editor();

            wp_enqueue_media();

            wp_enqueue_script( ‘word-count’ );

            wp_enqueue_script( ‘editor’ );

            wp_enqueue_script( ‘quicktags’ );

        }

    }

}

 

add_action( ‘plugins_loaded’, ‘st_global_install_tables’ );

function st_global_install_tables() {

    if ( get_option( ‘st_global_tables_installed_v3’ ) ) {

        return;

    }

 

    global $wpdb;

    $charset_collate = $wpdb->get_charset_collate();

    $prefix = $wpdb->prefix;

 

    require_once( ABSPATH . ‘wp-admin/includes/upgrade.php’ );

 

    $tables = [

        «CREATE TABLE IF NOT EXISTS {$prefix}st_al_user_profile (

            user_id BIGINT UNSIGNED NOT NULL, nickname VARCHAR(100) DEFAULT », age INT DEFAULT NULL, engagement_level VARCHAR(50) DEFAULT », bio TEXT, platform_goal VARCHAR(100) DEFAULT », languages LONGTEXT, current_vibe_id INT DEFAULT NULL, vibe_updated_at DATETIME DEFAULT NULL, PRIMARY KEY (user_id), FOREIGN KEY (user_id) REFERENCES {$prefix}users(ID) ON DELETE CASCADE

        ) $charset_collate;»,

        «CREATE TABLE IF NOT EXISTS {$prefix}st_al_user_locations (

            user_id BIGINT UNSIGNED NOT NULL, last_coordinates POINT DEFAULT NULL, habitat_zones LONGTEXT, saved_venues LONGTEXT, PRIMARY KEY (user_id), FOREIGN KEY (user_id) REFERENCES {$prefix}users(ID) ON DELETE CASCADE

        ) $charset_collate;»,

        «CREATE TABLE IF NOT EXISTS {$prefix}st_al_user_beverage_preferences (

            user_id BIGINT UNSIGNED NOT NULL, spirits LONGTEXT, wine LONGTEXT, beer_cider LONGTEXT, fermented LONGTEXT, PRIMARY KEY (user_id), FOREIGN KEY (user_id) REFERENCES {$prefix}users(ID) ON DELETE CASCADE

        ) $charset_collate;»,

        «CREATE TABLE IF NOT EXISTS {$prefix}st_al_user_lifestyle (

            user_id BIGINT UNSIGNED NOT NULL, favorite_time VARCHAR(50) DEFAULT », occupation VARCHAR(100) DEFAULT », event_format VARCHAR(50) DEFAULT », outing_frequency VARCHAR(50) DEFAULT », venue_choice VARCHAR(50) DEFAULT », company_pref VARCHAR(50) DEFAULT », venue_atmosphere VARCHAR(50) DEFAULT », comm_style VARCHAR(50) DEFAULT », rating_style VARCHAR(50) DEFAULT », explorer_type VARCHAR(50) DEFAULT », PRIMARY KEY (user_id), FOREIGN KEY (user_id) REFERENCES {$prefix}users(ID) ON DELETE CASCADE

        ) $charset_collate;»,

        «CREATE TABLE IF NOT EXISTS {$prefix}st_al_user_media_prompts (

            user_id BIGINT UNSIGNED NOT NULL, photos LONGTEXT, interest_tags LONGTEXT, text_prompts LONGTEXT, spotify_anthem VARCHAR(255) DEFAULT », social_links LONGTEXT, PRIMARY KEY (user_id), FOREIGN KEY (user_id) REFERENCES {$prefix}users(ID) ON DELETE CASCADE

        ) $charset_collate;»,

        «CREATE TABLE IF NOT EXISTS {$prefix}st_al_vibes_dictionary (

            id INT UNSIGNED NOT NULL AUTO_INCREMENT, category VARCHAR(50) NOT NULL, code VARCHAR(10) NOT NULL, name VARCHAR(100) NOT NULL, emoji VARCHAR(10) NOT NULL, description TEXT, PRIMARY KEY (id), UNIQUE KEY code (code)

        ) $charset_collate;»,

        «CREATE TABLE IF NOT EXISTS {$prefix}st_al_user_vibe_history (

            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, vibe_id INT UNSIGNED NOT NULL, set_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id), FOREIGN KEY (user_id) REFERENCES {$prefix}users(ID) ON DELETE CASCADE, FOREIGN KEY (vibe_id) REFERENCES {$prefix}st_al_vibes_dictionary(id) ON DELETE RESTRICT

        ) $charset_collate;»,

        «CREATE TABLE IF NOT EXISTS {$prefix}st_al_vibe_recommendation_rules (

            vibe_id INT UNSIGNED NOT NULL, target_beverage_category LONGTEXT, target_venue_atmosphere LONGTEXT, alternative_options LONGTEXT, PRIMARY KEY (vibe_id), FOREIGN KEY (vibe_id) REFERENCES {$prefix}st_al_vibes_dictionary(id) ON DELETE CASCADE

        ) $charset_collate;»,

        // Кастомная таблица для данных Summer Menu (изолирует wp_options)

        «CREATE TABLE IF NOT EXISTS {$prefix}st_sm_user_data (

            phone VARCHAR(50) NOT NULL, checks LONGTEXT, completed LONGTEXT, notes LONGTEXT, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (phone)

        ) $charset_collate;»

    ];

 

    foreach ($tables as $sql) {

        dbDelta( $sql );

    }

 

    if ( ! $wpdb->get_var( «SELECT COUNT(*) FROM {$prefix}st_al_vibes_dictionary» ) ) {

        st_al_seed_vibes( «{$prefix}st_al_vibes_dictionary» );

    }

 

    update_option( ‘st_global_tables_installed_v3’, 1 );

}

 

// ==============================================================================

// 2. ФОРМА ПУБЛИКАЦИИ:

Доступ закрыт

Для публикации новых заметок в базу знаний платформы необходимо авторизоваться.

// ==============================================================================

add_shortcode(‘frontend_post_form’, ‘render_comic_frontend_form’);

 

function render_comic_frontend_form() {

    ob_start();

if ( ! is_user_logged_in() ) {

        return st_get_auth_prompt_html(‘Для публикации новых заметок в базу знаний платформы необходимо авторизоваться.’);

    }

    ?>

    <style>

        .st-modal-container {

            —bg-paper: #fdfcf8;

            —ink-black: #1a1a1a;

            —marker-yellow: #ffde59;

            —marker-green: #c1ff00;

            —marker-red: #ff5757;

            —border-sketch: 2px solid var(—ink-black);

            —shadow-sketch: 3px 3px 0px var(—ink-black);

            —transition: 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);

        }

 

        .st-modal-container .st-modal-trigger-btn {

            background: var(—marker-yellow); color: var(—ink-black); border: var(—border-sketch); box-shadow: var(—shadow-sketch); padding: 12px 24px; font-family: ‘Georgia’, cursive, Georgia; font-size: 1.1rem; font-weight: 900; text-transform: uppercase; border-radius: 8px 2px 10px 4px; cursor: pointer; transition: var(—transition); display: inline-flex; align-items: center; gap: 8px;

        }

        .st-modal-container .st-modal-trigger-btn:hover { transform: translateY(-2px); box-shadow: 5px 5px 0px var(—ink-black); background: #ffea8c; }

        .st-modal-container .st-modal-trigger-btn:active { transform: translateY(2px); box-shadow: 0px 0px 0px var(—ink-black); }

 

        .st-modal-container .st-modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(26, 26, 26, 0.8); backdrop-filter: blur(5px); z-index: 99999; justify-content: center; align-items: center; opacity: 0; transition: opacity 0.3s ease; }

        .st-modal-container .st-modal-overlay.active { display: flex; opacity: 1; }

 

        .st-modal-container .st-modal-content { position: relative; width: 100%; max-width: 800px; max-height: 95vh; overflow-y: auto; transform: translateY(40px) scale(0.95); transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); padding: 10px; box-sizing: border-box; }

        .st-modal-container .st-modal-overlay.active .st-modal-content { transform: translateY(0) scale(1); }

        .st-modal-container .st-modal-content::-webkit-scrollbar { width: 0px; }

 

        .st-modal-container .st-modal-close-btn { position: absolute; top: 15px; right: 15px; background: var(—marker-red); color: #fff; border: var(—border-sketch); border-radius: 50%; width: 36px; height: 36px; font-size: 1.5rem; line-height: 1; cursor: pointer; z-index: 99; box-shadow: 2px 2px 0px var(—ink-black); transition: var(—transition); display: flex; align-items: center; justify-content: center; }

        .st-modal-container .st-modal-close-btn:hover { transform: scale(1.1) rotate(90deg); background: #ff3333; }

 

        .st-modal-container .comic-form-wrapper { background: var(—bg-paper); padding: 20px 25px; border: var(—border-sketch); border-radius: 2px 20px 5px 15px; box-shadow: 5px 5px 0px var(—ink-black); font-family: Georgia, ‘Times New Roman’, Times, serif; position: relative; color: var(—ink-black); box-sizing: border-box; width: 100%; }

        .st-modal-container .comic-form-wrapper::before { content: «»; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-image: radial-gradient(var(—ink-black) 1px, transparent 1px); background-size: 15px 15px; opacity: 0.05; z-index: 0; pointer-events: none; }

        .st-modal-container .comic-form-content { position: relative; z-index: 1; }

 

        .st-modal-container .comic-form-title { font-family: ‘Georgia’, cursive, Georgia; font-size: 1.8rem; text-transform: uppercase; text-align: center; margin-top: 0; margin-bottom: 20px; transform: rotate(-1.5deg); }

        .st-modal-container .comic-form-title span { background: var(—marker-yellow); padding: 0 8px; border: 2px solid var(—ink-black); box-shadow: 2px 2px 0 var(—ink-black); }

 

        .st-modal-container .st-field { margin-bottom: 15px; width: 100%; box-sizing: border-box; }

        .st-modal-container .st-label { display: inline-block; margin-bottom: 5px; font-family: ‘Georgia’, cursive, Georgia; font-weight: 900; font-size: 0.85rem; letter-spacing: 0.5px; text-transform: uppercase; background: #fff; padding: 2px 6px; border: 1px solid var(—ink-black); border-radius: 8px 4px 8px 2px; transform: rotate(-1deg); }

        .st-modal-container .st-input { width: 100%; padding: 10px 15px; border: var(—border-sketch); border-radius: 4px 10px 4px 10px; font-family: Georgia, ‘Times New Roman’, Times, serif; font-size: 1rem; line-height: 1.4; background: #fff; box-shadow: 2px 2px 0px rgba(0,0,0,0.1); transition: var(—transition); box-sizing: border-box; outline: none; }

        .st-modal-container .st-input:focus { background: #fffbea; box-shadow: var(—shadow-sketch); transform: translateY(-2px); }

 

        .st-modal-container .st-cat-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 5px; width: 100%; }

        .st-modal-container .st-cat-btn { background: #fff; border: 1px solid var(—ink-black); color: var(—ink-black); padding: 8px 10px; border-radius: 6px; font-family: ‘Georgia’, cursive, Georgia; font-size: 0.85rem; font-weight: bold; cursor: pointer; transition: 0.2s; width: 100%; box-sizing: border-box; text-align: center; }

        .st-modal-container .st-cat-btn:hover { background: #f5f5f5; }

        .st-modal-container .st-cat-btn.active { background: var(—marker-green); box-shadow: 2px 2px 0 var(—ink-black); transform: translateY(-2px); border-color: var(—ink-black); }

 

        .st-modal-container .st-pin-wrapper { text-align: left; margin-top: 15px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; }

        .st-modal-container .st-pin-label { font-family: ‘Georgia’, cursive, Georgia; font-weight: 900; font-size: 0.85rem; text-transform: uppercase; background: #fff; padding: 2px 6px; border: 1px solid var(—ink-black); border-radius: 8px 4px 8px 2px; transform: rotate(-1deg); white-space: nowrap; }

        .st-modal-container .st-pin-display { font-size: 1.5rem; letter-spacing: 8px; font-weight: 900; background: #fff; border: 1px solid var(—ink-black); padding: 2px 10px; border-radius: 8px; min-width: 100px; text-align: center; font-family: monospace; }

        .st-modal-container .st-pin-row { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; width: 100%; margin-top: 10px; }

        .st-modal-container .st-pin-btn { width: 100%; height: 45px; border: var(—border-sketch); background: #fff; border-radius: 8px; font-size: 1.2rem; font-weight: 900; color: var(—ink-black); cursor: pointer; box-shadow: 2px 2px 0 var(—ink-black); transition: 0.1s; min-width: 40px; }

        .st-modal-container .st-pin-btn:active { transform: translateY(3px); box-shadow: 0 0 0 var(—ink-black); background: #f0f0f0; }

 

        .st-modal-container .wp-editor-wrap { border: var(—border-sketch) !important; border-radius: 2px 10px 2px 10px; box-shadow: var(—shadow-sketch); background: #fff; overflow: hidden; width: 100%; box-sizing: border-box;}

        .st-modal-container .wp-editor-wrap .wp-editor-container { border: none !important; }

        .st-modal-container .wp-editor-wrap .wp-editor-tools { background: #f5f5f5; border-bottom: 1px solid var(—ink-black); padding: 3px 8px; }

        .st-modal-container .wp-editor-wrap .wp-editor-tabs button { font-family: Georgia, ‘Times New Roman’, Times, serif; border-radius: 4px 4px 0 0; font-size: 0.85rem; padding: 3px 8px; }

 

        .st-modal-container .st-btn { background: var(—marker-green); color: var(—ink-black); border: var(—border-sketch); box-shadow: var(—shadow-sketch); padding: 12px 20px; font-family: ‘Georgia’, cursive, Georgia; font-size: 1.2rem; font-weight: 900; text-transform: uppercase; border-radius: 8px 2px 10px 4px; cursor: pointer; width: 100%; transition: var(—transition); display: flex; justify-content: center; align-items: center; margin-top: 20px; }

        .st-modal-container .st-btn:hover { background: #aae000; transform: translateY(-2px) rotate(1deg); box-shadow: 4px 4px 0px var(—ink-black); }

        .st-modal-container .st-btn:active { transform: translateY(2px); box-shadow: 0px 0px 0px var(—ink-black); }

        .st-modal-container .st-btn.loading { background: #ddd; pointer-events: none; box-shadow: none; transform: translateY(2px); }

 

        .st-modal-container #st_response { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0) rotate(-5deg); z-index: 999999; width: 90%; max-width: 350px; padding: 25px; border: var(—border-sketch); border-radius: 20px; font-family: Georgia, serif; font-size: 1.2rem; font-weight: 900; text-align: center; box-shadow: 8px 8px 0px var(—ink-black); display: none; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); box-sizing: border-box; }

        .st-modal-container #st_response.show { display: block; transform: translate(-50%, -50%) scale(1) rotate(0deg); }

        .st-modal-container #st_response.success { background: var(—marker-green); color: var(—ink-black); }

        .st-modal-container #st_response.error { background: var(—marker-red); color: #fff; }

        

        .st-modal-container .st-success-actions { display: flex; gap: 10px; margin-top: 20px; justify-content: center; align-items: stretch; width: 100%; }

        .st-modal-container .st-success-journal { flex: 1; background: #1a1a1a; color: #ffde59; text-decoration: none; padding: 12px; border-radius: 12px; font-size: 1rem; font-weight: bold; font-family: sans-serif; border: 2px solid #1a1a1a; display: flex; align-items: center; justify-content: center; box-shadow: 3px 3px 0px rgba(0,0,0,0.3); transition: 0.2s; text-transform: uppercase; }

        .st-modal-container .st-success-journal:hover { background: #fff; color: #1a1a1a; transform: translateY(-2px); box-shadow: 4px 4px 0px rgba(0,0,0,0.4); }

        

        .st-modal-container .st-success-close { background: var(—marker-red); color: white; border: 2px solid #1a1a1a; border-radius: 12px; width: 50px; font-size: 1.5rem; cursor: pointer; box-shadow: 3px 3px 0px rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; transition: 0.2s; }

        .st-modal-container .st-success-close:hover { background: #ff3333; transform: scale(1.05) translateY(-2px); }

        .st-modal-container .success-boom { animation: boom 0.5s ease-out; }

        @keyframes boom { 0% { transform: translate(-50%, -50%) scale(0.8); } 50% { transform: translate(-50%, -50%) scale(1.1); } 100% { transform: translate(-50%, -50%) scale(1); } }

        

        @media (max-width: 600px) { 

            .st-modal-container .comic-form-wrapper { padding: 20px 15px; }

            .st-modal-container .st-pin-row { gap: 5px; }

            .st-modal-container .st-pin-btn { height: 40px; font-size: 1rem; min-width: 35px; }

            .st-modal-container .st-modal-close-btn { top: 5px; right: 5px; }

            .st-modal-container .st-pin-wrapper { flex-direction: column; align-items: stretch; }

            .st-modal-container .st-pin-label, .st-modal-container .st-pin-display { text-align: center; }

        }

    </style>

 

    <div class=»st-modal-container» id=»st-post-form-wrap»>

        <button id=»st-open-form-btn» class=»st-modal-trigger-btn»>✏️ Добавить заметку</button>

        <div id=»st_response»></div>

 

        <div id=»st-form-modal» class=»st-modal-overlay»>

            <div class=»st-modal-content»>

                <button id=»st-close-form-btn» class=»st-modal-close-btn»>×</button>

                

                <div class=»comic-form-wrapper»>

                    <div class=»comic-form-content»>

                        <h2 class=»comic-form-title»><span> + запись</span></h2>

                        

                        <form id=»advanced-post-form» method=»POST»>

                            <div class=»st-field»>

                                <label class=»st-label»>Заголовок</label>

                                <input type=»text» name=»st_title» id=»st_title» class=»st-input» placeholder=»О чем напишем?…» required>

                            </div>

 

                            <div class=»st-field»>

                                <div class=»st-cat-buttons»>

                                    <?php

                                    $categories = get_categories(array(‘hide_empty’ => 0));

                                    foreach($categories as $category) {

                                        echo ‘<button type=»button» class=»st-cat-btn» data-id=»‘ . esc_attr($category->term_id) . ‘»>’ . esc_html($category->name) . ‘</button>’;

                                    }

                                    ?>

                                </div>

                                <input type=»hidden» name=»st_category» id=»st_category» value=»0″>

                            </div>

 

                            <div class=»st-field»>

                                <div id=»editor-container»>

                                    <?php

                                    $editor_settings = array(

                                        ‘textarea_name’ => ‘st_description’,

                                        ‘textarea_rows’ => 8,

                                        ‘media_buttons’ => true,

                                        ‘quicktags’     => true,

                                        ‘dfw’           => true, 

                                        ‘tinymce’       => array(

                                            ‘toolbar1’ => ‘bold,italic,link,wp_adv’,

                                            ‘toolbar2’ => ‘formatselect,strikethrough,forecolor,bullist,numlist,blockquote,alignleft,aligncenter,alignright,unlink,removeformat,fullscreen’,

                                            ‘toolbar3’ => »,

                                            ‘toolbar4’ => »,

                                            ‘fontsize_formats’ => ’10px,12px,14px,16px,18px,20px,24px’,

                                            ‘content_css’ => false,

                                            ‘content_style’ => ‘body { font-family: Georgia, «Times New Roman», Times, serif; font-size: 16px; line-height: 1.5; color: #1a1a1a; }’,

                                        ),

                                        ‘editor_height’ => 150,

                                        ‘editor_class’ => ‘st-editor-content’,

                                        ‘drag_drop_upload’ => true,

                                    );

                                    wp_editor(», ‘st_description’, $editor_settings);

                                    ?>

                                </div>

                            </div>

 

                            <div class=»st-field st-pin-wrapper»>

                                <span class=»st-pin-label»>ПИН-КОД</span>

                                <div class=»st-pin-display» id=»pin-display»>- -</div>

                            </div>

                            <div class=»st-pin-row»>

                                <button type=»button» class=»st-pin-btn» data-val=»1″>1</button>

                                <button type=»button» class=»st-pin-btn» data-val=»3″>3</button>

                                <button type=»button» class=»st-pin-btn» data-val=»4″>4</button>

                                <button type=»button» class=»st-pin-btn» data-val=»6″>6</button>

                                <button type=»button» class=»st-pin-btn» data-val=»7″>7</button>

                                <button type=»button» class=»st-pin-btn» data-val=»9″>9</button>

                            </div>

                            <input type=»hidden» name=»st_admin_pass» id=»st_admin_pass» value=»»>

 

                            <?php wp_nonce_field(‘st_adv_nonce_action’, ‘st_adv_nonce’); ?>

                            <button type=»submit» class=»st-btn» id=»st_submit_btn»>Опубликовать!</button>

                        </form>

                    </div>

                </div>

            </div>

        </div>

    </div>

 

    <script>

    (function($) {

        const wrap = document.getElementById(‘st-post-form-wrap’);

        const btnOpen = wrap.querySelector(‘#st-open-form-btn’);

        const btnClose = wrap.querySelector(‘#st-close-form-btn’);

        const modal = wrap.querySelector(‘#st-form-modal’);

 

        btnOpen.addEventListener(‘click’, function(e) {

            e.preventDefault();

            modal.style.display = ‘flex’;

            setTimeout(() => modal.classList.add(‘active’), 10);

            if (typeof tinyMCE !== ‘undefined’ && tinyMCE.get(‘st_description’)) {

                tinyMCE.execCommand(‘mceRepaint’);

            }

        });

 

        function closeModal() {

            modal.classList.remove(‘active’);

            setTimeout(() => modal.style.display = ‘none’, 300);

        }

 

        btnClose.addEventListener(‘click’, closeModal);

        modal.addEventListener(‘click’, function(e) { if (e.target === modal) closeModal(); });

        document.addEventListener(‘keydown’, function(e) { if (e.key === ‘Escape’ && modal.classList.contains(‘active’)) closeModal(); });

 

        $(wrap).find(‘.st-cat-btn’).on(‘click’, function() {

            $(wrap).find(‘.st-cat-btn’).removeClass(‘active’);

            $(this).addClass(‘active’);

            $(wrap).find(‘#st_category’).val($(this).data(‘id’));

        });

 

        let currentPin = «»;

        $(wrap).find(‘.st-pin-btn’).on(‘click’, function() {

            if (currentPin.length >= 2) currentPin = «»;

            currentPin += $(this).data(‘val’);

            $(wrap).find(‘#st_admin_pass’).val(currentPin);

            

            if (currentPin.length === 1) $(wrap).find(‘#pin-display’).text(‘* -‘);

            else if (currentPin.length === 2) $(wrap).find(‘#pin-display’).text(‘* *’);

            else $(wrap).find(‘#pin-display’).text(‘- -‘);

        });

 

        function initEditorSave() {

            if (typeof tinyMCE !== ‘undefined’) {

                if ($(‘#st_description’).length && !tinyMCE.get(‘st_description’)) {

                    tinyMCE.init({

                        selector: ‘#st_description’, height: 150, menubar: false,

                        plugins: ‘lists,link,wordpress,textcolor,fullscreen’,

                        toolbar1: ‘bold,italic,link,wp_adv’,

                        toolbar2: ‘formatselect,strikethrough,forecolor,bullist,numlist,blockquote,alignleft,aligncenter,alignright,unlink,removeformat,fullscreen’,

                        branding: false, content_css: false,

                        content_style: ‘body { font-family: Georgia, «Times New Roman», Times, serif; font-size: 16px; line-height: 1.5; color: #1a1a1a; background: white; padding: 10px; }’

                    });

                }

            }

        }

        $(document).ready(function() { setTimeout(initEditorSave, 500); });

 

        $(wrap).find(‘#advanced-post-form’).on(‘submit’, function(e) {

            e.preventDefault();

            const btn = $(wrap).find(‘#st_submit_btn’);

            const responseDiv = $(wrap).find(‘#st_response’);

            

            if (typeof tinyMCE !== ‘undefined’) {

                const editor = tinyMCE.get(‘st_description’);

                if (editor) editor.save();

            }

            

            if (!$(wrap).find(‘#st_title’).val().trim()) {

                showError(responseDiv, ‘Заполните заголовок!’); return;

            }

 

            if ($(wrap).find(‘#st_category’).val() === «0») {

                showError(responseDiv, ‘Выберите категорию!’); return;

            }

            

            btn.addClass(‘loading’).text(‘✍️ Рисуем…’);

            responseDiv.removeClass(‘show’);

            

            const formData = new FormData(this);

            formData.append(‘action’, ‘process_advanced_post’);

            

            $.ajax({

                url: ‘<?php echo admin_url(‘admin-ajax.php’); ?>’, type: ‘POST’,

                data: formData, processData: false, contentType: false, dataType: ‘json’,

                success: function(res) {

                    btn.removeClass(‘loading’).text(‘Опубликовать!’);

                    responseDiv.removeClass(‘success error show’);

                    

                    if (res.success) {

                        responseDiv.addClass(‘success show success-boom’);

                        

                        let successHtml = `<span>WOW! 💥</span><br>${res.data.message}`;

                        successHtml += `

                            <div class=»st-success-actions»>

                                <a href=»https://i.spirittrade.kz/info/» class=»st-success-journal»>📖 Журнал</a>

                                <button type=»button» class=»st-success-close» id=»close-success-btn»>✕</button>

                            </div>

                        `;

                        responseDiv.html(successHtml);

                        

                        $(wrap).find(‘#st_title’).val(»); $(wrap).find(‘#st_category’).val(‘0’); $(wrap).find(‘.st-cat-btn’).removeClass(‘active’);

                        $(wrap).find(‘#st_admin_pass’).val(»); currentPin = «»; $(wrap).find(‘#pin-display’).text(‘- -‘);

                        if (typeof tinyMCE !== ‘undefined’) { const editor = tinyMCE.get(‘st_description’); if (editor) editor.setContent(»); }

                        $(wrap).find(‘#st_description’).val(»);

                        

                    } else {

                        showError(responseDiv, res.data.message);

                    }

                },

                error: function() {

                    btn.removeClass(‘loading’).text(‘Опубликовать!’);

                    showError(responseDiv, ‘Ошибка сети или сервера.’);

                }

            });

        });

 

        function showError(element, text) {

            element.removeClass(‘success show’).addClass(‘error show’).html(`<span>⚠️ Ошибка…</span><br>${text}`);

            setTimeout(() => element.removeClass(‘show’), 3000);

        }

        

        $(document).on(‘click’, ‘#close-success-btn’, function(e) {

            e.preventDefault();

            $(wrap).find(‘#st_response’).removeClass(‘show’);

            closeModal();

        });

 

    })(jQuery);

    </script>

    <?php

    return ob_get_clean();

}

 

add_action(‘wp_ajax_nopriv_process_advanced_post’, ‘st_process_advanced_post’);

add_action(‘wp_ajax_process_advanced_post’, ‘st_process_advanced_post’);

 

function st_process_advanced_post() {

    if ( ! isset( $_POST[‘st_adv_nonce’] ) || ! wp_verify_nonce( $_POST[‘st_adv_nonce’], ‘st_adv_nonce_action’ ) ) {

        wp_send_json_error( array( ‘message’ => ‘Ошибка безопасности. Обновите страницу.’ ) );

    }

 

    $pin = isset( $_POST[‘st_admin_pass’] ) ? sanitize_text_field( $_POST[‘st_admin_pass’] ) : »;

    if ( $pin !== ST_ADMIN_PIN ) {

        wp_send_json_error( array( ‘message’ => ‘Неверный ПИН-код.’ ) );

    }

 

    $title       = sanitize_text_field( $_POST[‘st_title’] );

    $content     = wp_kses_post( $_POST[‘st_description’] );

    $category_id = intval( $_POST[‘st_category’] );

 

    if ( empty( $title ) ) {

        wp_send_json_error( array( ‘message’ => ‘Заполните заголовок.’ ) );

    }

    if ( $category_id === 0 ) {

        wp_send_json_error( array( ‘message’ => ‘Выберите категорию.’ ) );

    }

 

    $post_data = array(

        ‘post_title’    => $title,

        ‘post_content’  => $content,

        ‘post_status’   => ‘publish’,

        ‘post_type’     => ‘post’,

        ‘post_category’ => array( $category_id ),

    );

 

    $post_id = wp_insert_post( $post_data, true );

 

    if ( is_wp_error( $post_id ) ) {

        wp_send_json_error( array( ‘message’ => ‘Ошибка создания записи: ‘ . $post_id->get_error_message() ) );

    } else {

        st_send_telegram_notification( $post_id, $title, $category_id );

        wp_send_json_success( array( ‘message’ => ‘✅ Запись опубликована! ID: ‘ . $post_id ) );

    }

}

 

function st_send_telegram_notification( $post_id, $title, $category_id ) {

    $category_name = ‘Без категории’;

    $cat = get_term( $category_id, ‘category’ );

    if ( $cat && ! is_wp_error( $cat ) ) {

        $category_name = $cat->name;

    }

    

    $post_url = get_permalink( $post_id );

    

    $message = «📢 *Spirit Trade Distribution | Новая запись*\n\n»;

    $message .= «📌 *{$title}*\n»;

    $message .= «🗂 Категория: {$category_name}\n»;

    $message .= «🔗 [Читать запись]({$post_url})\n»;

    $message .= «🕒 » . current_time( ‘d.m.Y H:i’ );

    

    $url = «https://api.telegram.org/bot» . ST_TG_BOT_TOKEN . «/sendMessage»;

    $args = array(

        ‘body’ => array(

            ‘chat_id’                  => ST_TG_CHAT_ID,

            ‘text’                     => $message,

            ‘parse_mode’               => ‘Markdown’,

            ‘disable_web_page_preview’ => false,

        ),

        ‘timeout’ => 5,

    );

    

    $response = wp_remote_post( $url, $args );

    if ( is_wp_error( $response ) ) {

        error_log( ‘Telegram notification error: ‘ . $response->get_error_message() );

    }

}

 

 

// ==============================================================================

// 3. БАЗА ЗНАНИЙ:

Описание

Загрузка…

0 записей по теме
Синхронизация данных…

// ==============================================================================

 

add_shortcode(‘knowledge_base_map’, ‘st_render_knowledge_base_map’);

function st_render_knowledge_base_map() {

 

    ob_start();

 

    $categories = get_categories(array(‘hide_empty’ => 0));

    $sidebar_html = »;

    $colors = [‘#ffde59’, ‘#c1ff00’, ‘#38b6ff’, ‘#ff914d’, ‘#cb6ce6’, ‘#ff6b6b’, ‘#48dbfb’];

 

    foreach ($categories as $cat) {

        $raw_name = esc_html($cat->name);

        $number = »;

        $rest_of_name = $raw_name;

 

        if (preg_match(‘/^\[(\d+)\]\s*(.*)$/u’, $raw_name, $matches)) {

            $number = $matches[1];

            $rest_of_name = $matches[2];

        }

 

        $parts = explode(‘|’, $rest_of_name);

        $main_title = trim($parts[0]);

        $sub_title = isset($parts[1]) ? trim($parts[1]) : »;

 

        $hash = abs(crc32($main_title));

        $bg_color = $colors[$hash % count($colors)];

 

        $icon = !empty($number) ? $number : mb_substr($main_title, 0, 1, ‘UTF-8’);

        $desc = trim($cat->description);

 

        $btn_inner = ‘<div class=»st-cat-icon» style=»background-color: ‘ . $bg_color . ‘;»>’ . $icon . ‘</div>’;

        $btn_inner .= ‘<div class=»st-cat-text»>’;

        $btn_inner .= ‘<span class=»st-cat-main»>’ . $main_title . ‘</span>’;

 

        if ($sub_title) {

            $btn_inner .= ‘<span class=»st-cat-sphere»><small>для:</small> <strong>’ . $sub_title . ‘</strong></span>’;

        }

        $btn_inner .= ‘</div>’;

 

        $sidebar_html .= sprintf(

            ‘<button class=»st-cat-btn» data-id=»%d» data-count=»%d» data-number=»%s» data-main=»%s» data-sub=»%s» data-desc=»%s» title=»%s»>%s</button>’,

            esc_attr($cat->term_id),

            esc_attr($cat->count),

            esc_attr($number),

            esc_attr($main_title),

            esc_attr($sub_title),

            esc_attr($desc),

            esc_attr($raw_name),

            $btn_inner

        );

    }

    ?>

 

    <style>

        .st-kb-container {

            —bg-paper: #fdfcf8;

            —ink-black: #1a1a1a;

            —marker-yellow: #ffde59;

            —marker-green: #c1ff00;

            —marker-blue: #38b6ff;

            —border-sketch: 2px solid var(—ink-black);

            —transition: 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);

            —font-main: Georgia, ‘Times New Roman’, Times, serif;

        }

 

        .st-kb-container .st-workspace {

            display: flex; height: calc(100vh — 60px); min-height: 500px;

            width: 100%; margin: 20px 0; background: var(—bg-paper);

            font-family: var(—font-main); color: var(—ink-black);

            overflow: hidden; box-sizing: border-box;

            background-image: radial-gradient(var(—ink-black) 1px, transparent 1px);

            background-size: 20px 20px; border: var(—border-sketch);

            border-radius: 16px; box-shadow: 6px 6px 0 var(—ink-black);

            position: relative;

        }

 

        .st-kb-container .st-sidebar {

            width: 280px; background: #fff; border-right: var(—border-sketch);

            display: flex; flex-direction: column;

            transition: width var(—transition), transform var(—transition);

            flex-shrink: 0; z-index: 20;

        }

        .st-kb-container .st-sidebar.collapsed { width: 75px; }

 

        .st-kb-container .st-sidebar-header {

            padding: 15px; display: flex; align-items: center; gap: 12px;

            border-bottom: 2px dashed rgba(0,0,0,0.15); background: #fff;

            flex-wrap: wrap;

        }

 

        .st-kb-container .st-toggle-btn {

            background: #f5f5f5; border: 1px solid var(—ink-black); font-size: 1.2rem; cursor: pointer;

            color: var(—ink-black); transition: var(—transition); box-shadow: 2px 2px 0 var(—ink-black);

            width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;

            border-radius: 8px; flex-shrink: 0;

        }

        .st-kb-container .st-toggle-btn:active { transform: translateY(2px); box-shadow: 0 0 0 var(—ink-black); }

        .st-kb-container .st-sidebar.collapsed .st-toggle-btn { transform: rotate(180deg); }

 

        .st-kb-container .st-sidebar-title-group {

            display: flex; align-items: center; gap: 10px;

            flex: 1; min-width: 0;

            transition: opacity 0.2s;

        }

        .st-kb-container .st-sidebar.collapsed .st-sidebar-title-group {

            opacity: 0; pointer-events: none; display: none;

        }

        .st-kb-container .st-sidebar-title-text {

            font-weight: 900; font-size: 0.9rem; line-height: 1.2;

            text-transform: uppercase; letter-spacing: 0.5px;

            background: #f0f0e8; padding: 6px 10px; border-radius: 8px;

            border-left: 3px solid var(—ink-black);

            white-space: normal; word-break: break-word;

        }

 

        .st-kb-container .st-guide-icon {

            width: 22px; height: 22px; font-size: 0.7rem;

            background: #fff; border: 2px solid var(—ink-black);

            box-shadow: 2px 2px 0 var(—ink-black);

        }

        .st-kb-container .st-guide-icon:hover {

            background: var(—marker-yellow); transform: translateY(-2px) scale(1.1);

            box-shadow: 3px 3px 0 var(—ink-black);

        }

 

        .st-kb-container .st-cat-btn {

            display: flex; align-items: center; width: 100%; padding: 10px 15px;

            border: none; background: transparent; cursor: pointer;

            text-align: left; border-bottom: 1px dashed rgba(0,0,0,0.05);

            transition: background 0.2s; box-sizing: border-box;

            position: relative;

        }

        .st-kb-container .st-cat-btn:hover { background: #fdfcf8; }

        .st-kb-container .st-cat-btn.active { background: #f4f9f4; box-shadow: inset 4px 0 0 var(—ink-black); }

 

        .st-kb-container .st-cat-icon {

            width: 40px; height: 40px; min-width: 40px;

            border: 2px solid var(—ink-black); border-radius: 10px 4px 10px 4px;

            display: flex; justify-content: center; align-items: center;

            font-family: var(—font-main); font-weight: 900; font-size: 1.5rem; color: var(—ink-black);

            box-shadow: 2px 2px 0 var(—ink-black); transition: var(—transition);

        }

        .st-kb-container .st-cat-btn.active .st-cat-icon { transform: scale(1.05) rotate(-3deg); border-width: 3px; }

        @media (hover: hover) { .st-kb-container .st-cat-btn:hover .st-cat-icon { transform: translateY(-2px) rotate(2deg); } }

 

        .st-kb-container .st-cat-text {

            display: flex; flex-direction: column; margin-left: 15px;

            white-space: nowrap; opacity: 1; transition: opacity 0.2s; overflow: hidden;

            flex: 1;

        }

        .st-kb-container .st-sidebar.collapsed .st-cat-text { opacity: 0; display: none; }

        .st-kb-container .st-cat-main { font-weight: 900; font-size: 0.95rem; letter-spacing: 0.5px; text-overflow: ellipsis; overflow: hidden; }

 

        .st-kb-container .st-cat-sphere {

            font-size: 0.7rem; margin-top: 2px; color: var(—ink-black);

            display: flex; align-items: baseline; gap: 4px;

        }

        .st-kb-container .st-cat-sphere small { color: #777; font-size: 0.6rem; text-transform: uppercase; font-weight: bold; }

        .st-kb-container .st-cat-sphere strong {

            font-weight: 900; letter-spacing: 0.5px;

            background: linear-gradient(to top, rgba(255,222,89, 0.6) 40%, transparent 40%);

            padding: 0 2px;

        }

 

        .st-kb-container .st-main-content {

            flex: 1; display: flex; flex-direction: column; position: relative;

            background: rgba(253, 252, 248, 0.9); backdrop-filter: blur(2px);

            overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(—ink-black) transparent;

        }

        .st-kb-container .st-main-content::-webkit-scrollbar { width: 6px; }

        .st-kb-container .st-main-content::-webkit-scrollbar-thumb { background: var(—ink-black); border-radius: 10px; }

 

        .st-kb-container .header-group {

            padding: 20px 30px; position: sticky; top: 0;

            background: rgba(253, 252, 248, 0.95); backdrop-filter: blur(5px);

            z-index: 10; border-bottom: 2px dashed rgba(0,0,0,0.15);

            display: flex; flex-direction: column; gap: 12px;

        }

 

        .st-kb-container .st-header-title-row {

            display: flex; align-items: center; gap: 12px; 

            flex-wrap: nowrap; width: 100%; overflow: hidden;

        }

        .st-kb-container .cat-seq-badge {

            background: var(—marker-yellow); border: 2.5px solid var(—ink-black);

            width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;

            font-family: var(—font-main); font-size: 1.4rem; font-weight: 900;

            border-radius: 8px 4px 10px 4px; box-shadow: 3px 3px 0 var(—ink-black);

            transform: rotate(-3deg); flex-shrink: 0;

        }

        .st-kb-container .category-header {

            font-weight: 900; font-size: clamp(1.1rem, 2.5vw, 1.6rem); text-transform: uppercase;

            margin: 0; line-height: 1.1; color: var(—ink-black); letter-spacing: 0.5px;

            white-space: nowrap; overflow: hidden; text-overflow: ellipsis;

            flex-shrink: 1;

        }

 

        .st-kb-container .cat-info-btn {

            background: #fff; border: 1.5px solid var(—ink-black); border-radius: 50%;

            width: 27px; height: 27px; padding: 0; font-family: var(—font-main);

            font-weight: bold; font-size: 0.84rem; cursor: pointer;

            display: inline-flex; align-items: center; justify-content: center;

            box-shadow: 2px 2px 0 var(—ink-black); transition: var(—transition);

            color: var(—ink-black); line-height: 1;

        }

        .st-kb-container .cat-info-btn:hover {

            background: var(—marker-yellow); transform: translateY(-2px); box-shadow: 3px 3px 0 var(—ink-black);

        }

        .st-kb-container .cat-info-btn:active {

            transform: translateY(1px); box-shadow: 1px 1px 0 var(—ink-black);

        }

 

        .st-kb-container .st-meta-sentence {

            display: flex; align-items: baseline; gap: 8px; 

            flex-wrap: nowrap;

            background: rgba(255, 255, 255, 0.6); border: 1px dashed rgba(0,0,0,0.2);

            padding: 8px 12px; border-radius: 8px; width: fit-content;

            max-width: 100%; overflow-x: auto;

            scrollbar-width: none; white-space: nowrap;

        }

        .st-kb-container .st-meta-sentence::-webkit-scrollbar { display: none; }

        .st-kb-container .category-stats-number {

            font-size: 1rem; font-weight: 900; background: var(—marker-green);

            display: inline-flex; justify-content: center; align-items: center;

            padding: 2px 8px; min-width: 22px; transform: rotate(-2deg);

            border: 2px solid var(—ink-black); box-shadow: 2px 2px 0 var(—ink-black);

            border-radius: 6px; flex-shrink: 0; position: relative; top: 2px;

        }

        .st-kb-container .category-stats-label {

            font-size: 0.85rem; font-weight: bold; color: #333;

            text-transform: uppercase; letter-spacing: 0.5px; flex-shrink: 0;

        }

        .st-kb-container .st-header-sphere {

            display: inline-flex; align-items: baseline; gap: 6px; flex-shrink: 0;

        }

        .st-kb-container .sphere-value {

            font-size: 0.95rem; font-weight: 900; color: var(—ink-black);

            text-transform: uppercase; letter-spacing: 0.5px;

            background: linear-gradient(to top, rgba(255,222,89, 0.7) 40%, transparent 40%);

            padding: 0 4px;

        }

 

        .st-kb-container .timeline { padding: 30px 40px 60px 40px; position: relative; width: 100%; box-sizing: border-box; max-width: 900px; }

        .st-kb-container .timeline::before { content: »; position: absolute; left: 40px; top: 30px; bottom: 0; width: 3px; background: var(—ink-black); }

        .st-kb-container .timeline-item {

            position: relative; margin-bottom: 30px; padding-left: 30px;

            text-decoration: none; display: flex; flex-direction: column;

            align-items: flex-start; gap: 8px; opacity: 0; transform: translateX(-15px);

            transition: var(—transition); width: 100%; box-sizing: border-box;

        }

        .st-kb-container .timeline-item.show { opacity: 1; transform: translateX(0); }

        .st-kb-container .date-box {

            background: var(—marker-blue); border: 2px solid var(—ink-black); padding: 3px 10px;

            font-weight: bold; font-size: 0.75rem; color: var(—ink-black);

            transform: rotate(-1deg); box-shadow: 2px 2px 0px var(—ink-black); z-index: 2;

        }

        .st-kb-container .leaf-box {

            background: #fff; border: var(—border-sketch); padding: 15px 20px;

            border-radius: 0px 15px 4px 15px; box-shadow: 3px 3px 0px var(—ink-black);

            transition: var(—transition); max-width: 100%; width: 100%; box-sizing: border-box; z-index: 1;

        }

        @media (hover: hover) { .st-kb-container .timeline-item:hover .leaf-box { transform: scale(1.01) translateX(5px); background: #fdfdfd; } }

        .st-kb-container .timeline-title { 

            font-weight: bold; font-size: clamp(1rem, 2vw, 1.2rem); margin: 0; 

            line-height: 1.4; color: var(—ink-black); word-break: break-word; 

        }

        .st-kb-container .timeline-excerpt {

            font-size: 0.9rem; line-height: 1.5; color: #444; margin: 10px 0 0 0; padding-top: 10px;

            border-top: 1px dashed rgba(26,26,26,0.2); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;  

            overflow: hidden; text-overflow: ellipsis;

        }

        .st-kb-container .timeline-item::after { content: ‘📌’; position: absolute; left: -10px; top: 25px; font-size: 20px; filter: drop-shadow(1px 1px 0px white); }

        .st-kb-container #st-kb-loader { text-align: center; font-weight: bold; font-size: 1.1rem; padding: 40px; }

 

        .st-kb-container .st-mobile-overlay, .st-kb-container .st-desc-modal-overlay {

            position: absolute; top: 0; left: 0; width: 100%; height: 100%;

            background: rgba(0,0,0,0.5); backdrop-filter: blur(2px);

            z-index: 15; opacity: 0; visibility: hidden; transition: 0.3s;

        }

        .st-kb-container .st-mobile-overlay.show { opacity: 1; visibility: visible; }

        .st-kb-container .st-desc-modal-overlay { z-index: 100; display: flex; align-items: center; justify-content: center; }

        .st-kb-container .st-desc-modal-overlay.active { opacity: 1; visibility: visible; }

        

        .st-kb-container .st-desc-modal-box {

            background: var(—bg-paper); width: 90%; max-width: 450px;

            border: var(—border-sketch); border-radius: 12px; box-shadow: 6px 6px 0 var(—ink-black);

            transform: translateY(20px) rotate(-1deg); transition: 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);

            background-image: radial-gradient(var(—ink-black) 1px, transparent 1px); background-size: 15px 15px;

            max-height: 80vh; display: flex; flex-direction: column;

        }

        .st-kb-container .st-desc-modal-overlay.active .st-desc-modal-box { transform: translateY(0) rotate(0); }

        

        .st-kb-container .st-desc-header {

            padding: 15px 20px; border-bottom: var(—border-sketch); background: var(—marker-yellow);

            border-radius: 10px 10px 0 0; display: flex; justify-content: space-between; align-items: center;

        }

        .st-kb-container .st-desc-header h3 { margin: 0; font-size: 1.1rem; font-weight: 900; text-transform: uppercase; }

        .st-kb-container .st-desc-close {

            background: #ff4444; border: 2px solid var(—ink-black); border-radius: 50%;

            width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;

            color: #fff; font-size: 1.2rem; cursor: pointer; box-shadow: 2px 2px 0 var(—ink-black);

            transition: 0.2s; padding: 0; line-height: 1;

        }

        .st-kb-container .st-desc-close:hover { transform: scale(1.1) rotate(10deg); background: #ff3333; }

        

        .st-kb-container .st-desc-body { 

            padding: 20px; font-size: 0.95rem; line-height: 1.6; font-weight: 500; 

            background: rgba(255,255,255,0.9); border-radius: 0 0 10px 10px; overflow-y: auto;

        }

 

        @media (max-width: 800px) {

            .st-kb-container .st-workspace { border-radius: 12px; margin: 10px 0; height: calc(100vh — 20px); min-height: 550px; box-shadow: 4px 4px 0 var(—ink-black); }

            .st-kb-container .st-sidebar { position: absolute; height: 100%; z-index: 20; transform: translateX(-100%); width: 280px; box-shadow: 10px 0 25px rgba(0,0,0,0.2); }

            .st-kb-container .st-sidebar.mobile-open { transform: translateX(0); }

            .st-kb-container .st-sidebar-header { justify-content: space-between; }

            .st-kb-container .st-toggle-btn { transform: rotate(180deg); }

            .st-kb-container .mobile-menu-trigger { 

                display: flex; align-items: center; justify-content: space-between;

                width: 100%; margin-bottom: 15px; background: #fff; 

                border: 2px solid var(—ink-black); padding: 8px 12px; 

                border-radius: 12px; box-shadow: 4px 4px 0 var(—ink-black); 

                cursor: pointer; transition: 0.1s; color: var(—ink-black);

            }

            .st-kb-container .mobile-menu-trigger:active { transform: translateY(3px); box-shadow: 1px 1px 0 var(—ink-black); }

            .st-kb-container .trigger-left { display: flex; align-items: center; gap: 12px; }

            .st-kb-container .trigger-icon { 

                display: flex; align-items: center; justify-content: center; width: 38px; height: 38px; background: var(—marker-yellow);

                border: 2px solid var(—ink-black); border-radius: 8px; font-size: 1.4rem; box-shadow: 2px 2px 0 var(—ink-black);

            }

            .st-kb-container .trigger-text-group { display: flex; flex-direction: column; text-align: left; }

            .st-kb-container .trigger-text { font-weight: 900; font-size: 1.05rem; text-transform: uppercase; letter-spacing: 0.5px; line-height: 1.1; }

            .st-kb-container .trigger-sub { font-size: 0.7rem; font-weight: bold; color: #666; margin-top: 2px; }

            .st-kb-container .trigger-arrow { 

                display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; background: #f4f4f4; border: 2px solid var(—ink-black); border-radius: 50%;

            }

            .st-kb-container .timeline { padding: 15px 15px 40px 20px; }

            .st-kb-container .timeline::before { left: 15px; top: 15px; }

            .st-kb-container .timeline-item::after { left: -10px; top: 22px; font-size: 16px; }

            .st-kb-container .timeline-item { padding-left: 20px; }

            .st-kb-container .header-group { padding: 15px; }

            .st-kb-container .leaf-box { padding: 12px 15px; }

            .st-kb-container .st-header-title-row { gap: 8px; }

            .st-kb-container .cat-seq-badge { width: 32px; height: 32px; font-size: 1.1rem; }

            .st-kb-container .st-meta-sentence { padding: 6px 10px; border: none; background: transparent; }

            .st-kb-container .sphere-value { font-size: 0.85rem; }

 

            .st-kb-container .st-guide-icon { width: 16px; height: 16px; font-size: 0.6rem; }

            .st-kb-container .cat-info-btn { width: 24px; height: 24px; font-size: 0.7rem; }

        }

        @media (min-width: 801px) {

            .st-kb-container .mobile-menu-trigger { display: none; }

        }

    </style>

 

    <div class=»st-kb-container» id=»st-kb-wrap»>

        <div class=»st-workspace»>

            <div class=»st-mobile-overlay» id=»st-mobile-overlay»></div>

 

            <div class=»st-desc-modal-overlay» id=»st-desc-modal»>

                <div class=»st-desc-modal-box»>

                    <div class=»st-desc-header»>

                        <h3 id=»st-desc-modal-title»>Описание</h3>

                        <button class=»st-desc-close» id=»st-desc-close»>×</button>

                    </div>

                    <div class=»st-desc-body» id=»st-desc-body»></div>

                </div>

            </div>

 

            <aside class=»st-sidebar» id=»st-sidebar»>

                <div class=»st-sidebar-header»>

                    <button id=»st-toggle-btn» class=»st-toggle-btn»>≡</button>

                    <div class=»st-sidebar-title-group»>

                        <div class=»st-sidebar-title-text»>НАЖМИ на</div>

                        <span class=»st-info-trigger st-guide-icon» data-info-title=»Как это работает?» data-info-content=»📌 Это окно появляется при нажатии на значок ❔.<br>Оно выводит описание о том объекте, рядом с которым он расположен.<br><br><strong>Пример:</strong> Так как этот значок расположен здесь, он выводит информацию о данном разделе:<br><div style=’margin-top:10px; padding:10px; background:rgba(255,222,89,0.15); border-left:3px solid var(—ink-black); font-style:normal;’><strong>Разделы упорядочены по номерам</strong> и содержат информацию, необходимую для реализации проекта в указанной сфере деятельности.<br><br>Нажмите на любой раздел — справа отобразятся все публикации, относящиеся к этой теме.</div>»>❔</span>

                    </div>

                </div>

                <div class=»st-sidebar-menu»>

                    <?php echo $sidebar_html; ?>

                </div>

            </aside>

 

            <main class=»st-main-content» id=»st-main-content»>

                <div class=»header-group»>

                    <button class=»mobile-menu-trigger» id=»st-mobile-trigger»>

                        <div class=»trigger-left»>

                            <span class=»trigger-icon»>🗂️</span>

                            <div class=»trigger-text-group»>

                                <span class=»trigger-text»>Направления</span>

                                <span class=»trigger-sub»>Показать все разделы</span>

                            </div>

                        </div>

                        <div class=»trigger-arrow»>

                            <svg width=»16″ height=»16″ viewBox=»0 0 24 24″ fill=»none» stroke=»currentColor» stroke-width=»3″ stroke-linecap=»round» stroke-linejoin=»round»><polyline points=»6 9 12 15 18 9″></polyline></svg>

                        </div>

                    </button>

 

                    <div class=»st-header-title-row»>

                        <div class=»cat-seq-badge» id=»cat-seq-display» style=»display:none;»></div>

                        <h1 class=»category-header» id=»current-category-title»>Загрузка…</h1>

                        <button class=»cat-info-btn» id=»cat-info-btn» style=»display: none;» title=»Описание рубрики»>📖</button>

                    </div>

 

                    <div class=»st-meta-sentence»>

                        <span class=»category-stats-number» id=»cat-count-display»>0</span>

                        <span class=»category-stats-label» id=»cat-stats-label»>записей по теме</span>

                        <span class=»st-header-sphere» id=»cat-sphere-display» style=»display:none;»>

                            <span class=»sphere-value» id=»cat-sphere-text»></span>

                        </span>

                    </div>

                </div>

 

                <div id=»st-kb-loader»>🔄 Синхронизация данных…</div>

                <div class=»timeline» id=»timeline-container»></div>

            </main>

        </div>

    </div>

 

    <script>

    document.addEventListener(‘DOMContentLoaded’, function() {

        const wrap = document.getElementById(‘st-kb-wrap’);

        const sidebar = wrap.querySelector(‘#st-sidebar’);

        const toggleBtn = wrap.querySelector(‘#st-toggle-btn’);

        const mobileTrigger = wrap.querySelector(‘#st-mobile-trigger’);

        const overlay = wrap.querySelector(‘#st-mobile-overlay’);

        const tabs = wrap.querySelectorAll(‘.st-cat-btn’);

        const container = wrap.querySelector(‘#timeline-container’);

        

        const titleEl = wrap.querySelector(‘#current-category-title’);

        const seqDisplay = wrap.querySelector(‘#cat-seq-display’);

        const countEl = wrap.querySelector(‘#cat-count-display’);

        const statsLabel = wrap.querySelector(‘#cat-stats-label’);

        const sphereDisplay = wrap.querySelector(‘#cat-sphere-display’);

        const sphereText = wrap.querySelector(‘#cat-sphere-text’);

        const infoBtn = wrap.querySelector(‘#cat-info-btn’);

        

        const descModal = wrap.querySelector(‘#st-desc-modal’);

        const descModalTitle = wrap.querySelector(‘#st-desc-modal-title’);

        const descBody = wrap.querySelector(‘#st-desc-body’);

        const closeDesc = wrap.querySelector(‘#st-desc-close’);

        

        const loader = wrap.querySelector(‘#st-kb-loader’);

        const contentArea = wrap.querySelector(‘#st-main-content’);

        

        const apiUrl = ‘<?php echo esc_url( rest_url( ‘wp/v2′ ) ); ?>’;

 

        function openInfoModal(title, contentHtml) {

            descModalTitle.innerHTML = title;

            descBody.innerHTML = contentHtml;

            descModal.classList.add(‘active’);

        }

 

        closeDesc.addEventListener(‘click’, () => descModal.classList.remove(‘active’));

        descModal.addEventListener(‘click’, (e) => { if(e.target === descModal) descModal.classList.remove(‘active’); });

 

        const infoTriggers = wrap.querySelectorAll(‘.st-info-trigger’);

        infoTriggers.forEach(trigger => {

            trigger.addEventListener(‘click’, (e) => {

                e.stopPropagation();

                const title = trigger.getAttribute(‘data-info-title’) || ‘Информация’;

                const content = trigger.getAttribute(‘data-info-content’) || ‘<em>Нет данных</em>’;

                openInfoModal(title, content);

            });

        });

 

        function closeMobileMenu() {

            sidebar.classList.remove(‘mobile-open’);

            overlay.classList.remove(‘show’);

        }

 

        function openMobileMenu() {

            sidebar.classList.add(‘mobile-open’);

            overlay.classList.add(‘show’);

        }

 

        toggleBtn.addEventListener(‘click’, () => {

            if (window.innerWidth > 800) sidebar.classList.toggle(‘collapsed’);

            else closeMobileMenu(); 

        });

 

        mobileTrigger.addEventListener(‘click’, openMobileMenu);

        overlay.addEventListener(‘click’, closeMobileMenu);

 

        infoBtn.addEventListener(‘click’, () => {

            const activeTab = wrap.querySelector(‘.st-cat-btn.active’);

            if (activeTab) {

                const desc = activeTab.getAttribute(‘data-desc’);

                const mainTitle = activeTab.getAttribute(‘data-main’);

                if (desc && desc.trim() !== ») {

                    openInfoModal(‘О рубрике: ‘ + mainTitle, desc);

                } else {

                    openInfoModal(‘О рубрике’, ‘<em>Описание отсутствует</em>’);

                }

            }

        });

 

        tabs.forEach(tab => {

            tab.addEventListener(‘click’, function() {

                const catId = this.getAttribute(‘data-id’);

                const catCount = this.getAttribute(‘data-count’) || 0;

                const catNum = this.getAttribute(‘data-number’);

                const mainTitle = this.getAttribute(‘data-main’);

                const subTitle = this.getAttribute(‘data-sub’);

                const catDesc = this.getAttribute(‘data-desc’);

 

                tabs.forEach(t => t.classList.remove(‘active’));

                this.classList.add(‘active’);

 

                if (window.innerWidth <= 800) closeMobileMenu();

 

                container.innerHTML = »;

 

                if (catNum && catNum.trim() !== ») {

                    seqDisplay.innerText = catNum;

                    seqDisplay.style.display = ‘flex’;

                } else {

                    seqDisplay.style.display = ‘none’;

                }

 

                titleEl.innerText = mainTitle;

                countEl.innerText = catCount;

 

                if (subTitle && subTitle.trim() !== ») {

                    statsLabel.innerText = ‘записей для:’;

                    sphereText.innerText = subTitle;

                    sphereDisplay.style.display = ‘inline-flex’;

                } else {

                    statsLabel.innerText = ‘записей по теме’;

                    sphereDisplay.style.display = ‘none’;

                }

 

                if (catDesc && catDesc.trim() !== ») {

                    infoBtn.style.display = ‘inline-flex’;

                } else {

                    infoBtn.style.display = ‘none’;

                }

 

                if (!catId) {

                    loader.style.display = ‘none’;

                    container.innerHTML = `<div class=»date-box» style=»margin:20px auto; background:#ff4444; color:#fff;»>Ошибка: ID не найден.</div>`;

                    return;

                }

                fetchPosts(catId);

            });

        });

 

        if(tabs.length > 0) tabs[0].click();

        else { loader.style.display = ‘none’; titleEl.innerText = ‘Нет рубрик’; }

 

        function fetchPosts(categoryId) {

            loader.style.display = ‘block’;

            loader.innerText = ‘📌 Подгружаем данные…’;

            contentArea.scrollTo({top: 0, behavior: ‘smooth’});

 

            fetch(`${apiUrl}/posts?categories=${categoryId}&per_page=50`)

                .then(response => response.json())

                .then(posts => {

                    loader.style.display = ‘none’;

                    if (!Array.isArray(posts) || posts.length === 0) {

                        container.innerHTML = ‘<div class=»date-box» style=»margin:20px;»>Нет записей в данной категории</div>’;

                        return;

                    }

 

                    posts.forEach((post, index) => {

                        const dateObj = new Date(post.date);

                        const date = dateObj.toLocaleDateString(‘ru-RU’, { day: ‘2-digit’, month: ‘2-digit’, year: ‘numeric’ });

                        

                        let excerptText = »;

                        if (post.excerpt && post.excerpt.rendered) {

                            let tempDiv = document.createElement(‘div’);

                            tempDiv.innerHTML = post.excerpt.rendered;

                            excerptText = tempDiv.textContent || tempDiv.innerText || »;

                            excerptText = excerptText.trim();

                        }

                        

                        let excerptHtml = excerptText ? `<div class=»timeline-excerpt»>${excerptText}</div>` : »;

 

                        const item = document.createElement(‘a’);

                        item.href = post.link; 

                        item.className = ‘timeline-item’;

                        item.innerHTML = `

                            <div class=»date-box»>${date}</div>

                            <div class=»leaf-box»>

                                <h3 class=»timeline-title»>${post.title.rendered}</h3>

                                ${excerptHtml}

                            </div>

                        `;

                        container.appendChild(item);

                        setTimeout(() => { item.classList.add(‘show’); }, 50 * index);

                    });

                })

                .catch(err => {

                    loader.style.display = ‘none’;

                    container.innerHTML = ‘<div class=»date-box» style=»margin:20px; background:#ff4444; color:#fff;»>Ошибка получения данных</div>’;

                });

        }

    });

    </script>

 

    <?php

    return ob_get_clean();

}

 

// ==============================================================================

// 4. ШОРТКОД ДРЕВА/ЛОЗЫ КАТЕГОРИЙ:

// ==============================================================================

add_shortcode(‘st_vine_buds’, ‘st_render_vine_buds_layout’);

 

function st_render_vine_buds_layout() {

    static $buds_assets_loaded = false;

    $html = »;

 

    if (!$buds_assets_loaded) {

        $html .= st_get_vine_buds_assets();

        $buds_assets_loaded = true;

    }

 

    $html .= ‘<div class=»st-vine-container» id=»st-vine-comp»>’;

    $html .= ‘<div class=»st-vine-wrapper»>’;

    $html .= ‘<div class=»st-vine-trunk»></div>’;

 

    $categories = get_categories(array(‘hide_empty’ => true));

    $branch_side = ‘left’; 

 

    foreach ($categories as $index => $cat) {

        $posts = get_posts(array(

            ‘category’    => $cat->term_id,

            ‘numberposts’ => 20,

            ‘post_status’ => ‘publish’

        ));

 

        if ($posts) {

            $branch_side = ($index % 2 === 0) ? ‘left’ : ‘right’;

 

            $html .= ‘<div class=»st-vine-branch-container st-side-‘ . $branch_side . ‘»>’;

            $html .= ‘  <div class=»st-branch-line»></div>’; 

            $html .= ‘  <div class=»st-branch-content»>’;

            $html .= ‘    <h3 class=»st-branch-title»>’ . esc_html($cat->name) . ‘</h3>’;

            $html .= ‘    <div class=»st-buds-row»>’;

            

            $hidden_list = ‘<ul class=»st-modal-post-list»>’;

 

            foreach ($posts as $post_index => $p) {

                $post_url = esc_url(get_permalink($p->ID));

                $post_title = esc_html($p->post_title);

                $post_date = get_the_date(‘d.m.Y’, $p->ID);

                

                $is_top = ($post_index % 2 === 0);

                $position_class = $is_top ? ‘st-bud-top’ : ‘st-bud-bottom’;

                

                $html .= ‘<a href=»‘ . $post_url . ‘» class=»st-bud-wrapper ‘ . $position_class . ‘» data-tooltip=»‘ . $post_title . ‘»>’;

                $html .= ‘  <span class=»st-bud-shape»></span>’;

                $html .= ‘</a>’;

 

                $hidden_list .= ‘<li><span class=»st-post-date»>’ . $post_date . ‘</span> <a href=»‘ . $post_url . ‘»>’ . $post_title . ‘</a></li>’;

            }

            

            $hidden_list .= ‘</ul>’;

 

            $html .= ‘      <div class=»st-branch-end-icon» data-cat=»‘ . esc_attr($cat->name) . ‘» data-tooltip=»Все записи»>’;

            $html .= ‘        <svg viewBox=»0 0 24 24″ width=»16″ height=»16″ stroke=»currentColor» stroke-width=»2.5″ fill=»none» stroke-linecap=»round» stroke-linejoin=»round»><line x1=»8″ y1=»6″ x2=»21″ y2=»6″></line><line x1=»8″ y1=»12″ x2=»21″ y2=»12″></line><line x1=»8″ y1=»18″ x2=»21″ y2=»18″></line><line x1=»3″ y1=»6″ x2=»3.01″ y2=»6″></line><line x1=»3″ y1=»12″ x2=»3.01″ y2=»12″></line><line x1=»3″ y1=»18″ x2=»3.01″ y2=»18″></line></svg>’;

            $html .= ‘      </div>’;

            $html .= ‘      <div class=»st-hidden-data» style=»display: none;»>’ . $hidden_list . ‘</div>’;

            $html .= ‘    </div>’; 

            $html .= ‘  </div>’; 

            $html .= ‘</div>’; 

        }

    }

 

    $html .= ‘</div>’; 

    $html .= ‘

    <div id=»st-vine-modal» class=»st-vine-overlay»>

        <div class=»st-vine-modal-box»>

            <div class=»st-vine-modal-header»>

                <h3 id=»st-vine-modal-title»>Рубрика</h3>

                <button class=»st-vine-close» id=»st-vine-close-btn»>×</button>

            </div>

            <div class=»st-vine-modal-body» id=»st-vine-modal-body»>

            </div>

        </div>

    </div>’;

    $html .= ‘</div>’; // Конец st-vine-container

 

    return $html;

}

 

function st_get_vine_buds_assets() {

    return ‘

    <style>

        @import url(«https://fonts.googleapis.com/css2?family=Neucha&display=swap»);

 

        .st-vine-container {

            —bg-paper: #fdfcf8;

            —ink-black: #1a1a1a;

            —marker-yellow: #ffde59;

            —marker-green: #c1ff00;

            —marker-blue: #38b6ff;

            —marker-red: #ff5757;

            —border-sketch: 2px solid var(—ink-black);

            —font-sketch: «Neucha», Georgia, cursive;

        }

 

        .st-vine-container .st-vine-wrapper { position: relative; max-width: 900px; margin: 40px auto; padding: 20px 0; font-family: var(—font-sketch); overflow: hidden; }

        

        .st-vine-container .st-vine-trunk { position: absolute; top: 0; bottom: 0; left: 50%; width: 4px; background: var(—ink-black); transform: translateX(-50%); z-index: 1; border-radius: 2px; }

        

        .st-vine-container .st-vine-branch-container { position: relative; width: 50%; margin-bottom: 50px; opacity: 0; transform: translateY(20px); transition: opacity 0.8s ease, transform 0.8s ease; z-index: 2; }

        .st-vine-container .st-vine-branch-container.st-grow { opacity: 1; transform: translateY(0); }

        .st-vine-container .st-side-left { left: 0; padding-right: 40px; text-align: right; }

        .st-vine-container .st-side-right { left: 50%; padding-left: 40px; text-align: left; }

        

        .st-vine-container .st-branch-line { position: absolute; top: 45px; width: calc(100% — 20px); height: 3px; background: var(—ink-black); z-index: -1; }

        .st-vine-container .st-side-left .st-branch-line { right: 0; }

        .st-vine-container .st-side-right .st-branch-line { left: 0; }

        

        .st-vine-container .st-branch-content { display: inline-block; max-width: 90%; }

        

        .st-vine-container .st-branch-title { margin: 0 0 10px 0; font-size: 22px; color: var(—ink-black); text-transform: uppercase; font-weight: 800; background: var(—marker-yellow); display: inline-block; padding: 2px 10px; border: var(—border-sketch); transform: rotate(-2deg); box-shadow: 3px 3px 0 var(—ink-black); }

        .st-vine-container .st-side-right .st-branch-title { transform: rotate(2deg); }

        

        .st-vine-container .st-buds-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; height: 40px; }

        .st-vine-container .st-side-left .st-buds-row { justify-content: flex-end; flex-direction: row-reverse; }

        .st-vine-container .st-side-right .st-buds-row { justify-content: flex-start; }

        

        .st-vine-container .st-bud-wrapper { position: relative; display: block; width: 18px; height: 18px; cursor: pointer; text-decoration: none; }

        .st-vine-container .st-bud-shape { display: block; width: 100%; height: 100%; border-radius: 0 50% 50% 50%; background: #fff; border: var(—border-sketch); box-shadow: 2px 2px 0 var(—ink-black); transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }

        

        .st-vine-container .st-bud-top { transform: translateY(-8px); }

        .st-vine-container .st-bud-top .st-bud-shape { transform: rotate(-45deg); }

        .st-vine-container .st-bud-bottom { transform: translateY(8px); }

        .st-vine-container .st-bud-bottom .st-bud-shape { transform: rotate(135deg); }

        

        .st-vine-container .st-bud-wrapper:hover { z-index: 10; }

        .st-vine-container .st-bud-wrapper:hover .st-bud-shape { background: var(—marker-green); transform: scale(1.3) rotate(0deg); box-shadow: 4px 4px 0 var(—ink-black); border-radius: 50% 50% 50% 0; }

 

        .st-vine-container .st-branch-end-icon {

            display: flex; align-items: center; justify-content: center;

            width: 32px; height: 32px; border-radius: 50%;

            background: #fff; border: var(—border-sketch); color: var(—ink-black); box-shadow: 3px 3px 0 var(—ink-black);

            cursor: pointer; z-index: 10; transition: all 0.2s ease; position: relative;

        }

        .st-vine-container .st-branch-end-icon:hover { background: var(—marker-blue); transform: scale(1.1) rotate(10deg); box-shadow: 4px 4px 0 var(—ink-black); }

 

        .st-vine-container .st-bud-wrapper::after, .st-vine-container .st-branch-end-icon::after {

            content: attr(data-tooltip); position: absolute; bottom: 150%; left: 50%; transform: translateX(-50%) translateY(10px);

            background: #fff; color: var(—ink-black); padding: 4px 10px; border-radius: 6px; font-size: 14px; white-space: nowrap; opacity: 0; visibility: hidden; transition: all 0.2s ease; pointer-events: none; border: var(—border-sketch); box-shadow: 3px 3px 0 var(—ink-black); z-index: 20; font-weight: bold;

        }

        .st-vine-container .st-bud-wrapper:hover::after, .st-vine-container .st-branch-end-icon:hover::after {

            opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0);

        }

 

        .st-vine-container .st-vine-overlay {

            position: fixed; top: 0; left: 0; width: 100%; height: 100%;

            background: rgba(26, 26, 26, 0.7); backdrop-filter: blur(4px);

            z-index: 999999; display: flex; align-items: center; justify-content: center;

            opacity: 0; visibility: hidden; transition: 0.3s ease;

        }

        .st-vine-container .st-vine-overlay.active { opacity: 1; visibility: visible; }

        

        .st-vine-container .st-vine-modal-box {

            background: var(—bg-paper); width: 100%; max-width: 500px; border-radius: 12px; border: var(—border-sketch); box-shadow: 8px 8px 0 var(—ink-black);

            transform: translateY(20px) scale(0.95) rotate(-1deg); transition: 0.3s ease;

            max-height: 85vh; display: flex; flex-direction: column; font-family: Georgia, serif;

            background-image: linear-gradient(var(—ink-black) 1px, transparent 1px);

            background-size: 100% 30px; background-position: 0 50px;

        }

        .st-vine-container .st-vine-overlay.active .st-vine-modal-box { transform: translateY(0) scale(1) rotate(-1deg); }

        

        .st-vine-container .st-vine-modal-header {

            padding: 15px 24px; border-bottom: var(—border-sketch); display: flex; justify-content: space-between; align-items: center; background: var(—marker-yellow); border-radius: 10px 10px 0 0;

        }

        .st-vine-container .st-vine-modal-header h3 { margin: 0; font-size: 22px; color: var(—ink-black); font-family: var(—font-sketch); text-transform: uppercase; font-weight: bold; }

        

        .st-vine-container .st-vine-close { background: var(—marker-red); border: 2px solid var(—ink-black); width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 22px; font-weight: bold; cursor: pointer; color: #fff; padding: 0; box-shadow: 2px 2px 0 var(—ink-black); transition: 0.2s; }

        .st-vine-container .st-vine-close:hover { transform: scale(1.1) rotate(15deg); background: #ff3333; }

        

        .st-vine-container .st-vine-modal-body { padding: 15px 24px 25px; overflow-y: auto; }

        .st-vine-container .st-vine-modal-body::-webkit-scrollbar { width: 5px; }

        .st-vine-container .st-vine-modal-body::-webkit-scrollbar-thumb { background: var(—ink-black); border-radius: 10px; }

 

        .st-vine-container .st-modal-post-list { list-style: none; padding: 0; margin: 0; }

        .st-vine-container .st-modal-post-list li {

            padding: 12px 0; display: flex; flex-direction: column; gap: 4px; border-bottom: 2px dashed rgba(0,0,0,0.1);

        }

        .st-vine-container .st-modal-post-list li:last-child { border-bottom: none; }

        .st-vine-container .st-modal-post-list .st-post-date {

            font-size: 13px; color: #555; font-family: «Courier New», Courier, monospace; font-weight: bold;

        }

        .st-vine-container .st-modal-post-list a {

            color: var(—ink-black); text-decoration: none; font-weight: bold; font-size: 16px; transition: 0.2s; line-height: 1.4;

        }

        .st-vine-container .st-modal-post-list a:hover { color: #2a7f6e; padding-left: 5px; }

 

        @media (max-width: 768px) {

            .st-vine-container .st-vine-trunk { left: 20px; width: 3px; }

            .st-vine-container .st-vine-branch-container { width: 100%; left: 0 !important; padding-left: 50px !important; text-align: left !important; }

            .st-vine-container .st-side-left .st-branch-line, .st-vine-container .st-side-right .st-branch-line { left: 20px; width: calc(100% — 20px); }

            .st-vine-container .st-side-left .st-buds-row { justify-content: flex-start; flex-direction: row; }

            .st-vine-container .st-side-left .st-branch-title { transform: rotate(2deg); }

        }

    </style>

 

    <script>

        document.addEventListener(«DOMContentLoaded», function() {

            const comp = document.getElementById(«st-vine-comp»);

            const observer = new IntersectionObserver((entries) => {

                entries.forEach(entry => {

                    if (entry.isIntersecting) {

                        entry.target.classList.add(«st-grow»);

                        observer.unobserve(entry.target);

                    }

                });

            }, { threshold: 0.1 });

 

            comp.querySelectorAll(«.st-vine-branch-container»).forEach(branch => {

                observer.observe(branch);

            });

 

            const modal = comp.querySelector(«#st-vine-modal»);

            const btnClose = comp.querySelector(«#st-vine-close-btn»);

 

            comp.querySelectorAll(«.st-branch-end-icon»).forEach(btn => {

                btn.addEventListener(«click», function() {

                    const dataContainer = this.nextElementSibling;

                    const title = this.getAttribute(«data-cat»);

                    if (dataContainer && dataContainer.classList.contains(«st-hidden-data»)) {

                        comp.querySelector(«#st-vine-modal-title»).innerText = title;

                        comp.querySelector(«#st-vine-modal-body»).innerHTML = dataContainer.innerHTML;

                        modal.classList.add(«active»);

                        document.body.style.overflow = «hidden»;

                    }

                });

            });

 

            btnClose.addEventListener(«click», function() {

                modal.classList.remove(«active»);

                document.body.style.overflow = «»;

            });

 

            modal.addEventListener(«click», function(e) {

                if (e.target === modal) {

                    modal.classList.remove(«active»);

                    document.body.style.overflow = «»;

                }

            });

        });

    </script>’;

} 

 

// ==============================================================================

// 5. SUMMER MENU REST API (БЕЗОПАСНОСТЬ)

// ==============================================================================

add_action( ‘rest_api_init’, function () {

    register_rest_route( ‘summer-menu/v1’, ‘/get-user-data’, array(

        ‘methods’  => ‘POST’,

        ‘callback’ => ‘summer_menu_api_get_data’,

        ‘permission_callback’ => ‘__return_true’ 

    ));

 

    register_rest_route( ‘summer-menu/v1’, ‘/save-user-data’, array(

        ‘methods’  => ‘POST’,

        ‘callback’ => ‘summer_menu_api_save_data’,

        ‘permission_callback’ => ‘__return_true’

    ));

});

 

function summer_menu_api_save_data( $request ) {

    global $wpdb;

    $params = $request->get_json_params();

    

    $user_phone = isset($params[‘user_phone’]) ? sanitize_text_field($params[‘user_phone’]) : »;

    if (empty($user_phone)) {

        return rest_ensure_response( array( ‘success’ => false, ‘message’ => ‘Телефон не указан’ ) );

    }

 

    // Сохранение индивидуальных данных через REPLACE INTO в кастомную таблицу (исключает переполнение wp_options)

    $table = $wpdb->prefix . ‘st_sm_user_data’;

    $wpdb->replace( $table, [

        ‘phone’ => $user_phone,

        ‘checks’ => wp_json_encode($params[‘checks’] ?? []),

        ‘completed’ => wp_json_encode($params[‘completed’] ?? []),

        ‘notes’ => wp_json_encode($params[‘notes’] ?? []),

    ]);

 

    // Глобальные настройки (можно хранить в wp_options, так как это общие данные)

    if ( isset($params[‘basketsData’]) ) update_option( ‘sm_global_baskets’, wp_json_encode($params[‘basketsData’]) );

    if ( isset($params[‘weeksCalendar’]) ) update_option( ‘sm_global_calendar’, wp_json_encode($params[‘weeksCalendar’]) );

    if ( isset($params[‘fullRecipes’]) ) update_option( ‘sm_global_recipes’, wp_json_encode($params[‘fullRecipes’]) );

    if ( isset($params[‘dessertsList’]) ) update_option( ‘sm_global_desserts’, wp_json_encode($params[‘dessertsList’]) );

    if ( isset($params[‘globalInfo’]) ) update_option( ‘sm_global_info’, wp_json_encode($params[‘globalInfo’]) );

 

    return rest_ensure_response( array( ‘success’ => true, ‘message’ => ‘Дані успішно збережено в базі’ ) );

}

 

function summer_menu_api_get_data( $request ) {

    global $wpdb;

    $params = $request->get_json_params();

    $user_phone = isset($params[‘user_phone’]) ? sanitize_text_field($params[‘user_phone’]) : »;

 

    $table = $wpdb->prefix . ‘st_sm_user_data’;

    $user_data = $wpdb->get_row($wpdb->prepare(«SELECT checks, completed, notes FROM $table WHERE phone = %s», $user_phone), ARRAY_A);

 

    $response_data = array(

        ‘checks’    => $user_data && !empty($user_data[‘checks’]) ? $user_data[‘checks’] : ‘{}’,

        ‘completed’ => $user_data && !empty($user_data[‘completed’]) ? $user_data[‘completed’] : ‘{}’,

        ‘notes’     => $user_data && !empty($user_data[‘notes’]) ? $user_data[‘notes’] : ‘{}’,

        

        ‘basketsData’   => get_option( ‘sm_global_baskets’, null ),

        ‘weeksCalendar’ => get_option( ‘sm_global_calendar’, null ),

        ‘fullRecipes’   => get_option( ‘sm_global_recipes’, null ),

        ‘dessertsList’  => get_option( ‘sm_global_desserts’, null ),

        ‘globalInfo’    => get_option( ‘sm_global_info’, null ),

    );

 

    return rest_ensure_response( $response_data );

}

 

// ==============================================================================

// 6. МОДУЛЬ AL-ТИНДЕР: АНКЕТА ПОКУПАТЕЛЯ, ВАЙБЫ, РЕКОМЕНДАЦИИ (ОБНОВЛЕНО)

// ==============================================================================

 

function st_al_seed_vibes( $table ) {

    global $wpdb;

    $vibes = [

        // Положительные (20)

        [‘category’ => ‘Положительные’, ‘code’ => ‘П1’, ‘name’ => ‘Радость’, ’emoji’ => ‘😄’, ‘description’ => ‘Чувство удовольствия, веселья и наслаждения позитивными моментами.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П2’, ‘name’ => ‘Любовь’, ’emoji’ => ‘❤️’, ‘description’ => ‘Глубокая привязанность, нежность и забота о другом человеке.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П3’, ‘name’ => ‘Удовлетворение’, ’emoji’ => ‘😌’, ‘description’ => ‘Ощущение успеха, гармонии и довольства достигнутыми результатами.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П4’, ‘name’ => ‘Восхищение’, ’emoji’ => ‘😲’, ‘description’ => ‘Сильное удивление, восторг и вдохновение чем‑то или кем‑то.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П5’, ‘name’ => ‘Благодарность’, ’emoji’ => ‘🙏’, ‘description’ => ‘Чувство признательности за полученное, уважение и осознание ценности.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П6’, ‘name’ => ‘Умиротворение’, ’emoji’ => ‘🌿’, ‘description’ => ‘Внутреннее спокойствие, доверие к миру и ощущение стабильности.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П7’, ‘name’ => ‘Спокойствие’, ’emoji’ => ‘😊’, ‘description’ => ‘Отсутствие тревог, расслабленность и ощущение контроля над ситуацией.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П8’, ‘name’ => ‘Довольство’, ’emoji’ => ‘😋’, ‘description’ => ‘Приятные ощущения, наслаждение и блаженство от текущего момента.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П9’, ‘name’ => ‘Воодушевление’, ’emoji’ => ‘⚡’, ‘description’ => ‘Прилив энергии, желание действовать и мотивация к свершениям.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П10’, ‘name’ => ‘Вдохновение’, ’emoji’ => ‘✨’, ‘description’ => ‘Озарение, творческий подъём и стремление создавать новое.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П11’, ‘name’ => ‘Уверенность’, ’emoji’ => ‘💪’, ‘description’ => ‘Ощущение собственной силы, смелость и решительность.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П12’, ‘name’ => ‘Надежда’, ’emoji’ => ‘🌟’, ‘description’ => ‘Вера в лучшее будущее, позитивные ожидания и оптимистичный настрой.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П13’, ‘name’ => ‘Лёгкость’, ’emoji’ => ‘🍃’, ‘description’ => ‘Чувство свободы от напряжения, беззаботность и непринуждённость.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П14’, ‘name’ => ‘Свобода’, ’emoji’ => ‘🕊️’, ‘description’ => ‘Ощущение независимости, отсутствия ограничений и возможности выбора.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П15’, ‘name’ => ‘Азарт’, ’emoji’ => ‘🎲’, ‘description’ => ‘Стремление испытать свои силы, вызов и жажда победы.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П16’, ‘name’ => ‘Интерес’, ’emoji’ => ‘🔍’, ‘description’ => ‘Любопытство, желание исследовать и узнавать новое.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П17’, ‘name’ => ‘Преданность’, ’emoji’ => ‘🤝’, ‘description’ => ‘Верность, готовность поддерживать и неразрывная связь с близкими.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П18’, ‘name’ => ‘Дружелюбие’, ’emoji’ => ‘😊’, ‘description’ => ‘Доброжелательность, открытость и желание делиться радостью.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П19’, ‘name’ => ‘Принятие’, ’emoji’ => ‘🤗’, ‘description’ => ‘Смирение перед реальностью, согласие с собой и готовность отпускать.’],

        [‘category’ => ‘Положительные’, ‘code’ => ‘П20’, ‘name’ => ‘Оптимизм’, ’emoji’ => ‘🌈’, ‘description’ => ‘Уверенность в хорошем исходе, вера в себя и светлый взгляд в будущее.’],

 

        // Отрицательные (11)

        [‘category’ => ‘Отрицательные’, ‘code’ => ‘О1’, ‘name’ => ‘Страх’, ’emoji’ => ‘😨’, ‘description’ => ‘Опасение перед угрозой или неизвестностью, боязнь потерять контроль.’],

        [‘category’ => ‘Отрицательные’, ‘code’ => ‘О2’, ‘name’ => ‘Тревога’, ’emoji’ => ‘😟’, ‘description’ => ‘Напряжённое ожидание неприятностей, беспокойство о будущем.’],

        [‘category’ => ‘Отрицательные’, ‘code’ => ‘О3’, ‘name’ => ‘Паника’, ’emoji’ => ‘😱’, ‘description’ => ‘Крайняя тревога, хаос в мыслях и чувство полной беспомощности.’],

        [‘category’ => ‘Отрицательные’, ‘code’ => ‘О4’, ‘name’ => ‘Гнев’, ’emoji’ => ‘😠’, ‘description’ => ‘Сильное раздражение, возмущение несправедливостью и готовность к активному противостоянию.’],

        [‘category’ => ‘Отрицательные’, ‘code’ => ‘О5’, ‘name’ => ‘Обида’, ’emoji’ => ‘😤’, ‘description’ => ‘Чувство несправедливости, разочарование и душевная боль от действий других.’],

        [‘category’ => ‘Отрицательные’, ‘code’ => ‘О6’, ‘name’ => ‘Зависть’, ’emoji’ => ‘😒’, ‘description’ => ‘Недовольство чужим успехом, желание обладать тем, что есть у другого.’],

        [‘category’ => ‘Отрицательные’, ‘code’ => ‘О7’, ‘name’ => ‘Ревность’, ’emoji’ => ‘😖’, ‘description’ => ‘Страх потерять внимание или любовь, подозрительность и собственнические чувства.’],

        [‘category’ => ‘Отрицательные’, ‘code’ => ‘О8’, ‘name’ => ‘Стыд’, ’emoji’ => ‘😞’, ‘description’ => ‘Ощущение унижения, неловкости и опасение осуждения окружающими.’],

        [‘category’ => ‘Отрицательные’, ‘code’ => ‘О9’, ‘name’ => ‘Вина’, ’emoji’ => ‘😔’, ‘description’ => ‘Сожаление о совершённой ошибке, раскаяние и желание всё исправить.’],

        [‘category’ => ‘Отрицательные’, ‘code’ => ‘О10’, ‘name’ => ‘Презрение’, ’emoji’ => ‘🙄’, ‘description’ => ‘Чувство превосходства, высокомерие и отстранённость от тех, кого считают ниже.’],

        [‘category’ => ‘Отрицательные’, ‘code’ => ‘О11’, ‘name’ => ‘Ненависть’, ’emoji’ => ‘🤬’, ‘description’ => ‘Крайнее отторжение, враждебность и отвращение к кому‑либо.’],

 

        // Смешанные (7)

        [‘category’ => ‘Смешанные’, ‘code’ => ‘С1’, ‘name’ => ‘Смущение’, ’emoji’ => ‘😳’, ‘description’ => ‘Растерянность, неуверенность из‑за неожиданной или неловкой ситуации.’],

        [‘category’ => ‘Смешанные’, ‘code’ => ‘С2’, ‘name’ => ‘Неловкость’, ’emoji’ => ‘😬’, ‘description’ => ‘Дискомфорт в общении, стеснение и ощущение «не на своём месте».’],

        [‘category’ => ‘Смешанные’, ‘code’ => ‘С3’, ‘name’ => ‘Амбивалентность’, ’emoji’ => ‘🤷’, ‘description’ => ‘Одновременное существование противоположных чувств или желаний.’],

        [‘category’ => ‘Смешанные’, ‘code’ => ‘С4’, ‘name’ => ‘Шок’, ’emoji’ => ‘😧’, ‘description’ => ‘Оцепенение, внезапное потрясение и неспособность сразу осознать произошедшее.’],

        [‘category’ => ‘Смешанные’, ‘code’ => ‘С5’, ‘name’ => ‘Изумление’, ’emoji’ => ‘🤯’, ‘description’ => ‘Крайняя степень удивления, восхищённое потрясение от невероятного.’],

        [‘category’ => ‘Смешанные’, ‘code’ => ‘С6’, ‘name’ => ‘Противоречивость’, ’emoji’ => ‘⚖️’, ‘description’ => ‘Внутренний конфликт между разными оценками или решениями.’],

        [‘category’ => ‘Смешанные’, ‘code’ => ‘С7’, ‘name’ => ‘Колебание’, ’emoji’ => ‘🎢’, ‘description’ => ‘Неуверенность, сомнения и трудность сделать окончательный выбор.’],

    ];

 

    foreach ( $vibes as $v ) {

        $wpdb->insert( $table, $v );

    }

}

 

add_shortcode( ‘al_profile_setup’, ‘st_al_render_profile_form’ );

function st_al_render_profile_form() {

    if ( ! is_user_logged_in() ) {

        return st_get_auth_prompt_html(‘Чтобы заполнить анкету гурмана, находить идеальные заведения и свою компанию, вам необходимо войти в систему.’);

    }

 

    $current_user = wp_get_current_user();

    $user_email   = $current_user->user_email;

    $user_nickname = $current_user->display_name ?: $current_user->user_login;

 

    ob_start();

    ?>

    <style>

    /* УБИРАЕМ АДМИН ПАНЕЛЬ WP */

    #wpadminbar { display: none !important; }

    html, body { margin-top: 0 !important; height: 100%; }

 

    .st-al-container {

        —primary: #1B2A3B; —secondary: #D4AF37; —accent-green: #2F5A3C;

        —light-bg: #FDFBF7; —border: #E0D8C8; —text: #1B2A3B;

        —text-light: #6B7280; —text-on-dark: #FDFBF7; —success: #2F5A3C; —error: #B23B2E;

        —transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);

        —radius: 16px; —radius-sm: 10px; —radius-xs: 6px;

        —serif: «Playfair Display», «Times New Roman», serif;

        —sans: «Lato», «Helvetica Neue», sans-serif;

        —sidebar-width: 80px; —sidebar-expanded: 280px;

    }

 

    .st-al-container .st-al-layout {

        display: flex; min-height: 100vh;

        background: var(—light-bg); font-family: var(—sans); margin: 0; padding: 0; position: relative;

    }

 

    /* САЙДБАР */

    .st-al-container .st-al-sidebar {

        width: var(—sidebar-width); background: linear-gradient(180deg, #1B2A3B 0%, #0F1923 100%);

        border-right: 1px solid rgba(212, 175, 55, 0.2); transition: width 0.4s ease;

        display: flex; flex-direction: column; white-space: nowrap;

        position: fixed; top: 0; left: 0; height: 100vh; z-index: 1000;

    }

    .st-al-container .st-al-sidebar:hover, .st-al-container .st-al-sidebar.expanded { width: var(—sidebar-expanded); }

 

    .st-al-sidebar-header {

        padding: 1.5rem 1rem; display: flex; align-items: center; color: var(—secondary);

        font-family: var(—serif); font-size: 1.1rem; font-weight: 700; gap: 1rem;

    }

    .st-al-logo-icon { min-width: 42px; height: 42px; display: flex; justify-content: center; align-items: center; background: rgba(212, 175, 55, 0.1); border-radius: 12px; font-size: 1.5rem; }

    .st-al-logo-text { opacity: 0; transition: 0.3s; }

    .st-al-sidebar:hover .st-al-logo-text, .st-al-sidebar.expanded .st-al-logo-text { opacity: 1; }

 

    .st-al-sidebar-nav { flex: 1; display: flex; flex-direction: column; padding: 1rem 0.6rem; gap: 0.5rem; }

    .st-al-nav-item {

        display: flex; align-items: center; padding: 0.8rem; color: rgba(253, 251, 247, 0.7);

        text-decoration: none; border-radius: var(—radius-sm); transition: 0.3s; cursor: pointer;

    }

    .st-al-nav-item:hover, .st-al-nav-item.active { background: rgba(212, 175, 55, 0.1); color: var(—secondary); }

    .st-al-nav-icon { min-width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; }

    .st-al-nav-text { margin-left: 10px; opacity: 0; transition: 0.3s; font-weight: 500;}

    .st-al-sidebar:hover .st-al-nav-text, .st-al-sidebar.expanded .st-al-nav-text { opacity: 1; }

 

    /* МЕНЮ ПОЛЬЗОВАТЕЛЯ В САЙДБАРЕ ВНИЗУ */

    .st-al-user-bottom {

        padding: 1rem; border-top: 1px solid rgba(255,255,255,0.05); position: relative;

    }

    .st-user-trigger {

        display: flex; align-items: center; gap: 12px; cursor: pointer; color: #fff;

        padding: 8px; border-radius: 10px; transition: 0.3s;

    }

    .st-user-trigger:hover { background: rgba(255,255,255,0.05); }

    .st-user-avatar { min-width: 40px; height: 40px; background: var(—secondary); color: #1B2A3B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-family: var(—serif);}

    .st-user-details { opacity: 0; transition: 0.3s; overflow: hidden; display: flex; flex-direction: column; }

    .st-user-details strong { font-size: 14px; }

    .st-user-details small { font-size: 11px; color: rgba(255,255,255,0.5); }

    .st-al-sidebar:hover .st-user-details, .st-al-sidebar.expanded .st-user-details { opacity: 1; }

 

    .st-user-popup {

        position: absolute; bottom: 80px; left: 10px; width: calc(var(—sidebar-expanded) — 20px);

        background: #fff; border-radius: 12px; padding: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.3);

        opacity: 0; visibility: hidden; transform: translateY(10px); transition: 0.3s; z-index: 1001;

    }

    .st-user-popup.active { opacity: 1; visibility: visible; transform: translateY(0); }

    .st-user-popup a { display: block; padding: 10px; background: #f5f5f5; color: var(—error); text-align: center; border-radius: 8px; text-decoration: none; font-weight: bold; margin-top: 10px; }

    .st-user-popup a:hover { background: #fee; }

 

    /* РАБОЧАЯ ПЛОЩАДЬ И ШАПКА */

    .st-al-main-content {

        flex: 1; margin-left: var(—sidebar-width); width: calc(100% — var(—sidebar-width));

        display: flex; flex-direction: column; justify-content: flex-start; align-items: center;

        transition: margin-left 0.4s ease, width 0.4s ease; min-height: 100vh; padding-bottom: 50px;

    }

    

    .st-al-top-header {

        position: sticky; top: 0; width: 100%; background: rgba(253, 251, 247, 0.95);

        backdrop-filter: blur(10px); z-index: 100; padding: 15px 30px;

        box-shadow: 0 4px 20px rgba(0,0,0,0.03); border-bottom: 1px solid var(—border);

    }

    .st-al-progress-bar { background: #E5E0D5; border-radius: 20px; height: 6px; width: 100%; overflow: hidden; }

    .st-al-progress-fill { background: linear-gradient(90deg, #D4AF37, #F5D76E); width: 0%; height: 100%; transition: width 0.4s ease; }

    .st-al-progress-steps { display: flex; justify-content: space-between; margin-top: 10px; font-size: 12px; font-weight: 600; color: #888; }

    .st-al-progress-steps span.active { color: var(—primary); }

    .st-al-progress-steps span.completed { color: var(—secondary); }

 

    .st-al-modern {

        max-width: 850px; width: 100%; margin: 30px auto; background: #fff;

        border-radius: var(—radius); box-shadow: 0 10px 40px rgba(0,0,0,0.05); border: 1px solid var(—border);

    }

 

    .st-al-step-pane { display: none; padding: 40px; animation: paneFadeIn 0.4s ease; }

    .st-al-step-pane.active { display: block; }

    @keyframes paneFadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }

 

    .st-al-field { margin-bottom: 25px; }

    .st-al-label { display: block; font-family: var(—serif); font-weight: 700; font-size: 16px; margin-bottom: 10px; color: var(—primary); }

    

    .st-al-input, .st-al-textarea {

        width: 100%; padding: 12px 15px; border: 2px solid #E5E0D5; border-radius: var(—radius-sm);

        font-size: 15px; background: #fff; transition: 0.3s; box-sizing: border-box; font-family: var(—sans);

    }

    .st-al-input:focus, .st-al-textarea:focus { border-color: var(—secondary); outline: none; box-shadow: 0 4px 15px rgba(212,175,55,0.1); }

 

    /* РАДИО КНОПКИ ВМЕСТО SELECT И ЧЕКБОКСЫ */

    .st-btn-group { display: flex; flex-wrap: wrap; gap: 10px; }

    .st-btn-group label { cursor: pointer; user-select: none; }

    .st-btn-group input[type=»radio»], .st-btn-group input[type=»checkbox»] { display: none; }

    .st-btn-group span {

        display: inline-block; padding: 10px 18px; background: #fff; border: 2px solid #E5E0D5;

        border-radius: 30px; font-size: 14px; font-weight: 500; color: var(—text); transition: 0.2s;

        box-shadow: 0 0 0 1px transparent;

    }

    .st-btn-group label:hover span { border-color: #D4C4A8; transform: translateY(-2px); box-shadow: 0 4px 10px rgba(0,0,0,0.05), 0 0 0 2px rgba(212, 175, 55, 0.4); }

    .st-btn-group input:checked + span { background: var(—secondary); border-color: var(—secondary); color: #fff; box-shadow: 0 4px 15px rgba(212,175,55,0.3), 0 0 0 3px rgba(212, 175, 55, 0.6); }

 

    /* ВАЙБ КНОПКИ (СЕТКА ПО 2 В РЯД) */

    .st-vibe-grid {

        display: grid;

        grid-template-columns: repeat(2, 1fr);

        gap: 8px;

    }

    .st-vibe-btn {

        width: 100%;

        background: #fff; border: 1px solid #E5E0D5; border-radius: 20px;

        padding: 6px 14px; font-size: 13px; font-weight: 600; cursor: pointer; transition: 0.2s;

        display: flex; align-items: center; gap: 6px; box-shadow: 0 2px 5px rgba(0,0,0,0.02);

        justify-content: center;

    }

    .st-vibe-btn:hover { border-color: var(—secondary); transform: translateY(-2px); }

    .st-vibe-btn.selected { background: var(—primary); color: #fff; border-color: var(—primary); }

    .st-vibe-btn.selected .vibe-emoji { transform: scale(1.2); }

 

    /* МОДАЛКИ (ОБЩИЙ СТИЛЬ) */

    .st-modal-overlay {

        position: fixed; top: 0; left: 0; width: 100%; height: 100%;

        background: rgba(0,0,0,0.6); backdrop-filter: blur(5px);

        display: flex; align-items: center; justify-content: center;

        z-index: 9999; opacity: 0; visibility: hidden; transition: 0.3s;

    }

    .st-modal-overlay.active { opacity: 1; visibility: visible; }

    .st-modal-box {

        background: #fff; width: 90%; max-width: 450px; border-radius: 24px;

        padding: 30px; position: relative; transform: translateY(20px); transition: 0.3s; box-shadow: 0 20px 50px rgba(0,0,0,0.2);

    }

    .st-modal-overlay.active .st-modal-box { transform: translateY(0); }

    .st-modal-close { position: absolute; top: 15px; right: 20px; font-size: 24px; cursor: pointer; color: #888; }

 

    /* ФИНАЛЬНОЕ МОДАЛЬНОЕ ОКНО СОХРАНЕНИЯ */

    .st-success-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 20px; }

    .st-nav-card {

        background: #fdfcf8; border: 1px solid #E5E0D5; border-radius: 12px; padding: 15px;

        text-align: center; font-weight: bold; cursor: pointer; transition: 0.2s; color: var(—primary);

    }

    .st-nav-card:hover { border-color: var(—secondary); background: #fff; transform: translateY(-3px); box-shadow: 0 5px 15px rgba(212,175,55,0.1); }

    

    .st-referral-block { margin-top: 30px; border-top: 1px dashed #E5E0D5; padding-top: 20px; text-align: center; }

    .st-referral-block h3 { font-family: var(—serif); font-size: 18px; margin: 0 0 15px 0; }

    .st-icons-row { display: flex; justify-content: center; gap: 20px; }

    .st-icon-btn { display: flex; flex-direction: column; align-items: center; gap: 8px; font-size: 12px; color: #555; cursor: pointer; }

    .st-icon-btn .icon { width: 50px; height: 50px; border-radius: 50%; background: #f0f0f0; display: flex; align-items: center; justify-content: center; font-size: 20px; transition: 0.2s; }

    .st-icon-btn:hover .icon { background: var(—secondary); color: #fff; transform: scale(1.1); box-shadow: 0 5px 15px rgba(212,175,55,0.3); }

 

    .st-al-buttons { display: flex; justify-content: space-between; margin-top: 20px; padding-top: 20px; border-top: 1px solid #E5E0D5; }

    .st-al-btn { padding: 12px 25px; border-radius: 30px; font-weight: 600; border: none; cursor: pointer; transition: 0.3s; font-size: 15px; }

    .st-btn-prev { background: #f0f0f0; color: #333; }

    .st-btn-next, .st-btn-submit { background: var(—secondary); color: #fff; box-shadow: 0 4px 15px rgba(212,175,55,0.3); }

    .st-btn-next:hover, .st-btn-submit:hover { transform: translateY(-2px); background: #C9A22E; }

    /* Чёрный текст на кнопке подтверждения вайба */

    #vibe-modal-confirm { color: #000 !important; }

 

    /* ФОТО ЗАГРУЗКА */

    .st-al-photos-preview { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }

    .st-al-photo-item { position: relative; width: 80px; height: 80px; border-radius: 10px; overflow: hidden; border: 2px solid #E5E0D5; }

    .st-al-photo-item img { width: 100%; height: 100%; object-fit: cover; }

    .st-al-photo-remove { position: absolute; top: 0; right: 0; background: #B23B2E; color: #fff; border: none; width: 20px; height: 20px; font-size: 12px; cursor: pointer; }

 

    /* Разделители между вопросами */

    .st-al-field:not(:last-child) {

        border-bottom: 1px dashed #E0D8C8;

        padding-bottom: 20px;

        margin-bottom: 20px;

    }

 

    /* Адаптивность кнопок-чекбоксов (3 в ряд на мобильных) */

    @media (max-width: 768px) {

        .st-btn-group {

            display: grid;

            grid-template-columns: repeat(3, 1fr);

            gap: 8px;

        }

        .st-btn-group span {

            text-align: center;

            font-size: 13px;

            padding: 8px 5px;

        }

        .st-al-sidebar { width: 0; transform: translateX(-100%); }

        .st-al-sidebar.expanded { width: 280px; transform: translateX(0); }

        .st-al-main-content { margin-left: 0; width: 100%; }

        .st-al-top-header { padding: 15px; }

        .st-al-modern { margin: 15px auto; width: 95%; }

        .st-al-step-pane { padding: 20px; }

    }

    </style>

 

    <div class=»st-al-container» id=»st-al-container»>

        <div class=»st-al-layout»>

            

            <nav class=»st-al-sidebar» id=»stSidebar»>

                <div class=»st-al-sidebar-header»>

                    <div class=»st-al-logo-icon»>⚓</div>

                    <span class=»st-al-logo-text»>SPIRIT TRADE</span>

                </div>

                <div class=»st-al-sidebar-nav»>

                    <a class=»st-al-nav-item active» data-step=»1″><div class=»st-al-nav-icon»>📝</div><span class=»st-al-nav-text»>Основное</span></a>

                    <a class=»st-al-nav-item» data-step=»2″><div class=»st-al-nav-icon»>🍷</div><span class=»st-al-nav-text»>Напитки</span></a>

                    <a class=»st-al-nav-item» data-step=»3″><div class=»st-al-nav-icon»>🧭</div><span class=»st-al-nav-text»>Привычки</span></a>

                    <a class=»st-al-nav-item» data-step=»4″><div class=»st-al-nav-icon»>✨</div><span class=»st-al-nav-text»>Интересы</span></a>

                    <a class=»st-al-nav-item» data-step=»5″><div class=»st-al-nav-icon»>🍃</div><span class=»st-al-nav-text»>Вайб</span></a>

                </div>

                <div class=»st-al-user-bottom»>

                    <div class=»st-user-trigger» id=»stUserTrigger»>

                        <div class=»st-user-avatar»><?php echo mb_substr($user_nickname, 0, 1); ?></div>

                        <div class=»st-user-details»>

                            <strong><?php echo esc_html($user_nickname); ?></strong>

                            <small>Настройки</small>

                        </div>

                    </div>

                    <div class=»st-user-popup» id=»stUserPopup»>

                        <div style=»font-family: ‘Playfair Display’, serif; font-size:18px; font-weight:bold; margin-bottom:5px;»><?php echo esc_html($user_nickname); ?></div>

                        <div style=»font-size:12px; color:#888; margin-bottom:15px;»><?php echo esc_html($user_email); ?></div>

                        <a href=»<?php echo wp_logout_url(home_url()); ?>»>🚪 Выйти из аккаунта</a>

                    </div>

                </div>

            </nav>

 

            <main class=»st-al-main-content»>

                <div class=»st-al-top-header»>

                    <div class=»st-al-progress-bar»><div class=»st-al-progress-fill» id=»st-al-progress-fill»></div></div>

                    <div class=»st-al-progress-steps» id=»st-al-progress-steps»>

                        <span class=»active»>1. Основное</span><span>2. Напитки</span><span>3. Привычки</span><span>4. Интересы</span><span>5. Вайб</span>

                    </div>

                </div>

 

                <div class=»st-al-modern» id=»st-al-profile-container»>

                    <form id=»st-al-profile-form»>

                        <?php wp_nonce_field( ‘st_al_nonce_action’, ‘st_al_nonce’ ); ?>

                        <input type=»hidden» name=»action» value=»st_al_save_full_profile»>

 

                        <div class=»st-al-step-pane active» data-step=»1″>

                            <h2 style=»font-family: var(—serif); margin-top:0;»>Основная информация</h2>

                            <div class=»st-al-field»>

                                <label class=»st-al-label»>Никнейм</label>

                                <input type=»text» name=»nickname» class=»st-al-input» value=»<?php echo esc_attr( $user_nickname ); ?>» placeholder=»Ваш псевдоним»>

                            </div>

                            <div class=»st-al-field»>

                                <label class=»st-al-label»>Возраст</label>

                                <input type=»number» name=»age» class=»st-al-input» placeholder=»18+»>

                            </div>

                            <div class=»st-al-field»>

                                <label class=»st-al-label»>Уровень вовлеченности</label>

                                <div class=»st-btn-group»>

                                    <label><input type=»radio» name=»engagement_level» value=»Новичок»><span>🍼 Новичок</span></label>

                                    <label><input type=»radio» name=»engagement_level» value=»Энтузиаст»><span>🔥 Энтузиаст</span></label>

                                    <label><input type=»radio» name=»engagement_level» value=»Критик»><span>🧐 Критик</span></label>

                                </div>

                            </div>

                            <div class=»st-al-field»>

                                <label class=»st-al-label»>О себе</label>

                                <textarea name=»bio» class=»st-al-textarea» placeholder=»Пару слов о ваших вкусах…»></textarea>

                            </div>

                            <div class=»st-al-field»>

                                <label class=»st-al-label»>Цели на платформе</label>

                                <div class=»st-btn-group»>

                                    <label><input type=»radio» name=»platform_goal» value=»Поиск компании»><span>👥 Поиск компании</span></label>

                                    <label><input type=»radio» name=»platform_goal» value=»Дегустации»><span>🍷 Дегустации</span></label>

                                    <label><input type=»radio» name=»platform_goal» value=»Новые знакомства»><span>💬 Новые знакомства</span></label>

                                    <label><input type=»radio» name=»platform_goal» value=»Обучение»><span>📚 Обучение</span></label>

                                </div>

                            </div>

                            <div class=»st-al-field»>

                                <label class=»st-al-label»>Языки</label>

                                <div class=»st-btn-group»>

                                    <?php

                                    foreach ( [‘Русский’, ‘English’, ‘Қазақша’, ‘Deutsch’, ‘Français’] as $lang ) {

                                        echo ‘<label><input type=»checkbox» name=»languages[]» value=»‘.esc_attr($lang).’»><span>’.esc_html($lang).'</span></label>’;

                                    }

                                    ?>

                                </div>

                            </div>

                        </div>

 

                        <div class=»st-al-step-pane» data-step=»2″>

                            <h2 style=»font-family: var(—serif); margin-top:0;»>Напитки</h2>

                            <div class=»st-al-field»>

                                <label class=»st-al-label»>🥃 Крепкий алкоголь</label>

                                <div class=»st-btn-group»>

                                    <?php

                                    foreach ( [‘Виски’, ‘Водка’, ‘Джин’, ‘Ром’, ‘Текила’, ‘Мескаль’, ‘Коньяк’, ‘Арманьяк’, ‘Бренди’, ‘Ликеры’, ‘Настойки’] as $s ) {

                                        echo ‘<label><input type=»checkbox» name=»spirits[]» value=»‘.esc_attr($s).’»><span>’.esc_html($s).'</span></label>’;

                                    }

                                    ?>

                                </div>

                            </div>

                            <div class=»st-al-field»>

                                <label class=»st-al-label»>🍷 Вино</label>

                                <div class=»st-btn-group»>

                                    <?php

                                    foreach ( [‘Тихие вина’, ‘Игристые вина’, ‘Крепленые вина’, ‘Вермуты’] as $w ) {

                                        echo ‘<label><input type=»checkbox» name=»wine[]» value=»‘.esc_attr($w).’»><span>’.esc_html($w).'</span></label>’;

                                    }

                                    ?>

                                </div>

                            </div>

                            <div class=»st-al-field»>

                                <label class=»st-al-label»>🍺 Пиво и сидр</label>

                                <div class=»st-btn-group»>

                                    <?php

                                    foreach ( [‘Пиво’, ‘Сидр’] as $b ) {

                                        echo ‘<label><input type=»checkbox» name=»beer_cider[]» value=»‘.esc_attr($b).’»><span>’.esc_html($b).'</span></label>’;

                                    }

                                    ?>

                                </div>

                            </div>

                            <div class=»st-al-field»>

                                <label class=»st-al-label»>🍶 Ферментированные</label>

                                <div class=»st-btn-group»>

                                    <label><input type=»checkbox» name=»fermented[]» value=»Саке»><span>Саке</span></label>

                                </div>

                            </div>

                        </div>

 

                        <div class=»st-al-step-pane» data-step=»3″>

                            <h2 style=»font-family: var(—serif); margin-top:0;»>Привычки и Лайфстайл</h2>

                            <?php

                            $lifestyle_fields = [

                                ‘favorite_time’     => [‘Любимое время суток’, [‘Утро’, ‘День’, ‘Вечер’, ‘Ночь’]],

                                ‘occupation’        => [‘Сфера деятельности’, [‘IT’, ‘Маркетинг’, ‘Искусство’, ‘Образование’, ‘Медицина’, ‘Бизнес’, ‘Другое’]],

                                ‘event_format’      => [‘Отношение к ивентам’, [‘Люблю большие фестивали’, ‘Предпочитаю камерные вечеринки’, ‘Нейтрален’]],

                                ‘outing_frequency’  => [‘Частота выходов’, [‘Несколько раз в неделю’, ‘Раз в неделю’, ‘Пару раз в месяц’, ‘Редко’]],

                                ‘venue_choice’      => [‘Выбор заведения’, [‘Изучаю отзывы’, ‘Иду по рекомендации’, ‘Спонтанно’, ‘Только проверенные места’]],

                                ‘company_pref’      => [‘Компания’, [‘Соло’, ‘Пара’, ‘Группа друзей’, ‘Коллеги’]],

                                ‘venue_atmosphere’  => [‘Атмосфера’, [‘Уютный паб’, ‘Клуб’, ‘Лаунж’, ‘Терраса’, ‘Арт-пространство’, ‘Спортивный бар’]],

                                ‘comm_style’        => [‘Стиль общения’, [‘Активный’, ‘Спокойный’, ‘Интроверт’, ‘Экстраверт’]],

                                ‘rating_style’      => [‘Оценка заведений’, [‘Ставлю звёзды’, ‘Пишу развёрнутые отзывы’, ‘Не оцениваю’]],

                                ‘explorer_type’     => [‘Тип исследователя’, [‘Искатель новинок’, ‘Консерватор’, ‘Смешанный’]],

                            ];

                            foreach ( $lifestyle_fields as $field => $data ) {

                                list( $label, $options ) = $data;

                                echo ‘<div class=»st-al-field»><label class=»st-al-label»>’ . esc_html($label) . ‘</label>’;

                                echo ‘<div class=»st-btn-group»>’;

                                foreach ( $options as $opt ) {

                                    echo ‘<label><input type=»radio» name=»‘.$field.’» value=»‘.esc_attr($opt).’»><span>’.esc_html($opt).'</span></label>’;

                                }

                                echo ‘</div></div>’;

                            }

                            ?>

                        </div>

 

                        <div class=»st-al-step-pane» data-step=»4″>

                            <h2 style=»font-family: var(—serif); margin-top:0;»>Интересы и Медиа</h2>

                            <div class=»st-al-field»>

                                <label class=»st-al-label»>Теги увлечений (через запятую)</label>

                                <input type=»text» name=»interest_tags» class=»st-al-input» placeholder=»виски, джаз, путешествия»>

                            </div>

                            <div class=»st-al-field»>

                                <label class=»st-al-label»>Социальные сети</label>

                                <div style=»display:flex; flex-direction:column; gap:10px;»>

                                    <input type=»text» name=»instagram» class=»st-al-input» placeholder=»Instagram (@username)»>

                                    <input type=»text» name=»untappd» class=»st-al-input» placeholder=»Untappd профиль»>

                                    <input type=»text» name=»vivino» class=»st-al-input» placeholder=»Vivino профиль»>

                                    <input type=»url» name=»spotify_anthem» class=»st-al-input» placeholder=»Spotify трек (URL)»>

                                </div>

                            </div>

                            <div class=»st-al-field»>

    <label class=»st-al-label»>Фото профиля (до 9 шт.)</label>

    <div id=»st-al-photos-preview» class=»st-al-photos-preview»></div>

    <input type=»file» id=»st-al-upload-photo-input» accept=»image/jpeg,image/png,image/gif,image/webp» multiple style=»display:none;»>

    <button type=»button» id=»st-al-upload-photo-btn» class=»st-al-btn» style=»background:#f0f0f0; border:1px solid #ccc;»>➕ Загрузить фото</button>

    <input type=»hidden» name=»photos» id=»st-al-photos-input» value=»»>

</div>

                        </div>

 

                        <div class=»st-al-step-pane» data-step=»5″>

                            <h2 style=»font-family: var(—serif); margin-top:0;»>Текущий Вайб</h2>

                            <p style=»color:#666; font-size:14px; margin-bottom:20px;»>Нажмите на вайб, чтобы прочитать описание и установить его.</p>

                            

                            <div class=»st-al-field»>

                                <div id=»st-al-vibe-grid» class=»st-vibe-grid»></div>

                                <input type=»hidden» name=»current_vibe_id» id=»st-al-current-vibe-id» value=»»>

                            </div>

                        </div>

 

                        <div class=»st-al-buttons»>

                            <button type=»button» class=»st-al-btn st-btn-prev» id=»st_al_prev_btn» disabled>← Назад</button>

                            <button type=»button» class=»st-al-btn st-btn-next» id=»st_al_next_btn»>Далее →</button>

                            <button type=»submit» class=»st-al-btn st-btn-submit» id=»st_al_save_btn» style=»display:none;»>Сохранить анкету</button>

                        </div>

                    </form>

                </div>

            </main>

        </div>

    </div>

 

    <div class=»st-modal-overlay» id=»st-vibe-modal»>

        <div class=»st-modal-box»>

            <span class=»st-modal-close» onclick=»document.getElementById(‘st-vibe-modal’).classList.remove(‘active’)»>&times;</span>

            <div style=»text-align:center;»>

                <div id=»vibe-modal-emoji» style=»font-size:50px; margin-bottom:10px;»></div>

                <h3 id=»vibe-modal-name» style=»font-family: var(—serif); font-size:24px; margin:0 0 10px 0;»></h3>

                <p id=»vibe-modal-desc» style=»color:#555; font-size:15px; line-height:1.5; margin-bottom:25px;»></p>

                <button type=»button» id=»vibe-modal-confirm» class=»st-al-btn st-btn-submit» style=»width:100%;»>Выбрать этот вайб</button>

            </div>

        </div>

    </div>

 

    <div class=»st-modal-overlay» id=»st-success-modal»>

        <div class=»st-modal-box» style=»max-width: 500px;»>

            <span class=»st-modal-close» onclick=»document.getElementById(‘st-success-modal’).classList.remove(‘active’)»>&times;</span>

            <h2 style=»font-family: var(—serif); color:var(—primary); text-align:center; margin-top:0;»>Анкета обновлена! 🎉</h2>

            <p style=»text-align:center; color:#666; font-size:14px;»>Ваш профиль готов к работе. Что дальше?</p>

            

            <div class=»st-success-grid»>

                <div class=»st-nav-card»>🏠 Кабинет</div>

                <div class=»st-nav-card»>👥 Люди</div>

                <div class=»st-nav-card»>📍 Локации</div>

                <div class=»st-nav-card»>🎉 Ивенты</div>

            </div>

 

            <div class=»st-referral-block»>

                <h3>Реферальная программа</h3>

                <div class=»st-icons-row»>

                    <div class=»st-icon-btn»>

                        <div class=»icon»>👤</div>

                        <span>Пригласить<br>человека</span>

                    </div>

                    <div class=»st-icon-btn»>

                        <div class=»icon»>🏪</div>

                        <span>Подключить<br>заведение</span>

                    </div>

                    <div class=»st-icon-btn»>

                        <div class=»icon»>📅</div>

                        <span>Создать<br>ивент</span>

                    </div>

                </div>

            </div>

        </div>

    </div>

 

    <div class=»st-modal-overlay» id=»st-info-modal»>

        <div class=»st-modal-box» style=»max-width: 450px;»>

            <span class=»st-modal-close» onclick=»document.getElementById(‘st-info-modal’).classList.remove(‘active’)»>&times;</span>

            <h2 id=»info-modal-title» style=»font-family: var(—serif); color: var(—primary); margin-top:0;»></h2>

            <p id=»info-modal-desc» style=»color:#555; font-size:15px; line-height:1.5;»></p>

        </div>

    </div>

 

    <script>

    (function($) {

        const wrap = document.getElementById(‘st-al-container’);

        if(!wrap) return;

 

        let currentStep = 1;

        const totalSteps = 5;

        

 

        // Управление шагами

        function goToStep(step) {

            if (step === currentStep || step < 1 || step > totalSteps) return;

            wrap.querySelector(`.st-al-step-pane[data-step=»${currentStep}»]`).classList.remove(‘active’);

            wrap.querySelector(`.st-al-step-pane[data-step=»${step}»]`).classList.add(‘active’);

            currentStep = step;

            updateStepUI();

        }

 

        function updateProgress() {

            const percent = ((currentStep — 1) / (totalSteps — 1)) * 100;

            wrap.querySelector(‘#st-al-progress-fill’).style.width = percent + ‘%’;

            

            const steps = wrap.querySelectorAll(‘#st-al-progress-steps span’);

            steps.forEach((span, index) => {

                span.className = »;

                if (index < currentStep — 1) span.classList.add(‘completed’);

                else if (index === currentStep — 1) span.classList.add(‘active’);

            });

        }

 

        function updateStepUI() {

            const prevBtn = wrap.querySelector(‘#st_al_prev_btn’);

            const nextBtn = wrap.querySelector(‘#st_al_next_btn’);

            const saveBtn = wrap.querySelector(‘#st_al_save_btn’);

 

            prevBtn.disabled = currentStep === 1;

            if (currentStep === totalSteps) {

                nextBtn.style.display = ‘none’;

                saveBtn.style.display = ‘inline-block’;

            } else {

                nextBtn.style.display = ‘inline-block’;

                saveBtn.style.display = ‘none’;

            }

            updateProgress();

            window.scrollTo({ top: 0, behavior: ‘smooth’ });

        }

 

        wrap.querySelector(‘#st_al_next_btn’)?.addEventListener(‘click’, () => goToStep(currentStep + 1));

        wrap.querySelector(‘#st_al_prev_btn’)?.addEventListener(‘click’, () => goToStep(currentStep — 1));

 

        // Вызов меню пользователя (внизу слева)

        wrap.querySelector(‘#stUserTrigger’)?.addEventListener(‘click’, (e) => {

            e.stopPropagation();

            wrap.querySelector(‘#stUserPopup’).classList.toggle(‘active’);

        });

        document.addEventListener(‘click’, () => wrap.querySelector(‘#stUserPopup’)?.classList.remove(‘active’));

 

        // Фотографии

        // Фотографии – прямой загрузчик (без медиабиблиотеки)

let uploadedPhotos = [];

 

function updatePhotosPreview() {

    const preview = wrap.querySelector(‘#st-al-photos-preview’);

    preview.innerHTML = »;

    uploadedPhotos.forEach((url, idx) => {

        const item = document.createElement(‘div’);

        item.className = ‘st-al-photo-item’;

        item.innerHTML = `<img src=»${url}»><button type=»button» class=»st-al-photo-remove» data-idx=»${idx}»>×</button>`;

        preview.appendChild(item);

    });

    wrap.querySelector(‘#st-al-photos-input’).value = JSON.stringify(uploadedPhotos);

    const btnUpload = wrap.querySelector(‘#st-al-upload-photo-btn’);

    if (btnUpload) btnUpload.style.display = uploadedPhotos.length >= 9 ? ‘none’ : ‘inline-block’;

}

 

function uploadPhoto(file) {

    return new Promise((resolve, reject) => {

        const formData = new FormData();

        formData.append(‘action’, ‘st_upload_user_photo’);

        formData.append(‘nonce’, ‘<?php echo wp_create_nonce(‘st_al_nonce_action’); ?>’);

        formData.append(‘photo’, file);

 

        fetch(‘<?php echo admin_url(‘admin-ajax.php’); ?>’, {

            method: ‘POST’,

            body: formData

        })

        .then(response => response.json())

        .then(res => {

            if (res.success) {

                resolve(res.data.url);

            } else {

                reject(res.data.message || ‘Ошибка загрузки’);

            }

        })

        .catch(reject);

    });

}

 

// Обработчик выбора файлов

wrap.querySelector(‘#st-al-upload-photo-input’)?.addEventListener(‘change’, async function(e) {

    const files = Array.from(e.target.files);

    if (uploadedPhotos.length + files.length > 9) {

        alert(‘Можно загрузить не более 9 фото’);

        return;

    }

    

    const btn = wrap.querySelector(‘#st-al-upload-photo-btn’);

    const originalText = btn.innerText;

    btn.innerText = ‘Загрузка…’;

    btn.disabled = true;

 

    for (const file of files) {

        try {

            const url = await uploadPhoto(file);

            uploadedPhotos.push(url);

            updatePhotosPreview();

        } catch (err) {

            alert(‘Ошибка загрузки: ‘ + err);

        }

    }

    

    btn.innerText = originalText;

    btn.disabled = false;

    e.target.value = »;

});

 

// Кнопка открывает диалог выбора файлов

wrap.querySelector(‘#st-al-upload-photo-btn’)?.addEventListener(‘click’, function(e) {

    e.preventDefault();

    if (uploadedPhotos.length >= 9) return;

    wrap.querySelector(‘#st-al-upload-photo-input’).click();

});

 

// Удаление фото

wrap.querySelector(‘#st-al-photos-preview’)?.addEventListener(‘click’, function(e) {

    if (e.target.classList.contains(‘st-al-photo-remove’)) {

        const idx = parseInt(e.target.getAttribute(‘data-idx’));

        uploadedPhotos.splice(idx, 1);

        updatePhotosPreview();

    }

});

 

        // Загрузка Вайбов

        function loadVibesAndRender() {

            $.ajax({

                url: ‘<?php echo admin_url(‘admin-ajax.php’); ?>’,

                type: ‘POST’,

                data: {

                    action: ‘st_al_get_vibes_list’,

                    nonce: ‘<?php echo wp_create_nonce(‘st_al_nonce_action’); ?>’

                },

                dataType: ‘json’,

                success: function(res) {

                    if (res.success) {

                        const container = wrap.querySelector(‘#st-al-vibe-grid’);

                        container.innerHTML = »;

                        res.data.forEach(v => {

                            const btn = document.createElement(‘button’);

                            btn.type = ‘button’;

                            btn.className = ‘st-vibe-btn’;

                            btn.setAttribute(‘data-id’, v.id);

                            btn.innerHTML = `<span class=»vibe-emoji»>${v.emoji}</span> <span style=»white-space:nowrap;»>${v.name}</span>`;

                            

                            // Открытие модалки с описанием

                            btn.addEventListener(‘click’, () => {

                                document.getElementById(‘vibe-modal-emoji’).innerText = v.emoji;

                                document.getElementById(‘vibe-modal-name’).innerText = v.name;

                                document.getElementById(‘vibe-modal-desc’).innerText = v.description || ‘Описание отсутствует.’;

                                

                                document.getElementById(‘vibe-modal-confirm’).onclick = () => {

                                    wrap.querySelectorAll(‘.st-vibe-btn’).forEach(c => c.classList.remove(‘selected’));

                                    btn.classList.add(‘selected’);

                                    wrap.querySelector(‘#st-al-current-vibe-id’).value = v.id;

                                    document.getElementById(‘st-vibe-modal’).classList.remove(‘active’);

                                };

                                document.getElementById(‘st-vibe-modal’).classList.add(‘active’);

                            });

                            container.appendChild(btn);

                        });

                        

                        // Выделяем сохраненный

                        setTimeout(() => {

                            const saved = wrap.querySelector(‘#st-al-current-vibe-id’).value;

                            if (saved) wrap.querySelector(`.st-vibe-btn[data-id=»${saved}»]`)?.classList.add(‘selected’);

                        }, 500);

                    }

                }

            });

        }

 

        // Загрузка данных профиля

        function loadFullProfile() {

            $.ajax({

                url: ‘<?php echo admin_url(‘admin-ajax.php’); ?>’, type: ‘POST’,

                data: { action: ‘st_al_get_full_profile’, nonce: ‘<?php echo wp_create_nonce(‘st_al_nonce_action’); ?>’ },

                dataType: ‘json’,

                success: function(res) {

                    if (res.success && res.data) {

                        let d = res.data;

                        if (d.nickname) $(wrap).find(‘[name=»nickname»]’).val(d.nickname);

                        if (d.age) $(wrap).find(‘[name=»age»]’).val(d.age);

                        if (d.bio) $(wrap).find(‘[name=»bio»]’).val(d.bio);

                        

                        // Радио кнопки

                        if (d.engagement_level) $(wrap).find(`input[name=»engagement_level»][value=»${d.engagement_level}»]`).prop(‘checked’, true);

                        if (d.platform_goal) $(wrap).find(`input[name=»platform_goal»][value=»${d.platform_goal}»]`).prop(‘checked’, true);

                        

                        const lifestyleFields = [‘favorite_time’,’occupation’,’event_format’,’outing_frequency’,’venue_choice’,’company_pref’,’venue_atmosphere’,’comm_style’,’rating_style’,’explorer_type’];

                        $.each(lifestyleFields, function(i,f) {

                            if(d[f]) $(wrap).find(`input[name=»${f}»][value=»${d[f]}»]`).prop(‘checked’, true);

                        });

 

                        // Чекбоксы (Массивы)

                        if (d.languages) d.languages.forEach(v => $(wrap).find(`input[name=»languages[]»][value=»${v}»]`).prop(‘checked’, true));

                        if (d.spirits) d.spirits.forEach(v => $(wrap).find(`input[name=»spirits[]»][value=»${v}»]`).prop(‘checked’, true));

                        if (d.wine) d.wine.forEach(v => $(wrap).find(`input[name=»wine[]»][value=»${v}»]`).prop(‘checked’, true));

                        if (d.beer_cider) d.beer_cider.forEach(v => $(wrap).find(`input[name=»beer_cider[]»][value=»${v}»]`).prop(‘checked’, true));

                        if (d.fermented) d.fermented.forEach(v => $(wrap).find(`input[name=»fermented[]»][value=»${v}»]`).prop(‘checked’, true));

                        

                        if (d.interest_tags) $(wrap).find(‘[name=»interest_tags»]’).val(d.interest_tags.join(‘, ‘));

                        if (d.spotify_anthem) $(wrap).find(‘[name=»spotify_anthem»]’).val(d.spotify_anthem);

                        if (d.social_links) {

                            if(d.social_links.instagram) $(wrap).find(‘[name=»instagram»]’).val(d.social_links.instagram);

                            if(d.social_links.untappd) $(wrap).find(‘[name=»untappd»]’).val(d.social_links.untappd);

                            if(d.social_links.vivino) $(wrap).find(‘[name=»vivino»]’).val(d.social_links.vivino);

                        }

                        if (d.photos) { uploadedPhotos = d.photos; updatePhotosPreview(); }

                        if (d.current_vibe_id) $(wrap).find(‘#st-al-current-vibe-id’).val(d.current_vibe_id);

                    }

                }

            });

        }

 

        // Отправка формы

        $(wrap).find(‘#st-al-profile-form’).on(‘submit’, function(e) {

            e.preventDefault();

            const btn = wrap.querySelector(‘#st_al_save_btn’);

            btn.disabled = true; btn.innerText = ‘Сохранение…’;

 

            $.ajax({

                url: ‘<?php echo admin_url(‘admin-ajax.php’); ?>’, type: ‘POST’,

                data: $(this).serialize(), dataType: ‘json’,

                success: function(res) {

                    if (res.success) {

                        document.getElementById(‘st-success-modal’).classList.add(‘active’);

                    } else {

                        alert(res.data?.message || ‘Ошибка сохранения’);

                    }

                },

                complete: function() {

                    btn.disabled = false; btn.innerText = ‘Сохранить анкету’;

                }

            });

        });

 

        // Обработка финального модального окна

        document.querySelector(‘#st-success-modal .st-nav-card:nth-child(1)’)?.addEventListener(‘click’, function() {

            showInfoModal(‘🏠 Кабинет’, ‘Ваш личный кабинет со статистикой посещений и рекомендациями.’);

        });

        document.querySelector(‘#st-success-modal .st-nav-card:nth-child(2)’)?.addEventListener(‘click’, function() {

            window.location.href = ‘https://i.spirittrade.kz/at-privat-person/’;

        });

        document.querySelector(‘#st-success-modal .st-nav-card:nth-child(3)’)?.addEventListener(‘click’, function() {

            showInfoModal(‘📍 Локации’, ‘Карта баров и ресторанов с подборками по вашему вкусу.’);

        });

        document.querySelector(‘#st-success-modal .st-nav-card:nth-child(4)’)?.addEventListener(‘click’, function() {

            showInfoModal(‘🎉 Ивенты’, ‘Ближайшие дегустации, вечеринки и тематические встречи.’);

        });

 

        // Реферальные иконки

        document.querySelector(‘#st-success-modal .st-icon-btn:nth-child(1)’)?.addEventListener(‘click’, function() {

            showInfoModal(‘👤 Пригласить человека’, ‘Поделитесь реферальной ссылкой и получайте бонусы за каждого приглашённого друга.’);

        });

        document.querySelector(‘#st-success-modal .st-icon-btn:nth-child(2)’)?.addEventListener(‘click’, function() {

            showInfoModal(‘🏪 Подключить заведение’, ‘Если вы владелец или менеджер бара — присоединяйтесь к платформе и управляйте своим профилем.’);

        });

        document.querySelector(‘#st-success-modal .st-icon-btn:nth-child(3)’)?.addEventListener(‘click’, function() {

            showInfoModal(‘📅 Создать ивент’, ‘Организуйте собственную встречу, дегустацию или вечеринку и пригласите участников.’);

        });

 

        // Функция показа информационного модального окна

        function showInfoModal(title, description) {

            document.getElementById(‘info-modal-title’).innerText = title;

            document.getElementById(‘info-modal-desc’).innerText = description;

            document.getElementById(‘st-info-modal’).classList.add(‘active’);

        }

 

        // Закрытие по клику на оверлей

        document.getElementById(‘st-info-modal’)?.addEventListener(‘click’, function(e) {

            if (e.target === this) this.classList.remove(‘active’);

        });

 

        loadFullProfile();

        loadVibesAndRender();

        updateStepUI();

    })(jQuery);

    </script>

    <?php

    return ob_get_clean();

}

 

// AJAX: получить полный профиль

add_action( ‘wp_ajax_st_al_get_full_profile’, ‘st_al_ajax_get_full_profile’ );

function st_al_ajax_get_full_profile() {

    check_ajax_referer( ‘st_al_nonce_action’, ‘nonce’ );

    if ( ! is_user_logged_in() ) wp_send_json_error( array( ‘message’ => ‘Авторизуйтесь’ ) );

    $user_id = get_current_user_id();

    global $wpdb;

    $prefix = $wpdb->prefix;

 

    $profile = $wpdb->get_row( $wpdb->prepare( «SELECT * FROM {$prefix}st_al_user_profile WHERE user_id = %d», $user_id ), ARRAY_A );

    $beverages = $wpdb->get_row( $wpdb->prepare( «SELECT * FROM {$prefix}st_al_user_beverage_preferences WHERE user_id = %d», $user_id ), ARRAY_A );

    $lifestyle = $wpdb->get_row( $wpdb->prepare( «SELECT * FROM {$prefix}st_al_user_lifestyle WHERE user_id = %d», $user_id ), ARRAY_A );

    $media = $wpdb->get_row( $wpdb->prepare( «SELECT * FROM {$prefix}st_al_user_media_prompts WHERE user_id = %d», $user_id ), ARRAY_A );

 

    $result = [];

    if ( $profile ) {

        $result[‘nickname’] = $profile[‘nickname’];

        $result[‘age’] = $profile[‘age’];

        $result[‘engagement_level’] = $profile[‘engagement_level’];

        $result[‘bio’] = $profile[‘bio’];

        $result[‘platform_goal’] = $profile[‘platform_goal’];

        $result[‘languages’] = json_decode( $profile[‘languages’], true );

        $result[‘current_vibe_id’] = $profile[‘current_vibe_id’];

    }

    if ( $beverages ) {

        $result[‘spirits’] = json_decode( $beverages[‘spirits’], true );

        $result[‘wine’] = json_decode( $beverages[‘wine’], true );

        $result[‘beer_cider’] = json_decode( $beverages[‘beer_cider’], true );

        $result[‘fermented’] = json_decode( $beverages[‘fermented’], true );

    }

    if ( $lifestyle ) {

        foreach ( $lifestyle as $key => $val ) {

            if ( $key !== ‘user_id’ && ! empty( $val ) ) $result[$key] = $val;

        }

    }

    if ( $media ) {

        $result[‘photos’] = json_decode( $media[‘photos’], true );

        $result[‘interest_tags’] = json_decode( $media[‘interest_tags’], true );

        $result[‘spotify_anthem’] = $media[‘spotify_anthem’];

        $result[‘social_links’] = json_decode( $media[‘social_links’], true );

        $result[‘text_prompts’] = json_decode( $media[‘text_prompts’], true );

    }

    wp_send_json_success( $result );

}

 

// AJAX: сохранить полный профиль

add_action( ‘wp_ajax_st_al_save_full_profile’, ‘st_al_ajax_save_full_profile’ );

function st_al_ajax_save_full_profile() {

    check_ajax_referer( ‘st_al_nonce_action’, ‘st_al_nonce’ );

    if ( ! is_user_logged_in() ) wp_send_json_error( array( ‘message’ => ‘Авторизуйтесь’ ) );

    $user_id = get_current_user_id();

    global $wpdb;

    $prefix = $wpdb->prefix;

 

    $nickname = sanitize_text_field( $_POST[‘nickname’] ?? » );

    $age = intval( $_POST[‘age’] ?? 0 );

    $engagement_level = sanitize_text_field( $_POST[‘engagement_level’] ?? » );

    $bio = sanitize_textarea_field( $_POST[‘bio’] ?? » );

    $platform_goal = sanitize_text_field( $_POST[‘platform_goal’] ?? » );

    $languages = isset( $_POST[‘languages’] ) ? array_map( ‘sanitize_text_field’, $_POST[‘languages’] ) : [];

    $current_vibe_id = intval( $_POST[‘current_vibe_id’] ?? 0 );

 

    $profile_data = [

        ‘nickname’ => $nickname,

        ‘age’ => $age ?: null,

        ‘engagement_level’ => $engagement_level,

        ‘bio’ => $bio,

        ‘platform_goal’ => $platform_goal,

        ‘languages’ => wp_json_encode( $languages ),

        ‘current_vibe_id’ => $current_vibe_id ?: null,

        ‘vibe_updated_at’ => $current_vibe_id ? current_time( ‘mysql’ ) : null,

    ];

    $profile_exists = $wpdb->get_var( $wpdb->prepare( «SELECT user_id FROM {$prefix}st_al_user_profile WHERE user_id = %d», $user_id ) );

    if ( $profile_exists ) {

        $wpdb->update( $prefix . ‘st_al_user_profile’, $profile_data, [ ‘user_id’ => $user_id ] );

    } else {

        $profile_data[‘user_id’] = $user_id;

        $wpdb->insert( $prefix . ‘st_al_user_profile’, $profile_data );

    }

    if ( $current_vibe_id ) {

        $wpdb->insert( $prefix . ‘st_al_user_vibe_history’, [ ‘user_id’ => $user_id, ‘vibe_id’ => $current_vibe_id, ‘set_at’ => current_time( ‘mysql’ ) ] );

    }

 

    $beverage_data = [

        ‘spirits’ => wp_json_encode( isset( $_POST[‘spirits’] ) ? array_map( ‘sanitize_text_field’, $_POST[‘spirits’] ) : [] ),

        ‘wine’ => wp_json_encode( isset( $_POST[‘wine’] ) ? array_map( ‘sanitize_text_field’, $_POST[‘wine’] ) : [] ),

        ‘beer_cider’ => wp_json_encode( isset( $_POST[‘beer_cider’] ) ? array_map( ‘sanitize_text_field’, $_POST[‘beer_cider’] ) : [] ),

        ‘fermented’ => wp_json_encode( isset( $_POST[‘fermented’] ) ? array_map( ‘sanitize_text_field’, $_POST[‘fermented’] ) : [] ),

    ];

    $beverage_exists = $wpdb->get_var( $wpdb->prepare( «SELECT user_id FROM {$prefix}st_al_user_beverage_preferences WHERE user_id = %d», $user_id ) );

    if ( $beverage_exists ) {

        $wpdb->update( $prefix . ‘st_al_user_beverage_preferences’, $beverage_data, [ ‘user_id’ => $user_id ] );

    } else {

        $beverage_data[‘user_id’] = $user_id;

        $wpdb->insert( $prefix . ‘st_al_user_beverage_preferences’, $beverage_data );

    }

 

    $lifestyle_fields = [ ‘favorite_time’, ‘occupation’, ‘event_format’, ‘outing_frequency’, ‘venue_choice’, ‘company_pref’, ‘venue_atmosphere’, ‘comm_style’, ‘rating_style’, ‘explorer_type’ ];

    $lifestyle_data = [];

    foreach ( $lifestyle_fields as $field ) {

        $lifestyle_data[ $field ] = sanitize_text_field( $_POST[ $field ] ?? » );

    }

    $lifestyle_exists = $wpdb->get_var( $wpdb->prepare( «SELECT user_id FROM {$prefix}st_al_user_lifestyle WHERE user_id = %d», $user_id ) );

    if ( $lifestyle_exists ) {

        $wpdb->update( $prefix . ‘st_al_user_lifestyle’, $lifestyle_data, [ ‘user_id’ => $user_id ] );

    } else {

        $lifestyle_data[‘user_id’] = $user_id;

        $wpdb->insert( $prefix . ‘st_al_user_lifestyle’, $lifestyle_data );

    }

 

    $interest_tags = isset( $_POST[‘interest_tags’] ) ? array_map( ‘trim’, explode( ‘,’, sanitize_text_field( $_POST[‘interest_tags’] ) ) ) : [];

    $spotify_anthem = esc_url_raw( $_POST[‘spotify_anthem’] ?? » );

    $social_links = [

        ‘instagram’ => sanitize_text_field( $_POST[‘instagram’] ?? » ),

        ‘untappd’   => sanitize_text_field( $_POST[‘untappd’] ?? » ),

        ‘vivino’    => sanitize_text_field( $_POST[‘vivino’] ?? » ),

    ];

    $text_prompts = [];

    $raw_prompts = sanitize_textarea_field( $_POST[‘text_prompts’] ?? » );

    if ( ! empty( $raw_prompts ) ) {

        $decoded = json_decode( $raw_prompts, true );

        if ( is_array( $decoded ) ) $text_prompts = $decoded;

    }

    $photos = isset( $_POST[‘photos’] ) ? json_decode( stripslashes( $_POST[‘photos’] ), true ) : [];

    if ( ! is_array( $photos ) ) $photos = [];

 

    $media_data = [

        ‘photos’ => wp_json_encode( $photos ),

        ‘interest_tags’ => wp_json_encode( $interest_tags ),

        ‘spotify_anthem’ => $spotify_anthem,

        ‘social_links’ => wp_json_encode( $social_links ),

        ‘text_prompts’ => wp_json_encode( $text_prompts ),

    ];

    $media_exists = $wpdb->get_var( $wpdb->prepare( «SELECT user_id FROM {$prefix}st_al_user_media_prompts WHERE user_id = %d», $user_id ) );

    if ( $media_exists ) {

        $wpdb->update( $prefix . ‘st_al_user_media_prompts’, $media_data, [ ‘user_id’ => $user_id ] );

    } else {

        $media_data[‘user_id’] = $user_id;

        $wpdb->insert( $prefix . ‘st_al_user_media_prompts’, $media_data );

    }

 

    wp_send_json_success( array( ‘message’ => ‘Профиль сохранён’ ) );

}

 

// AJAX: список вайбов с полным описанием

add_action( ‘wp_ajax_st_al_get_vibes_list’, ‘st_al_ajax_get_vibes_list’ );

function st_al_ajax_get_vibes_list() {

    check_ajax_referer( ‘st_al_nonce_action’, ‘nonce’ );

    if ( ! is_user_logged_in() ) wp_send_json_error( ‘Авторизуйтесь’ );

    global $wpdb;

    $vibes = $wpdb->get_results( «SELECT id, emoji, name, description FROM {$wpdb->prefix}st_al_vibes_dictionary ORDER BY id» );

    wp_send_json_success( $vibes );

}

 

// REST API

add_action( ‘rest_api_init’, ‘st_al_register_rest_routes’ );

function st_al_register_rest_routes() {

    register_rest_route( ‘st-al/v1’, ‘/profile’, array(

        ‘methods’             => ‘GET’,

        ‘callback’            => ‘st_al_rest_get_my_profile’,

        ‘permission_callback’ => ‘is_user_logged_in’,

    ) );

    register_rest_route( ‘st-al/v1’, ‘/profile/(?P<id>[\d]+)’, array(

        ‘methods’             => ‘GET’,

        ‘callback’            => ‘st_al_rest_get_user_profile’,

        ‘permission_callback’ => ‘__return_true’,

        ‘args’                => array( ‘id’ => array( ‘validate_callback’ => ‘is_numeric’ ) ),

    ) );

}

function st_al_rest_get_my_profile( $request ) {

    $user_id = get_current_user_id();

    if ( ! $user_id ) return new WP_REST_Response( array( ‘error’ => ‘Not logged in’ ), 401 );

    return st_al_build_profile_response( $user_id );

}

function st_al_rest_get_user_profile( $request ) {

    $user_id = intval( $request[‘id’] );

    if ( ! $user_id || ! get_userdata( $user_id ) ) return new WP_REST_Response( array( ‘error’ => ‘User not found’ ), 404 );

    return st_al_build_profile_response( $user_id );

}

function st_al_build_profile_response( $user_id ) {

    global $wpdb;

    $prefix = $wpdb->prefix;

    $profile = $wpdb->get_row( $wpdb->prepare( «SELECT * FROM {$prefix}st_al_user_profile WHERE user_id = %d», $user_id ), ARRAY_A );

    $beverages = $wpdb->get_row( $wpdb->prepare( «SELECT * FROM {$prefix}st_al_user_beverage_preferences WHERE user_id = %d», $user_id ), ARRAY_A );

    $lifestyle = $wpdb->get_row( $wpdb->prepare( «SELECT * FROM {$prefix}st_al_user_lifestyle WHERE user_id = %d», $user_id ), ARRAY_A );

    $media = $wpdb->get_row( $wpdb->prepare( «SELECT * FROM {$prefix}st_al_user_media_prompts WHERE user_id = %d», $user_id ), ARRAY_A );

    $user = get_userdata( $user_id );

    $result = array( ‘user_id’ => $user_id, ’email’ => $user->user_email, ‘display_name’ => $user->display_name );

    if ( $profile ) {

        $result[‘nickname’] = $profile[‘nickname’];

        $result[‘age’] = $profile[‘age’];

        $result[‘engagement_level’] = $profile[‘engagement_level’];

        $result[‘bio’] = $profile[‘bio’];

        $result[‘platform_goal’] = $profile[‘platform_goal’];

        $result[‘languages’] = json_decode( $profile[‘languages’], true );

        $result[‘current_vibe_id’] = $profile[‘current_vibe_id’];

        $result[‘vibe_updated_at’] = $profile[‘vibe_updated_at’];

    }

    if ( $beverages ) {

        $result[‘spirits’] = json_decode( $beverages[‘spirits’], true );

        $result[‘wine’] = json_decode( $beverages[‘wine’], true );

        $result[‘beer_cider’] = json_decode( $beverages[‘beer_cider’], true );

        $result[‘fermented’] = json_decode( $beverages[‘fermented’], true );

    }

    if ( $lifestyle ) {

        foreach ( $lifestyle as $key => $val ) {

            if ( $key !== ‘user_id’ && ! empty( $val ) ) $result[$key] = $val;

        }

    }

    if ( $media ) {

        $result[‘photos’] = json_decode( $media[‘photos’], true );

        $result[‘interest_tags’] = json_decode( $media[‘interest_tags’], true );

        $result[‘spotify_anthem’] = $media[‘spotify_anthem’];

        $result[‘social_links’] = json_decode( $media[‘social_links’], true );

        $result[‘text_prompts’] = json_decode( $media[‘text_prompts’], true );

    }

    return new WP_REST_Response( $result, 200 );

}

 

// Админка рекомендаций

add_action( ‘admin_menu’, ‘st_al_add_recommendations_admin_page’ );

function st_al_add_recommendations_admin_page() {

    add_options_page( ‘AL Вайб рекомендации’, ‘AL Вайб рекомендации’, ‘manage_options’, ‘st-al-recommendations’, ‘st_al_render_recommendations_page’ );

}

function st_al_render_recommendations_page() {

    global $wpdb;

    $table_vibes = $wpdb->prefix . ‘st_al_vibes_dictionary’;

    $table_rules = $wpdb->prefix . ‘st_al_vibe_recommendation_rules’;

    if ( isset( $_POST[‘st_al_save_rules’] ) && check_admin_referer( ‘st_al_recommendations’ ) ) {

        foreach ( $_POST[‘rules’] as $vibe_id => $rule ) {

            $beverage = isset( $rule[‘target_beverage_category’] ) ? array_map( ‘sanitize_text_field’, explode( «\n», $rule[‘target_beverage_category’] ) ) : [];

            $atmosphere = isset( $rule[‘target_venue_atmosphere’] ) ? array_map( ‘sanitize_text_field’, explode( «\n», $rule[‘target_venue_atmosphere’] ) ) : [];

            $alternatives = isset( $rule[‘alternative_options’] ) ? array_map( ‘sanitize_text_field’, explode( «\n», $rule[‘alternative_options’] ) ) : [];

            $data = [

                ‘vibe_id’ => intval( $vibe_id ),

                ‘target_beverage_category’ => wp_json_encode( $beverage ),

                ‘target_venue_atmosphere’ => wp_json_encode( $atmosphere ),

                ‘alternative_options’ => wp_json_encode( $alternatives ),

            ];

            $exists = $wpdb->get_var( $wpdb->prepare( «SELECT vibe_id FROM $table_rules WHERE vibe_id = %d», $vibe_id ) );

            if ( $exists ) $wpdb->update( $table_rules, $data, [ ‘vibe_id’ => $vibe_id ] );

            else $wpdb->insert( $table_rules, $data );

        }

        echo ‘<div class=»notice notice-success»><p>Правила сохранены.</p></div>’;

    }

    $vibes = $wpdb->get_results( «SELECT * FROM $table_vibes ORDER BY category, id» );

    $rules = [];

    $existing = $wpdb->get_results( «SELECT * FROM $table_rules» );

    foreach ( $existing as $r ) $rules[ $r->vibe_id ] = $r;

    ?>

    <div class=»wrap»>

        <h1>Правила рекомендаций по вайбу</h1>

        <form method=»post»>

            <?php wp_nonce_field( ‘st_al_recommendations’ ); ?>

            <table class=»widefat striped»>

                <thead><tr><th>Вайб</th><th>Рекомендуемые напитки</th><th>Атмосфера</th><th>Безалкогольные альтернативы</th></tr></thead>

                <tbody>

                <?php foreach ( $vibes as $vibe ) :

                    $rule = isset( $rules[ $vibe->id ] ) ? $rules[ $vibe->id ] : null;

                    $beverage = $rule ? json_decode( $rule->target_beverage_category, true ) : [];

                    $atmosphere = $rule ? json_decode( $rule->target_venue_atmosphere, true ) : [];

                    $alternatives = $rule ? json_decode( $rule->alternative_options, true ) : [];

                    ?>

                    <tr>

                        <td><strong><?php echo esc_html( $vibe->emoji . ‘ ‘ . $vibe->name ); ?></strong><br><code><?php echo esc_html( $vibe->code ); ?></code></td>

                        <td><textarea name=»rules[<?php echo $vibe->id; ?>][target_beverage_category]» rows=»3″ cols=»35″><?php echo esc_textarea( implode( «\n», $beverage ) ); ?></textarea></td>

                        <td><textarea name=»rules[<?php echo $vibe->id; ?>][target_venue_atmosphere]» rows=»3″ cols=»35″><?php echo esc_textarea( implode( «\n», $atmosphere ) ); ?></textarea></td>

                        <td><textarea name=»rules[<?php echo $vibe->id; ?>][alternative_options]» rows=»3″ cols=»35″><?php echo esc_textarea( implode( «\n», $alternatives ) ); ?></textarea></td>

                    </tr>

                <?php endforeach; ?>

                </tbody>

            </table>

            <p class=»submit»><button type=»submit» name=»st_al_save_rules» class=»button-primary»>Сохранить все правила</button></p>

        </form>

    </div>

    <?php

}

 

// Шорткод быстрого переключения вайба

add_shortcode( ‘al_current_vibe_switcher’, ‘st_al_vibe_switcher’ );

function st_al_vibe_switcher() {

    if ( ! is_user_logged_in() ) {

        return ‘<div style=»margin:10px 0; padding:15px; border:1px solid rgba(212, 175, 55, 0.4); border-radius:12px; background:#fff; text-align:center; box-shadow: 0 4px 15px rgba(0,0,0,0.03);»>

            <div style=»font-size:1.2rem; margin-bottom:5px;»>🍃 <strong>Мой вайб сейчас</strong></div>

            <span style=»font-size:0.85rem; color:#666;»>Чтобы алгоритм подбирал заведения, <a href=»‘.wp_login_url(get_permalink()).’» style=»color:#D4AF37; text-decoration:none; font-weight:bold;»>войдите</a> или <a href=»‘.wp_registration_url().’» style=»color:#D4AF37; text-decoration:none; font-weight:bold;»>создайте аккаунт</a>.</span>

        </div>’;

    }

    ob_start();

    ?>

    <div class=»st-al-vibe-switch» style=»margin:10px 0; padding:10px; border:1px solid #e2e8f0; border-radius:12px; background:#fff;»>

        <label for=»st_quick_vibe» style=»font-weight:500;»>🍃 Мой вайб сейчас:</label>

        <select id=»st_quick_vibe» style=»padding:6px 12px; margin-left:10px; border:1px solid #e2e8f0; border-radius:8px;»>

            <option value=»»>— Выберите —</option>

            <?php

            global $wpdb;

            $current_vibe_id = $wpdb->get_var( $wpdb->prepare( «SELECT current_vibe_id FROM {$wpdb->prefix}st_al_user_profile WHERE user_id = %d», get_current_user_id() ) );

            $vibes = $wpdb->get_results( «SELECT id, emoji, name FROM {$wpdb->prefix}st_al_vibes_dictionary» );

            foreach ( $vibes as $v ) {

                $selected = ( $current_vibe_id == $v->id ) ? ‘selected’ : »;

                echo ‘<option value=»‘ . $v->id . ‘» ‘ . $selected . ‘>’ . $v->emoji . ‘ ‘ . esc_html( $v->name ) . ‘</option>’;

            }

            ?>

        </select>

        <button id=»st_update_vibe_btn» style=»padding:6px 16px; background:#e67e22; color:white; border:none; border-radius:8px; cursor:pointer;»>Обновить</button>

    </div>

    <script>

    document.getElementById(‘st_update_vibe_btn’)?.addEventListener(‘click’, function() {

        let vibeId = document.getElementById(‘st_quick_vibe’).value;

        if (!vibeId) return;

        fetch(‘<?php echo admin_url(‘admin-ajax.php’); ?>’, {

            method: ‘POST’,

            headers: { ‘Content-Type’: ‘application/x-www-form-urlencoded’ },

            body: new URLSearchParams({

                action: ‘st_al_update_vibe_only’,

                vibe_id: vibeId,

                nonce: ‘<?php echo wp_create_nonce(‘st_al_nonce_action’); ?>’

            })

        }).then(r => r.json()).then(res => {

            alert(res.success ? ‘Вайб обновлён!’ : ‘Ошибка’);

        });

    });

    </script>

    <?php

    return ob_get_clean();

}

 

// AJAX обновление только вайба

add_action( ‘wp_ajax_st_al_update_vibe_only’, ‘st_al_ajax_update_vibe_only’ );

function st_al_ajax_update_vibe_only() {

    check_ajax_referer( ‘st_al_nonce_action’, ‘nonce’ );

    if ( ! is_user_logged_in() ) wp_send_json_error( ‘Нет авторизации’ );

    $user_id = get_current_user_id();

    $vibe_id = intval( $_POST[‘vibe_id’] );

    if ( ! $vibe_id ) wp_send_json_error( ‘Неверный вайб’ );

    global $wpdb;

    $prefix = $wpdb->prefix;

    $wpdb->update( $prefix . ‘st_al_user_profile’, [ ‘current_vibe_id’ => $vibe_id, ‘vibe_updated_at’ => current_time( ‘mysql’ ) ], [ ‘user_id’ => $user_id ] );

    $wpdb->insert( $prefix . ‘st_al_user_vibe_history’, [ ‘user_id’ => $user_id, ‘vibe_id’ => $vibe_id, ‘set_at’ => current_time( ‘mysql’ ) ] );

    wp_send_json_success( ‘ok’ );

}

 

// ==============================================================================

// УНИВЕРСАЛЬНЫЙ ЭКРАН АВТОРИЗАЦИИ ДЛЯ НЕАВТОРИЗОВАННЫХ ПОЛЬЗОВАТЕЛЕЙ

// ==============================================================================

function st_get_auth_prompt_html( $message = ‘Для доступа к этому разделу необходимо авторизоваться.’ ) {

    $login_url = wp_login_url( get_permalink() );

    $register_url = wp_registration_url();

 

    ob_start();

    ?>

    <style>

        .st-auth-prompt {

            display: flex; flex-direction: column; align-items: center; justify-content: center;

            padding: 40px 30px; background: #fff; border: 1px solid rgba(212, 175, 55, 0.3);

            border-radius: 16px; text-align: center; font-family: «Helvetica Neue», sans-serif;

            box-shadow: 0 10px 40px rgba(0,0,0,0.05); max-width: 500px; margin: 40px auto;

            position: relative; overflow: hidden;

        }

        .st-auth-prompt::before {

            content: »; position: absolute; top: 0; left: 0; right: 0; height: 4px;

            background: linear-gradient(90deg, #D4AF37, #F5D76E, #D4AF37);

        }

        .st-auth-prompt-icon { font-size: 50px; margin-bottom: 20px; line-height: 1; }

        .st-auth-prompt-title {

            font-size: 24px; font-weight: 800; color: #1B2A3B; margin: 0 0 15px 0;

            font-family: «Playfair Display», Georgia, serif;

        }

        .st-auth-prompt-text { font-size: 15px; color: #6B7280; margin-bottom: 30px; line-height: 1.5; }

        .st-auth-prompt-actions { display: flex; gap: 15px; flex-wrap: wrap; justify-content: center; width: 100%; }

        .st-auth-prompt-btn {

            flex: 1; min-width: 180px; padding: 12px 20px; border-radius: 30px;

            font-weight: 700; text-decoration: none !important; transition: 0.3s ease;

            font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px;

        }

        .st-auth-btn-in { background: #1B2A3B; color: #fff !important; border: 2px solid #1B2A3B; }

        .st-auth-btn-in:hover { background: #0F1923; transform: translateY(-2px); box-shadow: 0 8px 20px rgba(27,42,59,0.2); }

        .st-auth-btn-up { background: transparent; color: #D4AF37 !important; border: 2px solid #D4AF37; }

        .st-auth-btn-up:hover { background: #D4AF37; color: #1B2A3B !important; transform: translateY(-2px); box-shadow: 0 8px 20px rgba(212,175,55,0.3); }

    </style>

    <div class=»st-auth-prompt»>

        <div class=»st-auth-prompt-icon»>🔐</div>

        <h3 class=»st-auth-prompt-title»>Доступ закрыт</h3>

        <p class=»st-auth-prompt-text»><?php echo esc_html( $message ); ?></p>

        <div class=»st-auth-prompt-actions»>

            <a href=»<?php echo esc_url( $login_url ); ?>» class=»st-auth-prompt-btn st-auth-btn-in»>Войти</a>

            <a href=»<?php echo esc_url( $register_url ); ?>» class=»st-auth-prompt-btn st-auth-btn-up»>Регистрация</a>

        </div>

    </div>

    <?php

    return ob_get_clean();

}

 

 

 

// ==============================================================================

// 7. TINDER-ПОИСК: ВЫГРУЗКА АНКЕТ И ШОРТКОД

Ищем компанию…

(ОБНОВЛЕНО v4)

// ==============================================================================

 

add_action( ‘wp_ajax_st_get_tinder_profiles’, ‘st_ajax_get_tinder_profiles’ );

add_action( ‘wp_ajax_nopriv_st_get_tinder_profiles’, ‘st_ajax_get_tinder_profiles’ );

 

function st_ajax_get_tinder_profiles() {

    global $wpdb;

    $prefix = $wpdb->prefix;

 

    $exclude_ids = [];

    if ( isset($_POST[‘exclude_ids’]) ) {

        $decoded = json_decode(stripslashes($_POST[‘exclude_ids’]), true);

        if ( is_array($decoded) ) $exclude_ids = array_map(‘intval’, $decoded);

    }

 

    $exclude_sql = «»;

    if ( !empty($exclude_ids) ) {

        $exclude_ids_str = implode(‘,’, $exclude_ids);

        $exclude_sql = «WHERE p.user_id NOT IN ($exclude_ids_str)»;

    }

 

    $query = »

        SELECT 

            p.user_id, p.nickname, p.age, p.engagement_level, p.bio, p.platform_goal, p.languages,

            m.photos, m.interest_tags, m.spotify_anthem, m.text_prompts,

            b.spirits, b.wine, b.beer_cider, b.fermented,

            l.favorite_time, l.occupation, l.event_format, l.outing_frequency, l.venue_choice, l.company_pref, l.venue_atmosphere, l.comm_style, l.rating_style, l.explorer_type,

            v.emoji, v.name as vibe_name, v.description as vibe_description

        FROM {$prefix}st_al_user_profile p

        LEFT JOIN {$prefix}st_al_user_media_prompts m ON p.user_id = m.user_id

        LEFT JOIN {$prefix}st_al_user_beverage_preferences b ON p.user_id = b.user_id

        LEFT JOIN {$prefix}st_al_user_lifestyle l ON p.user_id = l.user_id

        LEFT JOIN {$prefix}st_al_vibes_dictionary v ON p.current_vibe_id = v.id

        $exclude_sql

        ORDER BY RAND() 

        LIMIT 10

    «;

    

    $users = $wpdb->get_results( $query, ARRAY_A );

    $formatted_users = [];

 

    if ($users) {

        foreach ( $users as $u ) {

            $photos = json_decode( $u[‘photos’], true );

            $photos_array = ( is_array($photos) && !empty($photos) ) ? $photos : [‘https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?q=80&w=350&h=600&fit=crop’];

            

            $tags = json_decode( $u[‘interest_tags’], true ) ?: [];

            if ( !empty($u[‘platform_goal’]) ) array_unshift($tags, ‘🎯 ‘ . $u[‘platform_goal’]);

            

            $vibe = !empty($u[‘vibe_name’]) ? [‘name’ => $u[‘vibe_name’], ’emoji’ => $u[’emoji’], ‘description’ => $u[‘vibe_description’]] : null;

 

            $spirits = json_decode($u[‘spirits’], true) ?: [];

            $wine = json_decode($u[‘wine’], true) ?: [];

            $beer = json_decode($u[‘beer_cider’], true) ?: [];

            $raw_prefs = array_merge($spirits, $wine, $beer);

 

            $extra_fields = [];

            

            if (!empty($u[‘engagement_level’])) $extra_fields[‘Уровень’] = $u[‘engagement_level’];

            $langs = json_decode($u[‘languages’], true);

            if (!empty($langs)) $extra_fields[‘Языки’] = implode(‘, ‘, $langs);

 

            if (!empty($spirits)) $extra_fields[‘Крепкий алкоголь’] = implode(‘, ‘, $spirits);

            if (!empty($wine)) $extra_fields[‘Вино’] = implode(‘, ‘, $wine);

            if (!empty($beer)) $extra_fields[‘Пиво и сидр’] = implode(‘, ‘, $beer);

            

            $ferm = json_decode($u[‘fermented’], true);

            if (!empty($ferm)) $extra_fields[‘Ферментированные’] = implode(‘, ‘, $ferm);

 

            $lifestyle_map = [

                ‘favorite_time’ => ‘Любимое время суток’, ‘occupation’ => ‘Сфера’,

                ‘event_format’ => ‘Формат ивентов’, ‘outing_frequency’ => ‘Частота выходов’,

                ‘venue_choice’ => ‘Выбор заведения’, ‘company_pref’ => ‘Компания’,

                ‘venue_atmosphere’ => ‘Атмосфера’, ‘comm_style’ => ‘Стиль общения’,

                ‘rating_style’ => ‘Оценка’, ‘explorer_type’ => ‘Тип исследователя’

            ];

            foreach ($lifestyle_map as $db_key => $label) {

                if (!empty($u[$db_key])) $extra_fields[$label] = $u[$db_key];

            }

 

            $prompts = json_decode($u[‘text_prompts’], true);

            if (!empty($prompts) && is_array($prompts)) {

                foreach ($prompts as $q => $a) {

                    if (!empty($a)) $extra_fields[‘💬 ‘ . $q] = $a;

                }

            }

 

            $formatted_users[] = [

                ‘id’          => $u[‘user_id’],

                ‘name’        => !empty($u[‘nickname’]) ? $u[‘nickname’] : ‘Аноним’,

                ‘age’         => !empty($u[‘age’]) ? $u[‘age’] : »,

                ‘bio’         => !empty($u[‘bio’]) ? wp_trim_words($u[‘bio’], 15) : ‘Гурман исследует новые вкусы…’,

                ‘full_bio’    => !empty($u[‘bio’]) ? $u[‘bio’] : »,

                ‘photos’      => $photos_array,

                ‘tags’        => array_slice($tags, 0, 4),

                ‘vibe’        => $vibe,

                ‘raw_prefs’   => $raw_prefs,

                ‘extra_fields’=> $extra_fields

            ];

        }

    }

 

    wp_send_json_success( $formatted_users );

}

 

add_shortcode( ‘al_tinder_search’, ‘st_render_tinder_search’ );

 

function st_render_tinder_search() {

    ob_start();

    ?>

    <style>

        #wpadminbar { display: none !important; }

        html { margin-top: 0 !important; }

 

        .st-tinder-wrapper {

            display: flex; flex-direction: column; justify-content: center; align-items: center;

            height: 100vh; background-color: #fdfcf8; font-family: «Lato», «Helvetica Neue», sans-serif;

            overflow: hidden; position: relative;

        }

 

        .card-container {

            position: relative; width: 420px; height: 720px; perspective: 1000px;

        }

 

        @media (max-width: 600px) {

            .card-container { width: 95%; max-width: 360px; height: 85vh; max-height: 650px; }

        }

 

        .tinder-card {

            position: absolute; width: 100%; height: 100%; background: #ffffff;

            border-radius: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.15);

            overflow: hidden; display: flex; flex-direction: column;

            transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease;

            border: 1px solid #E0D8C8; will-change: transform; touch-action: pan-y;

        }

 

        /* СЛАЙДЕР ФОТОГРАФИЙ */

        .st-slider { position: absolute; width: 100%; height: 100%; top: 0; left: 0; z-index: 1; }

        .st-slide {

            position: absolute; width: 100%; height: 100%; object-fit: cover;

            opacity: 0; transition: opacity 0.3s ease; pointer-events: none;

        }

        .st-slide.active { opacity: 1; }

        

        .st-slider-indicators {

            position: absolute; top: 12px; left: 0; width: 100%; display: flex;

            justify-content: center; gap: 6px; padding: 0 15px; z-index: 6; box-sizing: border-box;

        }

        .st-slider-dot {

            flex: 1; height: 5px; background: rgba(255,255,255,0.4); border-radius: 3px; transition: 0.3s;

        }

        .st-slider-dot.active { background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,0.5); }

 

        /* ИКОНКИ ПРЕДПОЧТЕНИЙ (СПРАВА) */

        .pref-icons-wrapper {

            position: absolute; top: 80px; right: 15px; display: flex;

            flex-direction: column; gap: 10px; z-index: 6; pointer-events: none;

        }

        .pref-icon {

            width: 42px; height: 42px; border-radius: 50%; background: rgba(0,0,0,0.4);

            backdrop-filter: blur(8px); display: flex; justify-content: center; align-items: center;

            font-size: 20px; border: 1px solid rgba(255,255,255,0.25);

            box-shadow: 0 4px 15px rgba(0,0,0,0.15); animation: fadeSlideIn 0.5s ease backwards;

        }

        @keyframes fadeSlideIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }

 

        /* НОВЫЕ КНОПКИ ВЕРХНЕГО МЕНЮ (НАВИГАЦИЯ ФОТО И ИНФО) */

        .top-btn-wrapper {

            position: absolute; top: 25px; z-index: 10; display: flex; gap: 10px;

        }

        .top-btn-wrapper.left { left: 15px; }

        .top-btn-wrapper.right { right: 15px; }

 

        .btn-glass {

            width: 44px; height: 44px; border-radius: 50%; background: rgba(0,0,0,0.4);

            backdrop-filter: blur(8px); border: 1px solid rgba(255, 255, 255, 0.3);

            color: #fff; display: flex; align-items: center; justify-content: center;

            cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 10px rgba(0,0,0,0.2);

            position: relative;

        }

        .btn-glass:hover { transform: scale(1.08); background: rgba(0,0,0,0.6); }

        .btn-glass svg { width: 22px; height: 22px; stroke: #fff; transition: 0.3s; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

        

        .btn-photo-nav .photo-counter {

            position: absolute; bottom: -6px; right: -6px; background: #D4AF37;

            color: #1B2A3B; font-size: 11px; font-weight: 800; font-family: «Lato», sans-serif;

            padding: 2px 6px; border-radius: 10px; box-shadow: 0 2px 5px rgba(0,0,0,0.3);

            border: 1px solid #fff;

        }

 

        .btn-detail .icon-close { display: none; }

        .btn-detail.active { background: #D4AF37; border-color: #D4AF37; }

        .btn-detail.active .icon-info { display: none; }

        .btn-detail.active .icon-close { display: block; stroke: #fff; transform: rotate(90deg); }

 

        .profile-overlay {

            position: absolute; bottom: 0; width: 100%; height: 60%;

            background: linear-gradient(to top, rgba(27, 42, 59, 0.95) 0%, transparent 100%);

            z-index: 2; pointer-events: none;

        }

 

        .profile-info {

            position: absolute; bottom: 95px; width: 100%; padding: 0 25px;

            color: #ffffff; box-sizing: border-box; z-index: 3; pointer-events: none;

        }

        .profile-name {

            font-size: 30px; font-family: «Playfair Display», serif; font-weight: 700;

            margin: 0 0 5px 0; text-shadow: 1px 2px 5px rgba(0,0,0,0.7);

        }

        .profile-bio { font-size: 15px; margin: 0 0 15px 0; opacity: 0.95; line-height: 1.4; }

        .tags { display: flex; flex-wrap: wrap; gap: 8px; }

        .tag {

            background: rgba(255, 255, 255, 0.2); backdrop-filter: blur(8px);

            padding: 5px 12px; border-radius: 20px; font-size: 12px;

            font-weight: 600; border: 1px solid rgba(212, 175, 55, 0.5); color: #FDFBF7;

        }

 

        /* Внутренняя панель деталей */

        .details-panel {

            position: absolute; top: 0; right: 0; width: 100%; height: calc(100% — 90px);

            background: #fdfcf8; z-index: 4; transform: translateY(100%);

            transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1);

            padding: 30px; box-sizing: border-box; overflow-y: auto; color: #1B2A3B; opacity: 0;

        }

        .details-panel.active { transform: translateY(0); opacity: 1; }

 

        .extra-fields-list {

            background: #fff; border: 1px solid #E0D8C8; border-radius: 16px;

            padding: 15px 20px; margin-top: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.03);

        }

        .extra-row {

            display: flex; justify-content: space-between; align-items: flex-start;

            padding: 12px 0; border-bottom: 1px dashed #E5E0D5; font-size: 14px;

        }

        .extra-row:last-child { border-bottom: none; }

        .extra-row strong { color: #888; max-width: 45%; font-weight: 500; }

        .extra-row span { text-align: right; max-width: 50%; color: #1B2A3B; font-weight: 600; }

 

        .vibe-block {

            background: #fff; border: 1px solid #E0D8C8; border-radius: 16px;

            padding: 15px 20px; margin: 20px 0; display: flex; align-items: center; gap: 15px;

        }

        .vibe-emoji { font-size: 36px; }

        .vibe-text h4 { margin: 0 0 5px 0; font-family: «Playfair Display», serif; font-size: 18px; }

        .vibe-text p { margin: 0; font-size: 13px; color: #666; line-height: 1.4; }

 

        /* Экшены (подвал) */

        .actions {

            position: absolute; bottom: 0; width: 100%; height: 90px; background: #fff;

            display: flex; justify-content: space-evenly; align-items: center;

            z-index: 5; border-top: 1px solid #E5E0D5;

        }

        .btn-action {

            width: 55px; height: 55px; border-radius: 50%; border: 2px solid #E5E0D5;

            cursor: pointer; display: flex; justify-content: center; align-items: center;

            background: #fff; transition: 0.2s; box-shadow: 0 5px 15px rgba(0,0,0,0.05); font-size: 22px;

        }

        .btn-action:hover { transform: translateY(-3px) scale(1.05); }

        .btn-skip { color: #B23B2E; border-color: rgba(178, 59, 46, 0.2); }

        .btn-invite { color: #D4AF37; border-color: rgba(212, 175, 55, 0.3); width: 65px; height: 65px; font-size: 26px; }

 

        /* ВСПЛЫВАЮЩЕЕ МЕНЮ ПРОСТРАНСТВ (ВМЕСТО МОДАЛКИ) */

        .invite-popup-menu {

            position: absolute; bottom: 100px; right: 20px; width: 240px;

            background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(15px);

            border: 1px solid rgba(212, 175, 55, 0.4); border-radius: 16px;

            padding: 15px; box-shadow: 0 15px 35px rgba(0,0,0,0.2);

            opacity: 0; visibility: hidden; transform: translateY(20px) scale(0.9);

            transform-origin: bottom right; transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);

            z-index: 100;

        }

        .invite-popup-menu.active { opacity: 1; visibility: visible; transform: translateY(0) scale(1); }

        .invite-popup-title { font-family: «Playfair Display», serif; font-size: 16px; font-weight: 700; margin: 0 0 10px 0; color: #1B2A3B; border-bottom: 1px dashed #E0D8C8; padding-bottom: 8px;}

        .invite-popup-btn {

            display: block; width: 100%; padding: 10px 12px; margin-bottom: 6px;

            background: #fdfcf8; border: 1px solid #E0D8C8; border-radius: 10px;

            text-align: left; font-size: 14px; font-weight: 600; color: #1B2A3B;

            cursor: pointer; transition: 0.2s;

        }

        .invite-popup-btn:last-child { margin-bottom: 0; }

        .invite-popup-btn:hover { background: rgba(212, 175, 55, 0.1); border-color: #D4AF37; transform: translateX(3px); }

    </style>

 

    <div class=»st-tinder-wrapper» id=»tinder-main-wrapper»>

        <div class=»card-container» id=»st-tinder-app»>

            <div id=»st-loader» style=»position:absolute; top:50%; width:100%; text-align:center; font-family:’Playfair Display’, serif; color:#1B2A3B;»>

                <h3>Ищем компанию… 🍷</h3>

            </div>

        </div>

    </div>

 

    <script>

    const stApp = (function() {

        const ajaxUrl = ‘<?php echo admin_url(‘admin-ajax.php’); ?>’;

        let deck = [];

        let excludedIds = [];

        let isLoading = false;

        const container = document.getElementById(‘st-tinder-app’);

 

        const iconMap = {

            ‘Виски’: ‘🥃’, ‘Водка’: ‘🧊’, ‘Джин’: ‘🍸’, ‘Ром’: ‘🏴‍☠️’, ‘Текила’: ‘🌵’, ‘Мескаль’: ‘🐛’,

            ‘Коньяк’: ‘🥃’, ‘Арманьяк’: ‘🏺’, ‘Бренди’: ‘🍷’, ‘Ликеры’: ‘🍒’, ‘Настойки’: ‘🌿’,

            ‘Тихие вина’: ‘🍷’, ‘Игристые вина’: ‘🥂’, ‘Крепленые вина’: ‘🍷’, ‘Вермуты’: ‘🍸’,

            ‘Пиво’: ‘🍺’, ‘Сидр’: ‘🍏’

        };

 

        async function fetchProfiles() {

            if (isLoading) return;

            isLoading = true;

            document.getElementById(‘st-loader’).style.display = ‘block’;

 

            const formData = new FormData();

            formData.append(‘action’, ‘st_get_tinder_profiles’);

            formData.append(‘exclude_ids’, JSON.stringify(excludedIds));

 

            try {

                const response = await fetch(ajaxUrl, { method: ‘POST’, body: formData });

                const res = await response.json();

                

                if (res.success && res.data.length > 0) {

                    deck.push(…res.data);

                    renderDeck();

                } else {

                    excludedIds = []; 

                    setTimeout(fetchProfiles, 500);

                }

            } catch (e) { console.error(e); } finally { isLoading = false; }

        }

 

        function renderDeck() {

            container.querySelectorAll(‘.tinder-card’).forEach(c => c.remove());

            if (deck.length === 0) return fetchProfiles();

            document.getElementById(‘st-loader’).style.display = ‘none’;

            

            const visibleDeck = deck.slice(0, 3).reverse();

            

            visibleDeck.forEach((user, i) => {

                const index = visibleDeck.length — 1 — i;

                const card = document.createElement(‘div’);

                card.className = ‘tinder-card’;

                card.setAttribute(‘data-id’, user.id);

                card.style.transform = `translateY(${index * 8}px) scale(${1 — index * 0.03})`;

                card.style.zIndex = 100 — index;

 

                // Слайдер

                const slidesHtml = user.photos.map((src, idx) => 

                    `<img src=»${src}» class=»st-slide ${idx === 0 ? ‘active’ : »}» data-idx=»${idx}»>`

                ).join(»);

                

                const dotsHtml = user.photos.length > 1 ? 

                    `<div class=»st-slider-indicators»>

                        ${user.photos.map((_, idx) => `<div class=»st-slider-dot ${idx === 0 ? ‘active’ : »}»></div>`).join(»)}

                    </div>` : »;

 

                // Кнопка листания фото (СЛЕВА ВЕРХУ)

                const photoNavHtml = user.photos.length > 1 ? `

                    <div class=»top-btn-wrapper left»>

                        <button class=»btn-glass btn-photo-nav» title=»Листать фото»>

                            <svg viewBox=»0 0 24 24″><rect x=»3″ y=»3″ width=»18″ height=»18″ rx=»2″ ry=»2″/><circle cx=»8.5″ cy=»8.5″ r=»1.5″/><polyline points=»21 15 16 10 5 21″/></svg>

                            <span class=»photo-counter»>1/${user.photos.length}</span>

                        </button>

                    </div>

                ` : »;

 

                // Иконки предпочтений (макс 6 шт)

                let iconsHtml = »;

                if (user.raw_prefs && user.raw_prefs.length > 0) {

                    const uniqueIcons = […new Set(user.raw_prefs.map(p => iconMap[p]).filter(Boolean))].slice(0, 6);

                    if (uniqueIcons.length > 0) {

                        iconsHtml = `<div class=»pref-icons-wrapper»>

                            ${uniqueIcons.map((icon, idx) => `<div class=»pref-icon» style=»animation-delay: ${0.1 * idx}s»>${icon}</div>`).join(»)}

                        </div>`;

                    }

                }

 

                let extraHtml = »;

                if (user.extra_fields && Object.keys(user.extra_fields).length > 0) {

                    extraHtml = `<div class=»extra-fields-list»>`;

                    for (const [key, val] of Object.entries(user.extra_fields)) {

                        extraHtml += `<div class=»extra-row»><strong>${key}</strong> <span>${val}</span></div>`;

                    }

                    extraHtml += `</div>`;

                }

 

                let vibeHtml = user.vibe ? `

                    <div class=»vibe-block»>

                        <div class=»vibe-emoji»>${user.vibe.emoji}</div>

                        <div class=»vibe-text»>

                            <h4>${user.vibe.name}</h4>

                            <p>${user.vibe.description || »}</p>

                        </div>

                    </div>` : »;

 

                card.innerHTML = `

                    <div class=»st-slider»>

                        ${slidesHtml}

                        ${dotsHtml}

                    </div>

                    

                    ${photoNavHtml}

 

                    <div class=»top-btn-wrapper right»>

                        <button class=»btn-glass btn-detail» title=»Подробнее»>

                            <svg class=»icon-info» viewBox=»0 0 24 24″><circle cx=»12″ cy=»12″ r=»10″/><path d=»M12 16v-4″/><path d=»M12 8h.01″/></svg>

                            <svg class=»icon-close» viewBox=»0 0 24 24″><path d=»M18 6L6 18″/><path d=»M6 6l12 12″/></svg>

                        </button>

                    </div>

 

                    ${iconsHtml}

                    

                    <div class=»profile-overlay»></div>

                    

                    <div class=»profile-info»>

                        <h2 class=»profile-name»>${user.name}${user.age ? `, <span style=»font-size:24px; font-weight:400;»>${user.age}</span>` : »}</h2>

                        <p class=»profile-bio»>${user.bio}</p>

                        <div class=»tags»>${user.tags.map(t => `<span class=»tag»>${t}</span>`).join(»)}</div>

                    </div>

 

                    <div class=»details-panel»>

                        <h2 style=»margin-top:0; margin-bottom:10px; font-family:’Playfair Display’, serif;»>О себе</h2>

                        <p style=»line-height:1.6; margin-bottom:10px; color:#555;»>${user.full_bio || ‘Пока не заполнено’}</p>

                        ${vibeHtml}

                        ${extraHtml}

                    </div>

 

                    <div class=»invite-popup-menu»>

                        <div class=»invite-popup-title»>Доступные пространства</div>

                        <button class=»invite-popup-btn» data-space=»🌐 Открытый зал»>🌐 Открытый зал</button>

                        <button class=»invite-popup-btn» data-space=»📍 Закрытая Дегустация»>📍 Закрытая Дегустация</button>

                        <button class=»invite-popup-btn» data-space=»🎉 VIP Lounge»>🎉 VIP Lounge</button>

                    </div>

 

                    <div class=»actions»>

                        <button class=»btn-action btn-skip» title=»Пропустить»>✖</button>

                        <button class=»btn-action btn-invite» title=»Пространства»>🥂</button>

                    </div>

                `;

 

                initCardEvents(card, user);

                container.appendChild(card);

            });

        }

 

        function initCardEvents(card, user) {

            // Листание фото через новую кнопку

            let currentPhoto = 0;

            const slides = card.querySelectorAll(‘.st-slide’);

            const dots = card.querySelectorAll(‘.st-slider-dot’);

            const counter = card.querySelector(‘.photo-counter’);

            const maxPhotos = slides.length;

 

            const btnPhotoNav = card.querySelector(‘.btn-photo-nav’);

            if(btnPhotoNav) {

                btnPhotoNav.addEventListener(‘click’, (e) => {

                    e.stopPropagation();

                    slides[currentPhoto].classList.remove(‘active’);

                    if(dots.length) dots[currentPhoto].classList.remove(‘active’);

                    

                    currentPhoto = (currentPhoto + 1) % maxPhotos; // Цикличный переход

                    

                    slides[currentPhoto].classList.add(‘active’);

                    if(dots.length) dots[currentPhoto].classList.add(‘active’);

                    counter.innerText = `${currentPhoto + 1}/${maxPhotos}`;

                });

            }

 

            // Кнопка «Детали» (Стильная анимация SVG)

            const btnDetail = card.querySelector(‘.btn-detail’);

            const detailsPanel = card.querySelector(‘.details-panel’);

            btnDetail.addEventListener(‘click’, (e) => {

                e.stopPropagation();

                btnDetail.classList.toggle(‘active’);

                detailsPanel.classList.toggle(‘active’);

            });

 

            // Pop-up меню «Доступные пространства»

            const popupMenu = card.querySelector(‘.invite-popup-menu’);

            const btnInvite = card.querySelector(‘.btn-invite’);

            

            btnInvite.addEventListener(‘click’, (e) => {

                e.stopPropagation();

                document.querySelectorAll(‘.invite-popup-menu’).forEach(p => {

                    if (p !== popupMenu) p.classList.remove(‘active’);

                });

                popupMenu.classList.toggle(‘active’);

            });

 

            popupMenu.querySelectorAll(‘.invite-popup-btn’).forEach(btn => {

                btn.addEventListener(‘click’, (e) => {

                    e.stopPropagation();

                    popupMenu.classList.remove(‘active’);

                    const space = e.target.getAttribute(‘data-space’);

                    processSpaceAction(card, user, space);

                });

            });

 

            document.addEventListener(‘click’, () => { popupMenu.classList.remove(‘active’); });

 

            card.querySelector(‘.btn-skip’).addEventListener(‘click’, () => triggerSwipe(card, ‘skip’));

 

            // Логика свайпов (Умное разделение скролла и свайпа)

            let startX = 0, startY = 0, currentX = 0, isDragging = false, isScrolling = false;

 

            card.addEventListener(‘pointerdown’, e => {

                if (e.target.closest(‘button’) || e.target.closest(‘.st-thumb-actions’)) return; 

                if (container.lastElementChild !== card) return;

                

                startX = e.clientX;

                startY = e.clientY;

                isDragging = true;

                isScrolling = false;

                card.style.transition = ‘none’;

            });

 

            card.addEventListener(‘pointermove’, e => {

                if (!isDragging) return;

 

                const deltaX = e.clientX — startX;

                const deltaY = e.clientY — startY;

 

                // Если движение больше вертикальное и превысило 5px — это скролл текста

                if (!isScrolling && Math.abs(deltaY) > Math.abs(deltaX) && Math.abs(deltaY) > 5) {

                    isScrolling = true;

                }

 

                if (isScrolling) {

                    isDragging = false;

                    card.style.transform = »;

                    return; // Отдаем управление нативному скроллу браузера

                }

 

                // Захватываем pointer (блокируем скролл), только когда явно начался горизонтальный свайп

                if (Math.abs(deltaX) > 10 && !card.hasPointerCapture(e.pointerId)) {

                    card.setPointerCapture(e.pointerId);

                }

 

                currentX = deltaX;

                card.style.transform = `translateX(${currentX}px) rotate(${currentX * 0.04}deg)`;

            });

 

            const endSwipe = (e) => {

                if (!isDragging) return;

                isDragging = false;

                

                if (card.hasPointerCapture(e.pointerId)) {

                    card.releasePointerCapture(e.pointerId);

                }

                

                card.style.transition = ‘transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease’;

                

                // Снижен порог срабатывания с 100px до 80px для удобства на узких экранах

                if (currentX > 80) triggerSwipe(card, ‘like’, user);

                else if (currentX < -80) triggerSwipe(card, ‘skip’, user);

                else card.style.transform = »;

                

                currentX = 0;

            };

 

            card.addEventListener(‘pointerup’, endSwipe);

            card.addEventListener(‘pointercancel’, endSwipe);

        }

 

        function processSpaceAction(card, user, spaceName) {

            alert(`Отправлено приглашение в: ${spaceName}`);

            triggerSwipe(card, ‘invite’);

        }

 

        function triggerSwipe(card, action) {

            const userId = parseInt(card.getAttribute(‘data-id’));

            excludedIds.push(userId);

            deck.shift();

            

            card.style.transform = action === ‘skip’ ? `translate(-150%, -50px) rotate(-20deg)` : `translate(150%, -50px) rotate(20deg)`;

            card.style.opacity = ‘0’;

 

            setTimeout(() => {

                card.remove();

                updateDepths();

            }, 300);

        }

 

        function updateDepths() {

            const cards = Array.from(container.querySelectorAll(‘.tinder-card’)).reverse();

            cards.forEach((card, i) => {

                card.style.transform = `translateY(${i * 8}px) scale(${1 — i * 0.03})`;

                card.style.zIndex = 100 — i;

            });

            if(cards.length < 2) fetchProfiles();

        }

 

        return {

            init: () => {

                fetchProfiles();

            }

        };

    })();

 

    document.addEventListener(«DOMContentLoaded», stApp.init);

    </script>

    <?php

    return ob_get_clean();

}

 

 

/**

 * Стилизация страниц входа и регистрации (wp-login.php)

 * в стиле Spirit Trade Digital Modules.

 * Вставьте этот код в functions.php вашей активной темы.

 */

 

add_action( ‘login_enqueue_scripts’, ‘st_custom_login_styles’ );

function st_custom_login_styles() {

    // Основные переменные и стили

    ?>

    <style>

        /* Импорт комикс-шрифта (аналогично плагину) */

        @import url(«https://fonts.googleapis.com/css2?family=Neucha&family=Playfair+Display:wght@400;700;900&family=Lato:wght@400;700&display=swap»);

 

        body.login {

            background: #fdfcf8; /* бумажный фон */

            background-image: radial-gradient(#1a1a1a 0.8px, transparent 0.8px);

            background-size: 24px 24px;

            font-family: ‘Lato’, ‘Helvetica Neue’, sans-serif;

        }

 

        .login h1 a {

            background-image: none; /* убираем стандартный логотип WP */

            width: auto;

            height: auto;

            text-indent: 0;

            margin-bottom: 20px;

            font-family: ‘Playfair Display’, serif;

            font-size: 32px;

            font-weight: 900;

            color: #1B2A3B;

            text-decoration: none;

            display: flex;

            justify-content: center;

            align-items: center;

            gap: 10px;

        }

 

        .login h1 a::before {

            content: «⚓»;

            font-size: 42px;

            background: #D4AF37;

            color: #1B2A3B;

            display: inline-block;

            padding: 8px 16px;

            border-radius: 20px 4px 20px 4px;

            border: 2px solid #1a1a1a;

            box-shadow: 4px 4px 0 #1a1a1a;

            transform: rotate(-2deg);

            margin-right: 10px;

        }

 

        .login h1 a::after {

            content: «SPIRIT TRADE»;

            font-size: 28px;

            letter-spacing: 1px;

        }

 

        /* Контейнер формы */

        #login {

            padding: 20px;

            background: rgba(253, 252, 248, 0.95);

            border-radius: 16px;

            border: 2px solid #1a1a1a;

            box-shadow: 8px 8px 0 #1a1a1a;

            backdrop-filter: blur(2px);

            width: 380px;

        }

 

        @media (max-width: 480px) {

            #login { width: 90%; padding: 15px; }

            .login h1 a::after { font-size: 22px; }

            .login h1 a::before { font-size: 32px; padding: 4px 12px; }

        }

 

        /* Поля ввода */

        .login form {

            background: transparent;

            border: none;

            box-shadow: none;

            padding: 0;

        }

 

        .login .input, .login input[type=»text»], 

        .login input[type=»password»], .login input[type=»email»] {

            background: #ffffff;

            border: 2px solid #1a1a1a;

            border-radius: 8px 2px 8px 2px;

            box-shadow: 3px 3px 0 rgba(0,0,0,0.1);

            font-family: ‘Lato’, sans-serif;

            font-size: 15px;

            padding: 10px 12px;

            transition: 0.2s;

        }

 

        .login .input:focus, .login input[type=»text»]:focus, 

        .login input[type=»password»]:focus, .login input[type=»email»]:focus {

            border-color: #D4AF37;

            box-shadow: 5px 5px 0 #1a1a1a;

            outline: none;

            transform: translateY(-2px);

        }

 

        /* Кнопка отправки */

        .login .button-primary {

            background: #c1ff00 !important; /* маркерный зелёный */

            border: 2px solid #1a1a1a !important;

            color: #1a1a1a !important;

            font-weight: 900;

            text-transform: uppercase;

            letter-spacing: 1px;

            font-family: ‘Neucha’, cursive;

            font-size: 16px;

            padding: 8px 20px;

            border-radius: 8px 2px 8px 2px;

            box-shadow: 4px 4px 0 #1a1a1a;

            transition: 0.2s;

            text-shadow: none;

        }

 

        .login .button-primary:hover {

            background: #aae000 !important;

            transform: translateY(-2px);

            box-shadow: 6px 6px 0 #1a1a1a;

        }

 

        .login .button-primary:active {

            transform: translateY(2px);

            box-shadow: 2px 2px 0 #1a1a1a;

        }

 

        /* Ссылки «Забыли пароль?» и «Регистрация» */

        .login #nav a, .login #backtoblog a {

            color: #1B2A3B;

            font-weight: 600;

            text-decoration: none;

            border-bottom: 2px dotted #D4AF37;

            transition: 0.2s;

        }

 

        .login #nav a:hover, .login #backtoblog a:hover {

            color: #D4AF37;

            border-bottom-style: solid;

        }

 

        /* Сообщения об ошибках и уведомления */

        .login .message, .login .notice, .login .error {

            background: #fffbea;

            border-left: 4px solid #D4AF37;

            box-shadow: 2px 2px 0 #1a1a1a;

            border-radius: 6px;

            font-weight: 500;

        }

 

        .login .error {

            border-left-color: #ff5757;

        }

 

        /* Адаптивная чекбокс «Запомнить меня» */

        .login .forgetmenot label {

            font-weight: 600;

            color: #1B2A3B;

        }

 

        /* Дополнительный стиль для формы регистрации */

        .login form .input {

            margin-bottom: 12px;

        }

    </style>

    <?php

}

 

// Изменяем URL логотипа на главную страницу сайта

add_filter( ‘login_headerurl’, ‘st_custom_login_logo_url’ );

function st_custom_login_logo_url() {

    return home_url();

}

 

// Изменяем title атрибут логотипа

add_filter( ‘login_headertext’, ‘st_custom_login_logo_title’ );

function st_custom_login_logo_title() {

    return get_bloginfo( ‘name’ ) . ‘ – Spirit Trade Community’;

}

 

// Добавляем поддержку кастомного CSS для страницы регистрации (используется тот же хук login_enqueue_scripts)

// При желании можно добавить специальные стили для страницы восстановления пароля.

 

// Убираем лишние стандартные стили WP, если они конфликтуют (опционально)

add_action( ‘login_init’, ‘st_remove_default_login_styles’ );

function st_remove_default_login_styles() {

    wp_dequeue_style( ‘login’ );

    wp_enqueue_style( ‘st-login-custom’, false );

}

 

// Убедимся, что наш стиль применяется после всех остальных

add_action( ‘login_enqueue_scripts’, ‘st_override_login_styles_order’, 99 );

function st_override_login_styles_order() {

    // Ничего дополнительно не делаем, так как наш стиль уже выведен через echo

    // Этот хук гарантирует его позднее выполнение

}

 

 

/**

 * Перенаправление после логина на страницу /at-reg-pp/

 */

add_filter( ‘login_redirect’, ‘st_custom_login_redirect’, 10, 3 );

function st_custom_login_redirect( $redirect_to, $request, $user ) {

    if ( ! isset( $user->ID ) ) {

        return $redirect_to;

    }

 

    // Перенаправляем всех на https://i.spirittrade.kz/at-reg-pp/

    $custom_url = home_url( ‘/at-reg-pp/’ ); // ← заменено на вашу страницу

 

    return $custom_url;

}

 

// ==============================================================================

// ПРЯМОЙ ЗАГРУЗЧИК ФАЙЛОВ ДЛЯ АНКЕТЫ (ШАГ 4)

// ==============================================================================

 

add_action( ‘wp_ajax_st_upload_user_photo’, ‘st_ajax_upload_user_photo’ );

function st_ajax_upload_user_photo() {

    // Проверка авторизации

    if ( ! is_user_logged_in() ) {

        wp_send_json_error( array( ‘message’ => ‘Вы не авторизованы’ ) );

    }

    // Проверка nonce

    if ( ! isset( $_POST[‘nonce’] ) || ! wp_verify_nonce( $_POST[‘nonce’], ‘st_al_nonce_action’ ) ) {

        wp_send_json_error( array( ‘message’ => ‘Неверный nonce’ ) );

    }

 

    $user_id = get_current_user_id();

 

    if ( empty( $_FILES[‘photo’] ) || $_FILES[‘photo’][‘error’] !== UPLOAD_ERR_OK ) {

        wp_send_json_error( array( ‘message’ => ‘Файл не загружен’ ) );

    }

 

    $file = $_FILES[‘photo’];

    $allowed_mimes = array( ‘image/jpeg’, ‘image/png’, ‘image/gif’, ‘image/webp’ );

    $finfo = finfo_open( FILEINFO_MIME_TYPE );

    $mime_type = finfo_file( $finfo, $file[‘tmp_name’] );

    finfo_close( $finfo );

 

    if ( ! in_array( $mime_type, $allowed_mimes ) ) {

        wp_send_json_error( array( ‘message’ => ‘Разрешены только JPEG, PNG, GIF или WEBP’ ) );

    }

 

    $upload_dir = wp_upload_dir();

    $user_subdir = ‘st-user-uploads/’ . $user_id;

    $user_path = $upload_dir[‘basedir’] . ‘/’ . $user_subdir;

    if ( ! file_exists( $user_path ) ) {

        wp_mkdir_p( $user_path );

    }

 

    $ext = pathinfo( $file[‘name’], PATHINFO_EXTENSION );

    $new_filename = ‘photo_’ . time() . ‘_’ . mt_rand( 1000, 9999 ) . ‘.’ . $ext;

    $dest_path = $user_path . ‘/’ . $new_filename;

 

    if ( ! move_uploaded_file( $file[‘tmp_name’], $dest_path ) ) {

        wp_send_json_error( array( ‘message’ => ‘Не удалось сохранить файл’ ) );

    }

 

    // Оптимизация изображения

    $image = wp_get_image_editor( $dest_path );

    if ( ! is_wp_error( $image ) ) {

        $size = $image->get_size();

        if ( $size[‘width’] > 1200 || $size[‘height’] > 1200 ) {

            $image->resize( 1200, 1200, false );

        }

        $image->set_quality( 85 );

        $image->save( $dest_path );

    }

 

    $file_url = $upload_dir[‘baseurl’] . ‘/’ . $user_subdir . ‘/’ . $new_filename;

    wp_send_json_success( array( ‘url’ => $file_url ) );

}

 

// ==============================================================================

// 8. МОДУЛЬ ПРЕЗЕНТАЦИИ УСЛУГ (SERVICE TINDER) — v5.4 (UI & Telegram Patch)

// ==============================================================================

 

// 1. Инициализация таблиц

add_action( ‘init’, ‘st_srv_install_tables_v52’ );

function st_srv_install_tables_v52() {

    global $wpdb;

    $table_deck = $wpdb->prefix . ‘st_services_deck_v2’;

    $table_swipes = $wpdb->prefix . ‘st_srv_swipes’;

    $charset_collate = $wpdb->get_charset_collate();

 

    require_once( ABSPATH . ‘wp-admin/includes/upgrade.php’ );

 

    $sql_deck = «CREATE TABLE IF NOT EXISTS $table_deck (

        id INT UNSIGNED NOT NULL AUTO_INCREMENT,

        title VARCHAR(255) NOT NULL,

        processes LONGTEXT NOT NULL,

        cover_image VARCHAR(255) DEFAULT »,

        icon VARCHAR(10) DEFAULT ‘📌’,

        category TEXT DEFAULT », 

        price VARCHAR(30) DEFAULT »,

        old_price VARCHAR(30) DEFAULT »,

        extra_data LONGTEXT,

        PRIMARY KEY  (id)

    ) $charset_collate;»;

    dbDelta( $sql_deck );

 

    $sql_swipes = «CREATE TABLE IF NOT EXISTS $table_swipes (

        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,

        user_id BIGINT UNSIGNED NOT NULL,

        service_id INT UNSIGNED NOT NULL,

        action VARCHAR(20) NOT NULL,

        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,

        PRIMARY KEY  (id),

        UNIQUE KEY user_service (user_id, service_id)

    ) $charset_collate;»;

    dbDelta( $sql_swipes );

 

    $cols = $wpdb->get_col(«DESCRIBE $table_deck»);

    if (!in_array(‘extra_data’, $cols)) $wpdb->query(«ALTER TABLE $table_deck ADD COLUMN extra_data LONGTEXT»);

    $wpdb->query(«ALTER TABLE $table_deck MODIFY COLUMN category TEXT»);

}

 

function st_srv_get_alcohol_categories() {

    return [

        ‘Виски’ => ‘🥃’, ‘Водка’ => ‘🧊’, ‘Джин’ => ‘🍸’, ‘Ром’ => ‘🏴‍☠️’, 

        ‘Текила’ => ‘🌵’, ‘Мескаль’ => ‘🐛’, ‘Коньяк’ => ‘🥃’, ‘Арманьяк’ => ‘🏺’, 

        ‘Бренди’ => ‘🍷’, ‘Ликеры’ => ‘🍒’, ‘Настойки’ => ‘🌿’, ‘Тихие вина’ => ‘🍷’, 

        ‘Игристые вина’ => ‘🥂’, ‘Крепленые вина’ => ‘🍷’, ‘Вермуты’ => ‘🍸’, 

        ‘Пиво’ => ‘🍺’, ‘Сидр’ => ‘🍏’

    ];

}

 

// ==============================================================================

// 8.1 AJAX ОБРАБОТЧИКИ

// ==============================================================================

 

add_action( ‘wp_ajax_st_srv_get_deck’, ‘st_srv_ajax_get_deck’ );

add_action( ‘wp_ajax_nopriv_st_srv_get_deck’, ‘st_srv_ajax_get_deck’ );

function st_srv_ajax_get_deck() {

    global $wpdb;

    $table_name = $wpdb->prefix . ‘st_services_deck_v2’;

    

    $results = $wpdb->get_results( «SELECT * FROM $table_name ORDER BY id DESC LIMIT 100″, ARRAY_A );

    if (empty($results)) wp_send_json_error([‘message’ => ‘Нет данных’]);

    

    foreach ($results as &$r) {

        $r[‘processes’] = json_decode($r[‘processes’], true) ?: [];

        $r[‘category’] = json_decode($r[‘category’], true) ?: [];

        $r[‘extra_data’] = json_decode($r[‘extra_data’], true) ?: [];

    }

    wp_send_json_success( $results );

}

 

add_action(‘wp_ajax_st_srv_save_service’, ‘st_srv_ajax_save_service’);

add_action(‘wp_ajax_nopriv_st_srv_save_service’, ‘st_srv_ajax_save_service’);

function st_srv_ajax_save_service() {

    global $wpdb;

    $table_name = $wpdb->prefix . ‘st_services_deck_v2’;

 

    $title = sanitize_text_field($_POST[‘srv_title’] ?? »);

    if (empty($title)) wp_send_json_error([‘message’ => ‘Название обязательно’]);

 

    $extra_keys = $_POST[‘extra_key’] ?? [];

    $extra_vals = $_POST[‘extra_val’] ?? [];

    $extra_data = [];

    for ($i = 0; $i < count($extra_keys); $i++) {

        if (!empty($extra_keys[$i])) $extra_data[sanitize_text_field($extra_keys[$i])] = sanitize_text_field($extra_vals[$i] ?? »);

    }

 

    // Авто-генерация артикула, если он не передан

    if (!isset($extra_data[‘Артикул’]) || empty($extra_data[‘Артикул’])) {

        $extra_data[‘Артикул’] = ‘ST-‘ . strtoupper(substr(md5(uniqid()), 0, 6));

    }

 

    $data = [

        ‘title’      => $title,

        ‘icon’       => sanitize_text_field($_POST[‘srv_icon’] ?? ‘📌’),

        ‘cover_image’=> esc_url_raw($_POST[‘srv_image’] ?? »),

        ‘price’      => sanitize_text_field($_POST[‘srv_price’] ?? »),

        ‘old_price’  => sanitize_text_field($_POST[‘srv_old_price’] ?? »),

        ‘category’   => wp_json_encode(isset($_POST[‘srv_category’]) ? array_map(‘sanitize_text_field’, $_POST[‘srv_category’]) : []),

        ‘extra_data’ => wp_json_encode($extra_data),

        ‘processes’  => wp_json_encode([])

    ];

 

    if ($wpdb->insert($table_name, $data) === false) wp_send_json_error([‘message’ => ‘Ошибка БД’]);

    wp_send_json_success([‘id’ => $wpdb->insert_id]);

}

 

add_action(‘wp_ajax_st_srv_bulk_save’, ‘st_srv_ajax_bulk_save’);

add_action(‘wp_ajax_nopriv_st_srv_bulk_save’, ‘st_srv_ajax_bulk_save’);

function st_srv_ajax_bulk_save() {

    if (!isset($_POST[‘st_srv_bulk_nonce’]) || !wp_verify_nonce($_POST[‘st_srv_bulk_nonce’], ‘st_srv_bulk_import’)) wp_send_json_error([‘message’ => ‘Security fail’]);

    global $wpdb;

    $table_name = $wpdb->prefix . ‘st_services_deck_v2’;

    $items = json_decode(stripslashes($_POST[‘bulk_data’] ?? »), true);

    if (!is_array($items)) wp_send_json_error([‘message’ => ‘Invalid JSON’]);

 

    $inserted = 0;

    foreach ($items as $item) {

        $title = sanitize_text_field($item[‘title’] ?? »);

        if (empty($title)) continue;

        

        $extra_data = $item[‘extra_data’] ?? [];

        if (!isset($extra_data[‘Артикул’]) || empty($extra_data[‘Артикул’])) {

            $extra_data[‘Артикул’] = ‘ST-‘ . strtoupper(substr(md5(uniqid()), 0, 6));

        }

 

        $res = $wpdb->insert($table_name, [

            ‘title’ => $title, ‘icon’ => sanitize_text_field($item[‘icon’] ?? ‘📌’), ‘cover_image’ => esc_url_raw($item[‘cover_image’] ?? »),

            ‘price’ => sanitize_text_field($item[‘price’] ?? »), ‘old_price’ => sanitize_text_field($item[‘old_price’] ?? »),

            ‘category’ => wp_json_encode($item[‘category’] ?? [], JSON_UNESCAPED_UNICODE),

            ‘extra_data’ => wp_json_encode($extra_data, JSON_UNESCAPED_UNICODE),

            ‘processes’ => wp_json_encode($item[‘processes’] ?? [], JSON_UNESCAPED_UNICODE)

        ]);

        if ($res) $inserted++;

    }

    wp_send_json_success([‘inserted’ => $inserted, ‘total’ => count($items)]);

}

 

// Запись свайпа

add_action(‘wp_ajax_st_srv_record_swipe’, ‘st_srv_ajax_record_swipe’);

add_action(‘wp_ajax_nopriv_st_srv_record_swipe’, ‘st_srv_ajax_record_swipe’);

function st_srv_ajax_record_swipe() {

    if (!$user_id = get_current_user_id()) wp_send_json_success([‘saved_to_cache’ => true]); 

    global $wpdb;

    $wpdb->replace($wpdb->prefix . ‘st_srv_swipes’, [

        ‘user_id’ => $user_id, ‘service_id’ => intval($_POST[‘service_id’]),

        ‘action’ => sanitize_text_field($_POST[‘swipe_action’]), ‘created_at’ => current_time(‘mysql’)

    ]);

    wp_send_json_success();

}

 

add_action(‘wp_ajax_st_srv_get_history’, ‘st_srv_ajax_get_history’);

add_action(‘wp_ajax_nopriv_st_srv_get_history’, ‘st_srv_ajax_get_history’);

function st_srv_ajax_get_history() {

    if (!$user_id = get_current_user_id()) wp_send_json_error();

    global $wpdb;

    $results = $wpdb->get_results($wpdb->prepare(»

        SELECT d.*, s.action, s.created_at FROM {$wpdb->prefix}st_srv_swipes s 

        JOIN {$wpdb->prefix}st_services_deck_v2 d ON s.service_id = d.id 

        WHERE s.user_id = %d ORDER BY s.created_at DESC

    «, $user_id), ARRAY_A);

    foreach ($results as &$r) $r[‘category’] = json_decode($r[‘category’], true) ?: [];

    wp_send_json_success($results);

}

 

// Отправка запроса в Telegram

add_action(‘wp_ajax_st_srv_send_request’, ‘st_srv_ajax_send_request’);

add_action(‘wp_ajax_nopriv_st_srv_send_request’, ‘st_srv_ajax_send_request’);

function st_srv_ajax_send_request() {

    $product = sanitize_text_field($_POST[‘product’] ?? »);

    $email = sanitize_email($_POST[’email’] ?? »);

    $note = sanitize_textarea_field($_POST[‘note’] ?? »);

 

    // === ВАЖНО: Вставьте сюда данные вашего бота ===

    $tg_token = ‘ВАШ_ТОКЕН_БОТА’; 

    $tg_chat_id = ‘ВАШ_CHAT_ID’;

    // ===============================================

 

    $message = «🔥 *Новый запрос из Tinder!*\n\n»;

    $message .= «📦 *Товар:* {$product}\n»;

    $message .= «📧 *Email:* {$email}\n»;

    if($note) $message .= «📝 *Примечание:* {$note}»;

 

    if($tg_token != ‘ВАШ_ТОКЕН_БОТА’) {

        $url = «https://api.telegram.org/bot{$tg_token}/sendMessage»;

        wp_remote_post($url, [‘body’ => [‘chat_id’ => $tg_chat_id, ‘text’ => $message, ‘parse_mode’ => ‘Markdown’]]);

    }

 

    wp_send_json_success([‘message’ => ‘Запрос успешно отправлен!’]);

}

 

// Удаление сервиса (таблица)

add_action(‘wp_ajax_st_srv_delete_service’, ‘st_srv_ajax_delete_service’);

add_action(‘wp_ajax_nopriv_st_srv_delete_service’, ‘st_srv_ajax_delete_service’);

function st_srv_ajax_delete_service() {

    global $wpdb;

    $id = intval($_POST[‘id’] ?? 0);

    if (!$id || $wpdb->delete($wpdb->prefix . ‘st_services_deck_v2’, [‘id’ => $id]) === false) wp_send_json_error([‘message’ => ‘Ошибка’]);

    wp_send_json_success();

}

 

// ==============================================================================

// 8.2 ШОРТКОД TINDER

Сборка коллекции…

Фильтры

Виски
Водка
Джин
Ром
Текила
Мескаль
Коньяк
Арманьяк
Бренди
Ликеры
Настойки
Тихие вина
Игристые вина
Крепленые вина
Вермуты
Пиво
Сидр

История свайпов

Мои заметки

Заметка к товару

// ==============================================================================

add_shortcode( ‘st_service_tinder’, ‘st_srv_render_tinder’ );

function st_srv_render_tinder() {

    $current_user_id = get_current_user_id();

    $cats = st_srv_get_alcohol_categories();

    ob_start();

    ?>

    <style>

        body.st-locked { overflow: hidden !important; position: fixed; width: 100%; height: 100%; overscroll-behavior-y: none; touch-action: none; }

        

        .st-srv-wrapper {

            —marker-yellow: #ffde59; —marker-green: #c1ff00; —marker-red: #ff5757;

            —ink-black: #1a1a1a; —bg-paper: #f0f2f5;

            display: flex; justify-content: center; align-items: center; width: 100%; max-width: 480px; 

            height: 100dvh; max-height: 900px; padding-bottom: env(safe-area-inset-bottom);

            box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, «Segoe UI», Roboto, sans-serif;

            position: relative; margin: 0 auto; overscroll-behavior-y: none;

        }

        .st-srv-app { width: 100%; height: 100%; background: var(—bg-paper); display: flex; flex-direction: column; position: relative; overflow: hidden;}

        @media (min-width: 481px) { .st-srv-app { height: 95dvh; border-radius: 24px; border: 1px solid #eee; } }

 

        .st-srv-cards-area { position: absolute; top: 0; left: 0; width: 100%; height: 100%; padding: 10px; box-sizing: border-box; touch-action: none; overflow: hidden; }

        

        .srv-card { 

            position: absolute; top: 10px; left: 10px; right: 10px; bottom: 10px; 

            background: #fff; border-radius: 20px; border: 1px solid #e8e8e8; 

            box-shadow: 0 4px 15px rgba(0,0,0,0.05); display: flex; flex-direction: column; 

            overflow: hidden; will-change: transform; transition: transform 0.4s ease, opacity 0.4s ease; 

            cursor: grab; contain: layout paint; 

        }

        .srv-card:active { cursor: grabbing; }

        

        .srv-card-thumb-wrap { position: relative; padding: 15px; text-align: center; background: #fafafa; border-bottom: 1px solid #eee; display: flex; flex-direction: column; align-items: center; }

        .srv-card-thumb { width: 120px; height: 120px; border-radius: 16px; object-fit: cover; box-shadow: 0 4px 12px rgba(0,0,0,0.1); border: 2px solid #fff; background: #f0f0f0; }

        .st-thumb-actions { position: absolute; top: 15px; right: 15px; display: flex; flex-direction: column; gap: 8px; z-index: 5; }

        .btn-action-icon { background: #fff; border: 1px solid #ddd; border-radius: 50%; width: 36px; height: 36px; cursor: pointer; display: flex; justify-content: center; align-items: center; font-size: 16px; }

        

        .srv-card-title { font-size: 20px; font-weight: 800; margin: 15px 0 0; color: var(—ink-black); text-align: center; }

        

        .srv-card-data { padding: 15px 15px 90px; display: flex; flex-direction: column; flex: 1; overflow-y: auto; touch-action: pan-y; -webkit-overflow-scrolling: touch; }

        

        .srv-cat-tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 5px; margin-bottom: 15px; }

        .srv-cat-tag { background: #f0f0f0; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: uppercase;}

        .st-char-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: auto; background: #fdfcf8; padding: 12px; border-radius: 12px; border: 1px dashed #ccc; margin-bottom: 15px; }

        .st-char-item { font-size: 12px; display: flex; flex-direction: column; gap: 2px; }

        .st-char-item strong { color: #888; font-size: 10px; text-transform: uppercase; }

        .st-price-val { font-size: 26px; font-weight: 900; text-align: center; margin-bottom: 10px; color: #1a1a1a; }

        .btn-details { width: 100%; padding: 10px; border: 1px solid var(—ink-black); background: #fff; border-radius: 12px; font-weight: bold; cursor: pointer; text-transform: uppercase; font-size: 12px; }

 

        /* ИЗМЕНЕННАЯ ПАНЕЛЬ: Меню слева, История по центру, Свайпы справа */

        .st-srv-controls { 

            position: absolute; bottom: 15px; left: 15px; right: 15px; z-index: 100; 

            background: #fff; border-radius: 24px; padding: 10px 15px; 

            box-shadow: 0 3px 10px rgba(0,0,0,0.12); border: 1px solid #eee;

            display: flex; justify-content: space-between; align-items: center; 

        }

        .st-ctrl-left { display: flex; gap: 8px; align-items: center; }

        .st-ctrl-center { display: flex; justify-content: center; flex: 1; }

        .st-ctrl-right { display: flex; gap: 10px; }

 

        .srv-btn { width: 45px; height: 45px; border-radius: 50%; font-size: 22px; border: none; background: #fff; box-shadow: 0 4px 10px rgba(0,0,0,0.08); cursor: pointer; transition: 0.2s; display:flex; justify-content:center; align-items:center; }

        .btn-pass { color: var(—marker-red); border: 1px solid #ffebeb; }

        .btn-match { color: #8bc34a; font-size: 26px; border: 1px solid #f4ffe6; }

        .srv-mini-btn { width: 40px; height: 40px; border-radius: 50%; border: 1px solid #eee; background: #fff; font-size: 16px; display:flex; justify-content:center; align-items:center; cursor: pointer; }

        .st-btn-history { width: auto; padding: 0 20px; border-radius: 20px; font-size: 14px; font-weight: bold; background: var(—ink-black); color: #fff; border: none; height: 40px; cursor: pointer;}

        

        .st-search-wrap { display: flex; align-items: center; background: #fff; border-radius: 20px; transition: 0.3s ease; }

        .st-search-wrap.active { box-shadow: 0 0 0 1px var(—ink-black); padding-left: 10px; }

        .st-search-hidden { width: 0; opacity: 0; border: none; outline: none; padding: 0; transition: 0.3s ease; font-size: 14px; background: transparent; }

        .st-search-wrap.active .st-search-hidden { width: 100px; opacity: 1; padding: 5px; }

 

        /* Модалки */

        .st-modal-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 9999; display: none; justify-content: center; align-items: center; opacity: 0; transition: 0.3s; }

        .st-modal-overlay.active { display: flex; opacity: 1; }

        .st-modal-content { background: #fff; width: 90%; max-height: 85%; border-radius: 20px; padding: 20px; display: flex; flex-direction: column; position: relative; transform: translateY(20px); transition: 0.3s; }

        .st-modal-overlay.active .st-modal-content { transform: translateY(0); }

        .st-modal-close { position: absolute; top: 15px; right: 15px; background: #eee; border: none; width: 30px; height: 30px; border-radius: 50%; font-weight: bold; cursor: pointer; display: flex; justify-content: center; align-items: center; z-index:10; }

        .btn-download-hist { position: absolute; top: 15px; left: 15px; background: #eee; border: none; width: 30px; height: 30px; border-radius: 50%; font-size: 14px; cursor: pointer; display: flex; justify-content: center; align-items: center; transition: 0.2s; z-index:10;}

        .btn-download-hist:hover { background: var(—marker-yellow); }

 

        .st-cat-grid { display: flex; flex-wrap: wrap; gap: 8px; max-height: 300px; overflow-y: auto; margin-bottom: 20px; }

        .st-cat-chip { padding: 8px 14px; border: 1px solid #eee; border-radius: 20px; font-size: 13px; cursor: pointer; user-select: none; font-weight: 500; }

        .st-cat-chip.selected { background: var(—marker-green); border-color: var(—ink-black); font-weight: bold; }

        

        .h-actions { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }

        .h-btn { padding: 6px 10px; border-radius: 8px; font-size: 11px; font-weight: bold; text-decoration: none; display: inline-flex; justify-content: center; flex: 1; border:none; cursor:pointer;}

    </style>

 

    <div class=»st-srv-wrapper» id=»stAppWrapper»>

        <div class=»st-srv-app»>

            <div class=»st-srv-cards-area» id=»srvCardsArea»>

                <div id=»emptyState» style=»text-align:center; color:#888; position:absolute; top:50%; width:100%; transform:translateY(-50%);»>Проверка данных…</div>

            </div>

 

            <div class=»st-srv-controls»>

                <div class=»st-ctrl-left»>

                    <button id=»stFilterToggleBtn» class=»srv-mini-btn»>☰</button>

                    <div class=»st-search-wrap» id=»stSearchWrap»>

                        <input type=»text» id=»stSearchInput» class=»st-search-hidden» placeholder=»Поиск…»>

                        <button id=»stSearchToggleBtn» class=»srv-mini-btn»>🔍</button>

                    </div>

                </div>

                <div class=»st-ctrl-center»>

                    <button id=»btnHistory» class=»st-btn-history»>История</button>

                </div>

                <div class=»st-ctrl-right»>

                    <button class=»srv-btn btn-pass» id=»btnPass»>✕</button>

                    <button class=»srv-btn btn-match» id=»btnMatch»>♥</button>

                </div>

            </div>

        </div>

 

        <div class=»st-modal-overlay» id=»filterModal»>

            <div class=»st-modal-content»>

                <button class=»st-modal-close» onclick=»document.getElementById(‘filterModal’).classList.remove(‘active’)»>✕</button>

                <h3 style=»margin-top:0;»>Меню и Фильтры</h3>

                <div class=»st-cat-grid» id=»stCatGrid»>

                    <?php foreach($cats as $name => $emoji): ?>

                        <div class=»st-cat-chip» data-val=»<?php echo esc_attr($name); ?>»><?php echo $emoji . ‘ ‘ . esc_html($name); ?></div>

                    <?php endforeach; ?>

                </div>

                <button id=»applyFilters» style=»width:100%; padding:12px; background:var(—marker-green); border:2px solid var(—ink-black); border-radius:12px; font-weight:bold; cursor:pointer; margin-bottom:10px;»>Показать результаты</button>

                <button id=»stClearCache» style=»width:100%; padding:10px; background:#eee; border:none; border-radius:12px; cursor:pointer; font-size:12px;»>🔄 Обновить данные с сервера</button>

            </div>

        </div>

 

        <div class=»st-modal-overlay» id=»historyModal»>

            <div class=»st-modal-content» style=»height: 80%;»>

                <button id=»btnDownloadHistory» class=»btn-download-hist» title=»Скачать список CSV»>📥</button>

                <button class=»st-modal-close» onclick=»document.getElementById(‘historyModal’).classList.remove(‘active’)»>✕</button>

                <h3 style=»margin-top:0; text-align: center;»>Ваши свайпы</h3>

                <div id=»historyList» style=»overflow-y:auto; flex:1; padding-right: 5px;»></div>

            </div>

        </div>

 

        <div class=»st-modal-overlay» id=»requestModal»>

            <div class=»st-modal-content» style=»max-height: auto;»>

                <button class=»st-modal-close» onclick=»document.getElementById(‘requestModal’).classList.remove(‘active’)»>✕</button>

                <h3 style=»margin-top:0;»>Запрос информации</h3>

                <p id=»reqProductName» style=»font-size:14px; font-weight:bold; color:var(—ink-black); margin-bottom:15px;»></p>

                <input type=»email» id=»reqEmail» placeholder=»Ваш Email» style=»width:100%; padding:10px; border:1px solid #ccc; border-radius:8px; margin-bottom:10px;»>

                <textarea id=»reqNote» placeholder=»Ваш комментарий…» rows=»4″ style=»width:100%; padding:10px; border:1px solid #ccc; border-radius:8px; margin-bottom:10px;»></textarea>

                <button id=»btnSendReq» style=»width:100%; padding:12px; background:var(—marker-yellow); border:2px solid var(—ink-black); border-radius:12px; font-weight:bold; cursor:pointer;»>Отправить запрос в TG</button>

                <div id=»reqMsg» style=»text-align:center; margin-top:10px; font-size:13px; font-weight:bold;»></div>

            </div>

        </div>

 

        <div class=»st-modal-overlay» id=»imageModal» style=»z-index: 10000; background: rgba(0,0,0,0.85);» onclick=»this.classList.remove(‘active’)»><img src=»» id=»lightboxImg» style=»max-width: 100%; max-height: 70vh; object-fit: contain; border-radius: 12px;» onclick=»event.stopPropagation()»><button class=»st-modal-close» style=»top: 20px; right: 20px;» onclick=»document.getElementById(‘imageModal’).classList.remove(‘active’)»>✕</button></div>

        

        <div class=»st-modal-overlay» id=»detailsModal»>

            <div class=»st-modal-content» style=»padding:0; max-height: 95%; overflow: hidden; border-radius:24px;»>

                <button class=»st-modal-close» style=»background:rgba(255,255,255,0.9); box-shadow:0 2px 10px rgba(0,0,0,0.1);» onclick=»document.getElementById(‘detailsModal’).classList.remove(‘active’)»>✕</button>

                <div id=»detContent» style=»overflow-y: auto; height: 100%; border-radius:24px; background:#fff;»></div>

            </div>

        </div>

    </div>

 

    <script>

    document.addEventListener(‘DOMContentLoaded’, () => {

        document.body.classList.add(‘st-locked’);

        const ajaxUrl = ‘<?php echo admin_url(‘admin-ajax.php’); ?>’;

        const isLogged = <?php echo $current_user_id ? ‘true’ : ‘false’; ?>;

        

        let deck = [], filteredDeck = [], selectedCats = new Set();

        let currentDeckIndex = 0; 

        let currentHistoryItems = []; 

        let currentProductForReq = »;

        

        const cardsArea = document.getElementById(‘srvCardsArea’);

        const filterModal = document.getElementById(‘filterModal’);

        

        // — 1. ЗАГРУЗКА —

        function loadData() {

            const cachedData = localStorage.getItem(‘st_tinder_data’);

            const cachedTime = localStorage.getItem(‘st_tinder_time’);

            if (cachedData && cachedTime && (Date.now() — cachedTime < 3600000)) {

                deck = JSON.parse(cachedData); applyFilters();

            } else {

                fetch(`${ajaxUrl}?action=st_srv_get_deck`).then(r => r.json()).then(res => {

                    if(res.success) { 

                        deck = res.data; 

                        const lightDeck = deck.map(d => ({id: d.id, title: d.title, cover_image: d.cover_image, icon: d.icon, price: d.price, category: d.category, extra_data: d.extra_data}));

                        localStorage.setItem(‘st_tinder_data’, JSON.stringify(lightDeck));

                        localStorage.setItem(‘st_tinder_time’, Date.now());

                        applyFilters(); 

                    } else cardsArea.innerHTML = ‘<div style=»text-align:center; position:absolute; top:50%; width:100%;»>Нет данных</div>’;

                });

            }

        }

        

        document.getElementById(‘stClearCache’).addEventListener(‘click’, () => {

            localStorage.removeItem(‘st_tinder_data’); localStorage.removeItem(‘st_tinder_time’);

            filterModal.classList.remove(‘active’); loadData();

        });

 

        // — 2. ПОИСК И ФИЛЬТРЫ —

        const searchWrap = document.getElementById(‘stSearchWrap’), searchInput = document.getElementById(‘stSearchInput’);

        document.getElementById(‘stSearchToggleBtn’).addEventListener(‘click’, () => { searchWrap.classList.toggle(‘active’); if (searchWrap.classList.contains(‘active’)) searchInput.focus(); });

        document.addEventListener(‘click’, (e) => { if (!searchWrap.contains(e.target) && searchWrap.classList.contains(‘active’) && searchInput.value === ») searchWrap.classList.remove(‘active’); });

 

        document.getElementById(‘stFilterToggleBtn’).addEventListener(‘click’, () => filterModal.classList.add(‘active’));

        document.querySelectorAll(‘.st-cat-chip’).forEach(chip => {

            chip.addEventListener(‘click’, () => {

                const val = chip.getAttribute(‘data-val’);

                if(selectedCats.has(val)) { selectedCats.delete(val); chip.classList.remove(‘selected’); }

                else { selectedCats.add(val); chip.classList.add(‘selected’); }

            });

        });

        

        document.getElementById(‘applyFilters’).addEventListener(‘click’, () => { filterModal.classList.remove(‘active’); applyFilters(); });

        searchInput.addEventListener(‘input’, applyFilters);

 

        function applyFilters() {

            const query = searchInput.value.toLowerCase();

            filteredDeck = deck.filter(item => {

                const matchSearch = item.title.toLowerCase().includes(query) || JSON.stringify(item.extra_data).toLowerCase().includes(query);

                const matchCat = selectedCats.size === 0 || item.category.some(c => selectedCats.has(c));

                return matchSearch && matchCat;

            });

            renderVirtualDeck();

        }

 

        // — 3. VIRTUAL DECK ENGINE —

        function createCardElement(data, zIndexOffset) {

            const card = document.createElement(‘div’);

            card.className = ‘srv-card’;

            card.dataset.id = data.id;

 

            let extraHtml = »;

            if(Object.keys(data.extra_data).length > 0) {

                extraHtml = ‘<div class=»st-char-grid»>’;

                for (const [k, v] of Object.entries(data.extra_data)) extraHtml += `<div class=»st-char-item»><strong>${k}</strong><span>${v}</span></div>`;

                extraHtml += ‘</div>’;

            }

 

            const sQuery = encodeURIComponent(`${data.title} ${Object.values(data.extra_data).join(‘ ‘)}`);

 

            card.innerHTML = `

                <div class=»srv-card-thumb-wrap»>

                    <div class=»st-thumb-actions»>

                        <button class=»btn-action-icon btn-zoom» data-img=»${data.cover_image || »}»>🔍</button>

                        <button class=»btn-action-icon btn-web» data-sq=»${sQuery}»>🌐</button>

                    </div>

                    <img class=»srv-card-thumb» loading=»lazy» src=»${data.cover_image || »}» alt=»»>

                    <h2 class=»srv-card-title»>${data.icon || »} ${data.title}</h2>

                </div>

                <div class=»srv-card-data»>

                    ${data.category.length ? `<div class=»srv-cat-tags»>${data.category.map(c => `<span class=»srv-cat-tag»>${c}</span>`).join(»)}</div>` : »}

                    ${extraHtml}

                    ${data.price ? `<div class=»st-price-val»>${data.price}</div>` : »}

                    <button class=»btn-details»>Подробнее</button>

                </div>

            `;

 

            card.querySelector(‘.btn-zoom’).addEventListener(‘click’, (e) => { e.stopPropagation(); document.getElementById(‘lightboxImg’).src = e.currentTarget.dataset.img; document.getElementById(‘imageModal’).classList.add(‘active’); });

            card.querySelector(‘.btn-web’).addEventListener(‘click’, (e) => { e.stopPropagation(); window.open(`https://www.google.com/search?tbm=isch&q=${e.currentTarget.dataset.sq}`, ‘_blank’, ‘width=900,height=700’); });

            card.querySelector(‘.btn-details’).addEventListener(‘click’, (e) => { 

                e.stopPropagation(); 

                

                // === СТИЛЬНАЯ КАРТОЧКА ===

                let dContent = `

                    <div style=»width:100%; height:250px; background:url(‘${data.cover_image}’) center/cover; position:relative;»>

                        <div style=»position:absolute; bottom:0; left:0; right:0; background:linear-gradient(transparent, rgba(0,0,0,0.85)); padding:25px 20px 15px; color:#fff;»>

                            <h2 style=»margin:0; font-size:24px; font-weight:900; text-shadow:0 2px 5px rgba(0,0,0,0.5); line-height:1.2;»>${data.icon || »} ${data.title}</h2>

                        </div>

                    </div>

                    <div style=»padding:20px;»>

                        <div style=»display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;»>

                            <div style=»font-size:26px; font-weight:900; color:var(—ink-black);»>${data.price ? data.price : ‘По запросу’}</div>

                            <div style=»background:#f0f2f5; padding:8px 12px; border-radius:12px; cursor:pointer; font-size:12px; font-weight:bold; color:#555;» onclick=»window.open(‘https://www.google.com/search?tbm=isch&q=${sQuery}’, ‘_blank’, ‘width=900,height=700’)»>🌐 В Google</div>

                        </div>

                        ${data.category.length ? `<div style=»display:flex; flex-wrap:wrap; gap:6px; margin-bottom:20px;»>${data.category.map(c => `<span style=»background:#f0f0f0; padding:5px 12px; border-radius:12px; font-size:12px; font-weight:bold; color:#333;»>${c}</span>`).join(»)}</div>` : »}

                `;

                if(Object.keys(data.extra_data).length > 0) {

                    dContent += `<div style=»background:#fdfcf8; border:1px solid #eee; border-radius:16px; padding:15px; margin-bottom:20px;»>`;

                    dContent += `<h4 style=»margin-top:0; border-bottom:1px solid #eee; padding-bottom:10px; margin-bottom:10px; color:#888; text-transform:uppercase; font-size:12px;»>Параметры</h4>`;

                    dContent += `<table style=»width:100%; border-collapse: collapse; font-size:14px;»>`;

                    for (const [k, v] of Object.entries(data.extra_data)) {

                        dContent += `<tr><td style=»padding:10px 0; border-bottom:1px dashed #ddd; color:#666;»>${k}</td><td style=»padding:10px 0; border-bottom:1px dashed #ddd; font-weight:bold; text-align:right; color:var(—ink-black);»>${v}</td></tr>`;

                    }

                    dContent += `</table></div>`;

                }

                dContent += `<button id=»btnDownloadPdf» style=»width:100%; padding:14px; background:var(—marker-yellow); border:2px solid var(—ink-black); border-radius:12px; font-weight:bold; font-size:14px; cursor:pointer;»>📄 Скачать PDF</button>`;

                dContent += `</div>`;

 

                document.getElementById(‘detContent’).innerHTML = dContent;

                document.getElementById(‘detailsModal’).classList.add(‘active’);

 

                // PDF Generator Script attachment

                document.getElementById(‘btnDownloadPdf’).addEventListener(‘click’, () => {

                    const printWin = window.open(», », ‘width=800,height=900’);

                    let extraRows = »;

                    for (const [k, v] of Object.entries(data.extra_data)) extraRows += `<tr><td style=»padding:10px; border-bottom:1px solid #ddd; color:#555;»>${k}</td><td style=»padding:10px; border-bottom:1px solid #ddd; font-weight:bold; text-align:right;»>${v}</td></tr>`;

                    printWin.document.write(`<html><head><title>Карточка — ${data.title}</title><style>body{font-family:sans-serif;padding:40px;color:#1a1a1a}.header{border-bottom:3px solid #1a1a1a;padding-bottom:20px;margin-bottom:30px;text-align:center}.product-img{width:100%;max-height:350px;object-fit:contain;margin-bottom:20px}table{width:100%;border-collapse:collapse;margin-bottom:30px;font-size:16px}.price{font-size:32px;font-weight:900;text-align:center;padding:20px;background:#fdfcf8;border:2px dashed #ccc;border-radius:12px}</style></head><body><div class=»header»><h1>Spirit Trade Distribution</h1><p>Импорт и дистрибуция</p></div><div style=»text-align:center;»><img src=»${data.cover_image}» class=»product-img» /><h2>${data.title}</h2></div>${extraRows ? `<table>${extraRows}</table>` : »}${data.price ? `<div class=»price»>${data.price}</div>` : »}<script>window.onload=function(){setTimeout(()=>{window.print();window.close();},500)}<\/script></body></html>`);

                    printWin.document.close();

                });

            });

 

            let startX = 0, startY = 0, currentX = 0, isDragging = false, isScrolling = false, rafId = null;

            

            card.addEventListener(‘pointerdown’, e => {

                if (e.target.closest(‘button’) || e.target.closest(‘.st-thumb-actions’) || e.target.closest(‘.det-search-side’)) return; 

                if (card !== cardsArea.lastElementChild) return;

                startX = e.clientX; startY = e.clientY;

                isDragging = true; isScrolling = false;

                card.style.transition = ‘none’;

            });

            

            card.addEventListener(‘pointermove’, e => {

                if (!isDragging) return;

                const dx = e.clientX — startX, dy = e.clientY — startY;

                if (!isScrolling && Math.abs(dy) > Math.abs(dx) && Math.abs(dy) > 5) isScrolling = true;

                if (isScrolling) { isDragging = false; card.style.transform = »; return; }

                if (Math.abs(dx) > 10 && !card.hasPointerCapture(e.pointerId)) card.setPointerCapture(e.pointerId);

                

                currentX = dx;

                if (!rafId) rafId = requestAnimationFrame(() => {

                    card.style.transform = `translate3d(${currentX}px, 0, 0) rotate(${currentX * 0.04}deg)`;

                    rafId = null;

                });

            });

            

            const endSwipe = (e) => {

                if (!isDragging) return;

                isDragging = false;

                if (card.hasPointerCapture(e.pointerId)) card.releasePointerCapture(e.pointerId);

                if (rafId) { cancelAnimationFrame(rafId); rafId = null; }

                

                card.style.transition = ‘transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease’;

                if (currentX > 80) processSwipe(card, ‘like’, data);

                else if (currentX < -80) processSwipe(card, ‘pass’, data);

                else card.style.transform = »;

                currentX = 0;

            };

 

            card.addEventListener(‘pointerup’, endSwipe);

            card.addEventListener(‘pointercancel’, endSwipe);

 

            return card;

        }

 

        function renderVirtualDeck() {

            cardsArea.innerHTML = »;

            currentDeckIndex = 0;

            if(filteredDeck.length === 0) { 

                cardsArea.innerHTML = ‘<div style=»text-align:center; color:#888; position:absolute; top:50%; width:100%; transform:translateY(-50%);»>Ничего не найдено</div>’; 

                return; 

            }

            const toRender = filteredDeck.slice(0, 3);

            for(let i = toRender.length — 1; i >= 0; i—) {

                const card = createCardElement(toRender[i], i);

                card.style.zIndex = 100 — i;

                cardsArea.appendChild(card); 

            }

        }

 

        function processSwipe(card, action, data) {

            card.style.transform = action === ‘like’ ? `translate(150%, -50px) rotate(20deg)` : `translate(-150%, -50px) rotate(-20deg)`;

            card.style.opacity = ‘0’;

            

            if (isLogged) {

                const fd = new FormData(); fd.append(‘action’, ‘st_srv_record_swipe’); fd.append(‘service_id’, data.id); fd.append(‘swipe_action’, action);

                fetch(ajaxUrl, { method: ‘POST’, body: fd });

            } else {

                let localHistory = JSON.parse(localStorage.getItem(‘st_swipes’) || ‘[]’);

                localHistory.unshift({ title: data.title, action: action, date: new Date().toLocaleDateString() });

                localStorage.setItem(‘st_swipes’, JSON.stringify(localHistory));

            }

 

            setTimeout(() => { 

                card.remove(); 

                currentDeckIndex++;

                

                if (currentDeckIndex + 2 < filteredDeck.length) {

                    const nextData = filteredDeck[currentDeckIndex + 2];

                    const newCard = createCardElement(nextData, currentDeckIndex + 2);

                    newCard.style.zIndex = 100 — (currentDeckIndex + 2);

                    cardsArea.prepend(newCard); 

                }

 

                if(currentDeckIndex >= filteredDeck.length) {

                    cardsArea.innerHTML = ‘<div style=»text-align:center; color:#888; font-weight:bold; position:absolute; top:50%; width:100%; transform:translateY(-50%);»>Все карточки просмотрены ✅</div>’; 

                }

            }, 400);

        }

 

        document.getElementById(‘btnMatch’).addEventListener(‘click’, () => { const t = cardsArea.lastElementChild; if(t && t.classList.contains(‘srv-card’)) processSwipe(t, ‘like’, filteredDeck[currentDeckIndex]); });

        document.getElementById(‘btnPass’).addEventListener(‘click’, () => { const t = cardsArea.lastElementChild; if(t && t.classList.contains(‘srv-card’)) processSwipe(t, ‘pass’, filteredDeck[currentDeckIndex]); });

 

        // — 4. ИСТОРИЯ И CSV —

        const renderHistoryItems = (items) => {

            currentHistoryItems = items; 

            const list = document.getElementById(‘historyList’);

            if (!items.length) { list.innerHTML = ‘<p style=»font-size:13px; text-align:center;»>История пуста</p>’; return; }

            

            list.innerHTML = items.map(h => {

                const isLike = h.action === ‘like’ || h.action === ‘right’;

                const msgText = encodeURIComponent(`Привет! Заинтересовал продукт: ${h.title}`);

                return `

                    <div style=»padding: 10px 0; border-bottom: 1px solid #eee;»>

                        <div style=»display:flex; justify-content:space-between; align-items:flex-start;»>

                            <strong style=»color:var(—ink-black); font-size:12px;»>${h.title}</strong>

                            <span style=»font-size:10px; padding:3px 8px; border-radius:12px; font-weight:bold; white-space:nowrap; ${isLike ? ‘background:#f4ffe6; color:#2e7d32;’ : ‘background:#ffebeb; color:#d32f2f;’}»>${isLike ? ‘Интерес ♥’ : ‘Пропуск ✕’}</span>

                        </div>

                        ${isLike ? `

                        <div class=»h-actions»>

                            <a href=»https://t.me/share/url?url=${encodeURIComponent(window.location.href)}&text=${msgText}» target=»_blank» class=»h-btn» style=»background:#229ED9; color:#fff;»>✈️ TG</a>

                            <a href=»https://wa.me/?text=${msgText}» target=»_blank» class=»h-btn» style=»background:#25D366; color:#fff;»>💬 WA</a>

                            <button class=»h-btn btn-req-tg» data-title=»${h.title}» style=»background:var(—ink-black); color:#fff;»>📨 Запрос</button>

                        </div>` : »}

                    </div>`;

            }).join(»);

 

            // Привязка событий к кнопкам «Запрос»

            document.querySelectorAll(‘.btn-req-tg’).forEach(btn => {

                btn.addEventListener(‘click’, (e) => {

                    currentProductForReq = e.target.getAttribute(‘data-title’);

                    document.getElementById(‘reqProductName’).innerText = `Продукт: ${currentProductForReq}`;

                    document.getElementById(‘reqMsg’).innerText = »;

                    document.getElementById(‘reqEmail’).value = »;

                    document.getElementById(‘reqNote’).value = »;

                    document.getElementById(‘requestModal’).classList.add(‘active’);

                });

            });

        };

 

        document.getElementById(‘btnHistory’).addEventListener(‘click’, () => {

            document.getElementById(‘historyModal’).classList.add(‘active’);

            document.getElementById(‘historyList’).innerHTML = ‘<p style=»font-size:13px; text-align:center;»>Загрузка…</p>’;

            

            if (isLogged) {

                fetch(`${ajaxUrl}?action=st_srv_get_history`).then(r=>r.json()).then(res => renderHistoryItems(res.success ? res.data : []));

            } else {

                renderHistoryItems(JSON.parse(localStorage.getItem(‘st_swipes’) || ‘[]’));

            }

        });

 

        // Отправка формы запроса

        document.getElementById(‘btnSendReq’).addEventListener(‘click’, () => {

            const email = document.getElementById(‘reqEmail’).value;

            const note = document.getElementById(‘reqNote’).value;

            if(!email) return alert(‘Пожалуйста, введите Email’);

 

            const btn = document.getElementById(‘btnSendReq’);

            btn.innerText = ‘Отправка…’;

            btn.disabled = true;

 

            const fd = new FormData();

            fd.append(‘action’, ‘st_srv_send_request’);

            fd.append(‘product’, currentProductForReq);

            fd.append(’email’, email);

            fd.append(‘note’, note);

 

            fetch(ajaxUrl, { method: ‘POST’, body: fd })

                .then(r=>r.json())

                .then(res => {

                    document.getElementById(‘reqMsg’).innerText = res.success ? ‘✅ Успешно отправлено’ : ‘❌ Ошибка’;

                    document.getElementById(‘reqMsg’).style.color = res.success ? ‘green’ : ‘red’;

                    setTimeout(() => document.getElementById(‘requestModal’).classList.remove(‘active’), 2000);

                }).finally(() => {

                    btn.innerText = ‘Отправить запрос в TG’;

                    btn.disabled = false;

                });

        });

 

        // Логика выгрузки CSV

        document.getElementById(‘btnDownloadHistory’).addEventListener(‘click’, () => {

            if (!currentHistoryItems || currentHistoryItems.length === 0) return alert(‘История пуста.’);

            let csvContent = «Название товара;Метка\n»;

            currentHistoryItems.forEach(h => {

                const isLike = (h.action === ‘like’ || h.action === ‘right’) ? ‘Интерес’ : ‘Пропуск’;

                const title = h.title.replace(/»/g, ‘»»‘);

                csvContent += `»${title}»;»${isLike}»\n`;

            });

            const blob = new Blob([«\uFEFF» + csvContent], { type: ‘text/csv;charset=utf-8;’ });

            const link = document.createElement(«a»);

            if (link.download !== undefined) {

                const url = URL.createObjectURL(blob);

                link.setAttribute(«href», url);

                link.setAttribute(«download», «history_spirit_trade.csv»);

                link.style.visibility = ‘hidden’;

                document.body.appendChild(link);

                link.click();

                document.body.removeChild(link);

            }

        });

 

        loadData();

    });

    </script>

    <?php

    return ob_get_clean();

}

 

// ==============================================================================

// 8.3 ШОРТКОД ТАБЛИЦЫ

Управление каталогом

Категории

Фото Позиция ↕ Категория ↕ Цена ↕ Действия
Загрузка данных…

Редактирование

Характеристики

Массовая правка характеристик

Выбрано позиций: 0.
Укажите название характеристики (например, «Объем» или «Страна») и новое значение. Если характеристики нет, она будет добавлена.

// ==============================================================================

add_shortcode(‘st_service_table’, ‘st_srv_render_table’);

function st_srv_render_table() {

    ob_start();

    ?>

    <style>.st-table-wrapper { width: 100%; overflow-x: auto; background: #fff; padding: 20px; border-radius: 16px; box-shadow: 0 5px 20px rgba(0,0,0,0.05); font-family: sans-serif; } .st-table { width: 100%; border-collapse: collapse; min-width: 800px; } .st-table th, .st-table td { padding: 12px 15px; border-bottom: 1px solid #eee; text-align: left; } .st-table th { background: #fdfcf8; font-weight: bold; } .cat-badge { display: inline-block; background: #e0f2f1; color: #00695c; padding: 2px 8px; border-radius: 12px; font-size: 12px; margin: 2px; } .tbl-btn { border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 12px; background: #ffebee; color: #c62828; } .st-tbl-filter { margin-bottom: 15px; padding: 8px 15px; border: 1px solid #ccc; border-radius: 8px; width: 100%; max-width: 300px; }</style>

    <div class=»st-table-wrapper»>

        <input type=»text» id=»tblSearch» class=»st-tbl-filter» placeholder=»Поиск по таблице…»>

        <table class=»st-table» id=»srvTable»><thead><tr><th>ID</th><th>Название</th><th>Категории</th><th>Цена</th><th>Параметры</th><th>Действия</th></tr></thead><tbody id=»tblBody»><tr><td colspan=»6″>Загрузка…</td></tr></tbody></table>

    </div>

    <script>

    document.addEventListener(‘DOMContentLoaded’, () => {

        let tableData = [];

        const loadTable = () => fetch(‘<?php echo admin_url(‘admin-ajax.php’); ?>?action=st_srv_get_deck’).then(r=>r.json()).then(res => { if(res.success) { tableData = res.data; renderTbl(tableData); } });

        const renderTbl = (data) => document.getElementById(‘tblBody’).innerHTML = data.map(row => `<tr data-id=»${row.id}»><td>${row.id}</td><td>${row.title}</td><td>${row.category.map(c => `<span class=»cat-badge»>${c}</span>`).join(»)}</td><td>${row.price}</td><td style=»font-size:12px; max-width:200px; overflow:hidden;»>${JSON.stringify(row.extra_data)}</td><td><button class=»tbl-btn btn-del» onclick=»delRow(${row.id})»>Удалить</button></td></tr>`).join(»);

        

        loadTable();

        document.getElementById(‘tblSearch’).addEventListener(‘input’, (e) => { const q = e.target.value.toLowerCase(); renderTbl(tableData.filter(r => r.title.toLowerCase().includes(q) || JSON.stringify(r.category).toLowerCase().includes(q))); });

        window.delRow = (id) => { if(!confirm(‘Удалить?’)) return; const fd = new FormData(); fd.append(‘action’, ‘st_srv_delete_service’); fd.append(‘id’, id); fetch(‘<?php echo admin_url(‘admin-ajax.php’); ?>’, {method: ‘POST’, body: fd}).then(loadTable); }

    });

    </script>

    <?php return ob_get_clean();

}

 

// ==============================================================================

// 8.4 ШОРТКОД РЕДАКТОРА

Добавление позиции

Пакетный импорт (JSON)

// ==============================================================================

add_shortcode(‘st_service_editor’, ‘st_srv_render_editor’);

function st_srv_render_editor() {

    $cats = st_srv_get_alcohol_categories();

    ob_start();

    ?>

    <style>.st-editor-wrap { max-width: 800px; margin: 20px auto; background: #fff; padding: 25px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.08); font-family: sans-serif; } .st-ed-field { margin-bottom: 20px; } .st-ed-field label { display: block; font-weight: bold; margin-bottom: 8px; color: #1a1a1a; } .st-ed-field input[type=»text»] { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; box-sizing: border-box; } .st-cat-checkboxes { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; } .st-cat-checkboxes label { font-weight: normal; background: #f9f9f9; padding: 8px 12px; border-radius: 8px; border: 1px solid #eee; display: flex; align-items: center; gap: 6px; margin: 0;} .st-extra-group { display: flex; gap: 10px; margin-bottom: 15px; align-items: flex-end; } .st-btn-save { background: #c1ff00; color: #1a1a1a; border: 2px solid #1a1a1a; padding: 12px; border-radius: 8px; font-weight: 800; cursor: pointer; width: 100%; font-size: 16px; } .st-import-box { background: #fdfcf8; border: 2px dashed #ccc; padding: 20px; border-radius: 12px; margin-top: 30px; text-align: center; }</style>

    <div class=»st-editor-wrap»>

        <form id=»stSrvForm»>

            <div class=»st-ed-field»><label>Название</label><input type=»text» name=»srv_title» required></div>

            <div class=»st-ed-field» style=»display:flex; gap:15px;»><div style=»flex:1;»><label>URL картинки</label><input type=»text» name=»srv_image»></div><div style=»width: 100px;»><label>Иконка</label><input type=»text» name=»srv_icon» value=»📌» style=»text-align:center;»></div></div>

            <div class=»st-ed-field»><label>Категории</label><div class=»st-cat-checkboxes»><?php $i=1; foreach($cats as $name => $emoji) echo «<label><input type=’checkbox’ name=’srv_category[]’ value=’$name’> $emoji $name</label>»; ?></div></div>

            <button type=»submit» class=»st-btn-save»>СОХРАНИТЬ В БАЗУ</button><div id=»formMsg» style=»text-align:center; margin-top:10px; font-weight:bold;»></div>

        </form>

        <div class=»st-import-box»>

            <h3>Пакетный импорт (JSON)</h3>

            <form id=»stBulkForm»><?php wp_nonce_field( ‘st_srv_bulk_import’, ‘st_srv_bulk_nonce’ ); ?><textarea name=»bulk_data» rows=»5″ style=»width:100%; padding:10px; border-radius:8px; border:1px solid #ccc;»></textarea><button type=»submit» style=»background:#ff914d; color:#fff; padding:10px; border:none; border-radius:8px; width:100%; margin-top:10px; font-weight:bold; cursor:pointer;»>ЗАГРУЗИТЬ JSON</button><div id=»bulkMsg» style=»margin-top:10px; font-weight:bold;»></div></form>

        </div>

    </div>

    <script>

    document.addEventListener(‘DOMContentLoaded’, () => {

        const ajaxUrl = ‘<?php echo admin_url(‘admin-ajax.php’); ?>’;

        document.getElementById(‘stSrvForm’).addEventListener(‘submit’, function(e) {

            e.preventDefault(); const fd = new FormData(this); fd.append(‘action’, ‘st_srv_save_service’);

            fetch(ajaxUrl, { method: ‘POST’, body: fd }).then(r=>r.json()).then(res => { document.getElementById(‘formMsg’).innerText = res.success ? ‘✅ Сохранено’ : ‘❌ Ошибка’; setTimeout(()=>document.getElementById(‘formMsg’).innerText=», 3000); if(res.success) this.reset(); });

        });

        document.getElementById(‘stBulkForm’).addEventListener(‘submit’, function(e) {

            e.preventDefault(); const fd = new FormData(this); fd.append(‘action’, ‘st_srv_bulk_save’);

            fetch(ajaxUrl, { method: ‘POST’, body: fd }).then(r=>r.json()).then(res => document.getElementById(‘bulkMsg’).innerText = res.success ? `✅ Загружено: ${res.data.inserted}` : `❌ Ошибка`);

        });

    });

    </script>

    <?php return ob_get_clean();

} В меню под Обновить данные с сервера сделай кнопку сгенерировать все досупные фильтрыю При нажатии вывобится окно с перечнем возмодно=ых фильров и при отметке мнодественный выблор и нажатии ок открывается модальное окно с сггенерроваными фильтрами и возможностью выбратьи применить настройки и при  применении запуск фильтрации

 

Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *