Автор: admin

  • дерево

    Перейти к основному содержимомуПерейти к верхней панели

    Помощь

    Редактировать темы

    Редактирование Twenty Twenty-Five (активен)

    Файл: functions.phpВыберите тему для изменения:   Twenty Twenty-Five  

    Файлы темы

    Выбранное содержимое файла:

    1

    <?php

    2

    /*

    3

    Plugin Name: SpiritTrade Digital Modules

    4

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

    5

    Version: 3.1.1

    6

    Author: Spirit Trade

    7

    */

    8

    ​

    9

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

    10

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

    11

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

    12

    define( 'ST_TG_BOT_TOKEN', '7675966976:AAFmAeowI9eGA5siDvsUy9kPr_SHZIOBI38' );

    13

    define( 'ST_TG_CHAT_ID', '-1002640050542' );

    14

    define( 'ST_ADMIN_PIN', '16' );

    15

    ​

    16

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

    17

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

    18

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

    19

    ​

    20

    add_action( 'wp_enqueue_scripts', 'st_enqueue_global_assets' );

    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(‘Для публикации новых заметок в базу знаний платформы необходимо авторизоваться.’); } ?> ✏️ Добавить заметку×

    + запись

    Заголовок

    0)); foreach($categories as $category) { echo » . esc_html($category->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); ?>

    ПИН-КОД

    — — 134679 Опубликовать! ‘Ошибка безопасности. Обновите страницу.’ ) ); } $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 = ‘

    ‘ . $icon . »; $btn_inner .= ‘

    ‘; $btn_inner .= » . $main_title . »; if ($sub_title) { $btn_inner .= ‘для: ‘ . $sub_title . ‘‘; } $btn_inner .= »; $sidebar_html .= sprintf( ‘%s’, 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 ‘

    function st_enqueue_global_assets() {

    22

        if ( ! is_admin() ) {

    23

            wp_enqueue_style( 'dashicons' );

    24

            wp_enqueue_style( 'editor-buttons' );

    25

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

    26

            wp_enqueue_script( 'jquery' );

    27

            

    28

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

    29

            if ( shortcode_exists( 'frontend_post_form' ) || shortcode_exists( 'al_profile_setup' ) ) {

    30

                wp_enqueue_editor();

    31

                wp_enqueue_media();

    32

                wp_enqueue_script( 'word-count' );

    33

                wp_enqueue_script( 'editor' );

    34

                wp_enqueue_script( 'quicktags' );

    35

            }

    36

        }

    37

    }

    38

    ​

    39

    add_action( 'plugins_loaded', 'st_global_install_tables' );

    Документация:Название функции…WP_REST_Response()abs()add_action()add_filter()add_options_page()add_shortcode()admin_url()array_fill()array_map()array_merge()array_slice()array_unshift()check_admin_referer()check_ajax_referer()count()crc32()current_time()dbDelta()define()error_log()esc_attr()esc_html()esc_textarea()esc_url()esc_url_raw()explode()file_exists()finfo_close()finfo_file()finfo_open()get_bloginfo()get_categories()get_current_user_id()get_option()get_permalink()get_posts()get_term()get_the_date()get_userdata()home_url()implode()in_array()includes_url()intval()is_admin()is_array()is_user_logged_in()is_wp_error()json_decode()mb_substr()md5()move_uploaded_file()mt_rand()ob_get_clean()ob_start()pathinfo()preg_match()register_rest_route()rest_ensure_response()rest_url()sanitize_email()sanitize_text_field()sanitize_textarea_field()shortcode_exists()sprintf()stripslashes()strtoupper()substr()time()trim()uniqid()update_option()wp_create_nonce()wp_dequeue_style()wp_editor()wp_enqueue_editor()wp_enqueue_media()wp_enqueue_script()wp_enqueue_style()wp_get_current_user()wp_get_image_editor()wp_insert_post()wp_json_encode()wp_kses_post()wp_login_url()wp_logout_url()wp_mkdir_p()wp_nonce_field()wp_registration_url()wp_remote_post()wp_send_json_error()wp_send_json_success()wp_trim_words()wp_upload_dir()wp_verify_nonce()

    Спасибо вам за творчество с WordPress.

    Версия 7.0

  • фунгк

    <?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();

    } В меню под Обновить данные с сервера сделай кнопку сгенерировать все досупные фильтрыю При нажатии вывобится окно с перечнем возмодно=ых фильров и при отметке мнодественный выблор и нажатии ок открывается модальное окно с сггенерроваными фильтрами и возможностью выбратьи применить настройки и при  применении запуск фильтрации

     

  • код блога

     

    <!DOCTYPE html>

    <html lang=»uk»>

    <head>

        <meta charset=»UTF-8″>

        <meta name=»viewport» content=»width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=yes»>

        <title>Літо 2026 · Повне меню з усіма рецептами</title>

        <script src=»https://cdn.jsdelivr.net/npm/html2pdf.js@0.10.1/dist/html2pdf.bundle.min.js»></script>

        <style>

            * { margin: 0; padding: 0; box-sizing: border-box; }

            :root {

                —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); —shadow-sketch: 3px 3px 0px var(—ink-black);

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

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

            }

            @media (min-width: 1024px) { :root { —border-sketch: 3px solid var(—ink-black); —shadow-sketch: 5px 5px 0px var(—ink-black); } }

            body, html { background: var(—bg-paper); overflow: hidden; width: 100vw; height: 100dvh; display: flex; align-items: center; justify-content: center; font-family: var(—font-main) !important; position: fixed; top: 0; left: 0; font-size: 18px; }

            @media (max-width: 768px) { body, html { font-size: 15px; } }

            @media (max-width: 480px) { body, html { font-size: 14px; } }

            .comic-device { width: 100vw; height: 100dvh; background: var(—bg-paper); display: flex; flex-direction: column; overflow: hidden; position: relative; }

            .device-header { height: 60px; background: #fff; border-bottom: var(—border-sketch); display: flex; align-items: center; justify-content: space-between; padding: 0 16px; flex-shrink: 0; z-index: 1000; }

            @media (min-width: 768px) { .device-header { height: 70px; padding: 0 24px; } }

            .mac-dots { display: flex; gap: 8px; }

            .mac-dot { width: 12px; height: 12px; border-radius: 50%; border: 2px solid var(—ink-black); animation: popIn 0.4s forwards; opacity: 0; }

            @keyframes popIn { 0% { transform: scale(0); opacity: 0; } 80% { transform: scale(1.2); opacity: 1; } 100% { transform: scale(1); opacity: 1; } }

            .mac-dot.red { background: var(—marker-red); animation-delay: 0.1s; }

            .mac-dot.yellow { background: var(—marker-yellow); animation-delay: 0.2s; }

            .mac-dot.green { background: var(—marker-green); animation-delay: 0.3s; }

            .logo-text { font-weight: bold; font-size: clamp(0.9rem, 5vw, 1.2rem); background: var(—marker-yellow); padding: 4px 12px; border-radius: 40px; border: var(—border-sketch); box-shadow: 2px 2px 0 var(—ink-black); }

            .user-info { display: flex; align-items: center; gap: 8px; background: #f0e9da; border: var(—border-sketch); border-radius: 40px; padding: 4px 12px; font-size: 0.8rem; cursor: pointer; transition: var(—transition); }

            .user-info:hover { background: #e3d8c2; transform: translateY(-2px); }

            .user-info span { font-weight: bold; }

            .tab-buttons { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; padding: 12px 12px 0; background: #fefaf0; border-bottom: 2px dashed #e3cfb5; flex-shrink: 0; }

            .tab-btn { background: #fff; border: var(—border-sketch); padding: 8px 16px; font-weight: bold; font-size: clamp(0.7rem, 4vw, 1rem); border-radius: 60px; cursor: pointer; font-family: var(—font-main); box-shadow: 2px 2px 0 var(—ink-black); transition: var(—transition); white-space: nowrap; }

            .tab-btn.active { background: var(—marker-green); transform: translateY(-2px); box-shadow: 4px 4px 0 var(—ink-black); }

            .tab-pane { display: none; flex: 1; overflow-y: auto; padding: 16px; animation: fade 0.25s; }

            .tab-pane.active-pane { display: block; }

            @keyframes fade { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }

            .baskets-container { display: flex; flex-direction: column; gap: 1.5rem; }

            .basket-week { background: #fffcf6; border: var(—border-sketch); border-radius: 28px; overflow: hidden; box-shadow: var(—shadow-sketch); }

            .basket-header { background: #f2e8dd; padding: 1rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; cursor: pointer; font-weight: 800; font-size: clamp(1rem, 5vw, 1.3rem); border-bottom: var(—border-sketch); }

            .basket-actions { display: flex; gap: 12px; align-items: center; }

            .send-icon, .pdf-icon { background: white; border: var(—border-sketch); border-radius: 40px; padding: 6px 14px; font-size: clamp(0.7rem, 4vw, 0.9rem); font-weight: bold; cursor: pointer; display: inline-flex; gap: 6px; box-shadow: 2px 2px 0 var(—ink-black); }

            .basket-cats { display: none; grid-template-columns: repeat(auto-fill, minmax(250px,1fr)); gap: 1rem; padding: 1rem; background: #fffef8; }

            .basket-cats.open { display: grid; }

            .cat-group { background: #fef7ef; border-radius: 20px; padding: 0.8rem; border: 1px solid #e6d5bc; }

            .cat-group h4 { font-size: clamp(0.9rem, 4vw, 1rem); margin-bottom: 10px; border-left: 5px solid var(—marker-blue); padding-left: 10px; }

            .check-item { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: clamp(0.75rem, 4vw, 0.85rem); }

            .clear-basket { margin: 0 1rem 1rem auto; background: none; border: var(—border-sketch); border-radius: 30px; padding: 5px 12px; font-weight: bold; cursor: pointer; }

            .calendar-grid { display: flex; flex-direction: column; gap: 2rem; }

            .week-card { background: white; border-radius: 32px; border: var(—border-sketch); box-shadow: var(—shadow-sketch); overflow: hidden; }

            .week-summary { background: #fef2e2; padding: 1rem 1.2rem; display: flex; justify-content: space-between; align-items: center; cursor: pointer; border-bottom: var(—border-sketch); font-weight: bold; font-size: clamp(1rem, 5vw, 1.3rem); }

            .week-arrow { font-size: 1.3rem; transition: transform 0.2s; }

            .week-card.collapsed .week-arrow { transform: rotate(-90deg); }

            .days-vertical { display: flex; flex-direction: column; width: 100%; }

            @media (min-width: 768px) { .days-vertical { display: grid; grid-template-columns: repeat(2, 1fr); } }

            @media (min-width: 1024px) { .days-vertical { grid-template-columns: repeat(3, 1fr); } }

            .week-card.collapsed .days-vertical { display: none; }

            .day-mini { padding: 1rem 1.2rem; border-bottom: 2px dotted #dccfbc; border-right: 1px solid #eedfcb; cursor: pointer; transition: 0.2s; background: #fffcf5; position: relative; }

            .day-mini:hover { background: #fff0df; }

            .day-mini.completed-day { opacity: 0.6; background: #ebf5ea !important; }

            .day-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }

            .day-num-icon { background: var(—marker-yellow); color: var(—ink-black); font-weight: 900; width: 32px; height: 32px; border-radius: 10px; display: flex; align-items: center; justify-content: center; border: 2px solid var(—ink-black); box-shadow: 2px 2px 0 var(—ink-black); font-size: 1rem; flex-shrink: 0; }

            .day-name-text { font-weight: 800; font-size: clamp(1rem, 5vw, 1.2rem); color: #b45f2b; }

            .day-completed-check { margin-left: auto; width: 22px; height: 22px; cursor: pointer; accent-color: var(—marker-green); }

            .day-meals-preview { font-size: clamp(0.8rem, 4vw, 1rem); line-height: 1.5; }

            .day-meals-preview div { margin-top: 6px; }

            .week-basket { border-top: 2px dashed var(—marker-blue); background: #fffdf7; }

            .basket-header-mini { background: #e9dfcf; padding: 0.8rem 1.2rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; cursor: pointer; font-weight: bold; border-bottom: var(—border-sketch); }

            .basket-header-mini:hover { background: #dfd4c2; }

            .basket-header-mini span:first-child { display: flex; gap: 8px; align-items: center; font-size: clamp(0.8rem, 4vw, 1rem); pointer-events: none; }

            .basket-actions-mini { display: flex; gap: 8px; }

            .mini-send, .mini-pdf { background: white; border: var(—border-sketch); border-radius: 40px; padding: 4px 12px; font-size: clamp(0.65rem, 3.5vw, 0.8rem); font-weight: bold; cursor: pointer; display: inline-flex; gap: 4px; box-shadow: 2px 2px 0 var(—ink-black); }

            .basket-content { display: none; padding: 1rem; background: #fffbf5; }

            .basket-content.open { display: block; }

            .basket-content .cat-group { margin-bottom: 1rem; }

            .basket-content .items-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px 12px; margin-top: 6px; }

            .note-card { background: white; border: var(—border-sketch); border-radius: 20px; padding: 16px; margin-bottom: 12px; box-shadow: 3px 3px 0 var(—ink-black); }

            .note-card h4 { margin-bottom: 8px; color: #b45f2b; display: flex; align-items: center; gap: 8px; white-space: nowrap; overflow-x: auto; text-transform: lowercase; font-size: 1rem; }

            .desserts-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px,1fr)); gap: 1rem; }

            .dessert-card { background: #fff6ec; border: var(—border-sketch); border-radius: 24px; padding: 1rem; box-shadow: var(—shadow-sketch); }

            .dessert-card h3 { font-size: clamp(1rem, 5vw, 1.2rem); margin-bottom: 0.4rem; }

            .full-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(26,26,26,0.85); backdrop-filter: blur(5px); display: flex; align-items: center; justify-content: center; visibility: hidden; opacity: 0; transition: 0.2s; z-index: 2000; }

            .full-modal.active { visibility: visible; opacity: 1; }

            .modal-comic { background: #fffcf0; width: 92%; max-width: 700px; max-height: 85vh; overflow-y: auto; border-radius: 28px; border: var(—border-sketch); box-shadow: 8px 8px 0 var(—ink-black); padding: 1.5rem; position: relative; }

            .modal-comic h3 { font-size: clamp(1.2rem, 6vw, 1.6rem); background: var(—marker-yellow); display: inline-block; padding: 0 10px; margin-bottom: 1rem; border: 2px solid var(—ink-black); }

            .meal-card { background: #fff; border: 2px dashed #dccfbc; border-radius: 20px; padding: 16px; margin-bottom: 16px; }

            .meal-title { font-weight: 900; color: #b45f2b; font-size: 1.1rem; border-bottom: 1px solid #eee; padding-bottom: 6px; margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }

            .notes-area { margin-top: 1rem; background: #fff8e0; padding: 1rem; border-radius: 20px; border: 1px solid #eedfcb; }

            .notes-area label { font-weight: bold; display: block; margin-bottom: 8px; }

            .notes-input-wrapper { display: flex; gap: 12px; align-items: stretch; }

            .notes-area textarea { flex: 1; font-family: var(—font-main); border: var(—border-sketch); border-radius: 16px; padding: 10px; font-size: 0.9rem; background: white; resize: none; }

            .btn-icon { background: var(—marker-green); border: var(—border-sketch); border-radius: 16px; width: 50px; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; cursor: pointer; box-shadow: 3px 3px 0 var(—ink-black); }

            .btn-group { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 1rem; justify-content: center; }

            .btn-send, .btn-pdf, .btn-outline { display: flex; align-items: center; justify-content: center; gap: 8px; border: var(—border-sketch); border-radius: 40px; padding: 10px 16px; font-weight: bold; font-size: clamp(0.8rem, 4vw, 0.9rem); cursor: pointer; box-shadow: 2px 2px 0 var(—ink-black); transition: 0.1s; color: var(—ink-black); text-decoration: none; background: white; }

            .btn-send.whatsapp { background: #25d366; color: white; }

            .btn-send.telegram { background: #26A5E4; color: white; }

            .btn-pdf { background: var(—marker-red); color: white; }

            .close-modal { position: absolute; top: 12px; right: 12px; font-size: 1.6rem; cursor: pointer; background: var(—marker-red); width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; border: var(—border-sketch); color: white; line-height: 1; }

            footer { text-align: center; padding: 0.8rem; font-size: 0.7rem; border-top: 1px solid #eedfcb; background: #fefaf0; flex-shrink: 0; }

            .auth-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; z-index: 3000; }

            .auth-card { background: #fffcf0; border: var(—border-sketch); border-radius: 40px; padding: 2rem; width: 90%; max-width: 400px; text-align: center; box-shadow: 12px 12px 0 var(—ink-black); }

            .auth-card input { width: 100%; padding: 12px; margin: 12px 0; font-family: var(—font-main); border: var(—border-sketch); border-radius: 40px; font-size: 1rem; }

            .auth-card button { background: var(—marker-green); border: var(—border-sketch); padding: 10px 20px; border-radius: 40px; font-weight: bold; cursor: pointer; font-size: 1rem; width: 100%; box-shadow: 3px 3px 0 var(—ink-black); }

            

            /* СТИЛІ ДЛЯ СІТКИ ПРОГРЕСУ */

            .progress-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 10px; max-width: 400px; margin: 0 auto; padding: 10px 0; }

            .progress-day { width: 100%; aspect-ratio: 1; display: flex; align-items: center; justify-content: center; background: #fff; border: var(—border-sketch); border-radius: 12px; font-weight: bold; cursor: pointer; box-shadow: 2px 2px 0 var(—ink-black); transition: 0.1s; font-size: 1.1rem; }

            .progress-day:active { transform: translate(2px, 2px); box-shadow: 0px 0px 0 var(—ink-black); }

            .progress-day.crossed-out { text-decoration: line-through; opacity: 0.5; background: #e0e0e0; pointer-events: none; }

            

            @media (max-width: 480px) { .basket-header { flex-direction: column; align-items: flex-start; } .basket-actions { margin-left: auto; } .tab-btn { padding: 6px 12px; } }

        </style>

    </head>

    <body>

    <div class=»comic-device» id=»appRoot» style=»display: none;»>

        <div class=»device-header»>

            <div class=»mac-dots»><div class=»mac-dot red»></div><div class=»mac-dot yellow»></div><div class=»mac-dot green»></div></div>

            <div class=»logo-text»>Літо 2026</div>

            <div class=»user-info» id=»headerProfileBtn»><span id=»userNameDisplay»>Гість</span> (<span id=»userPhoneDisplay»></span>) 👤</div>

        </div>

        <div class=»tab-buttons»>

            <button class=»tab-btn active» data-tab=»progress»>🕒 Прогрес</button>

            <button class=»tab-btn» data-tab=»info»>📘 Інструкція</button>

            <a href=»https://drive.google.com/uc?export=download&id=1-t1M9FHeN3cZ1u7g3k5h3NaLmNxQJe_w» target=»_blank» style=»background: #fff; border: var(—border-sketch); padding: 8px 16px; font-weight: bold; font-size: clamp(0.7rem, 4vw, 1rem); border-radius: 60px; cursor: pointer; font-family: var(—font-main); box-shadow: 2px 2px 0 var(—ink-black); text-decoration: none; color: var(—ink-black); white-space: nowrap; transition: var(—transition);»>

        📄 PDF

    </a>

            <button class=»tab-btn» data-tab=»checklists»>🛒 Корзини</button>

            <button class=»tab-btn» data-tab=»calendar»>📅 Календар</button>

            <button class=»tab-btn» data-tab=»desserts»>🍰 Десерти</button>

            <button class=»tab-btn» data-tab=»notes»>📝 Нотатки</button>

    </div>

        <div id=»progressPane» class=»tab-pane active-pane»>

            <h2 style=»margin-bottom:16px; text-align:center;»>Мій прогрес 🕒</h2>

            <div id=»progressGridContainer»></div>

        </div>

        <div id=»infoPane» class=»tab-pane»>

            <div style=»background:#fef3e8;border-radius:28px;padding:1.2rem;border:var(—border-sketch);box-shadow:var(—shadow-sketch);»>

                <h2 style=»margin-bottom:1rem;»>📖 Інструкція до використання</h2>

                <p style=»margin-bottom:10px;»>Дане меню складене за принципом здорової тарілки, є різноманітним та збалансованим. Спрямоване на розширення ваших харчових вподобань відповідно до сезону. Воно допоможе відкрити для себе нові поєднання продуктів, способи приготування страв, рецепти чи інгредієнти.</p>

                <p style=»margin-bottom:10px;»>Меню написано і пораховано для зручності використання. Ви будете готувати відразу 2 (за бажанням більше) порції, кожної страви переважно ввечері, щоб не стояти постійно біля плити. Якщо вам зручніше готувати одразу 2 порції в обід то змініть порядок страв, або готуйте в зручний час без прив’язки до прийому їжі. Якщо ви готуєте не лише собі, то збільште кількість потрібних порцій відповідно до членів сім’ї. Кількість порцій вказана у кожному рецепті, якщо немає поміток – 1 порція.</p>

                <ul style=»list-style:disc; margin-left:20px; line-height:1.6; display:flex; flex-direction:column; gap:8px;»>

                    <li><strong>Продукти пораховані у сирому/сухому вигляді</strong>, якщо не вказано протилежне. Наприклад: «Візьміть 300 г телятини і протушкуйте її …», «50 г гречки – відваріть» — означає, що продукт пораховано у сирому та сухому вигляді.</li>

                    <li><strong>«Покладіть 100 г запеченого буряка на …»</strong> — вказано що потрібно взяти 100 г готового, запеченого буряку.</li>

                    <li><strong>Плануйте свій раціон.</strong> Заздалегідь перегляньте меню, складіть список продуктів які вам необхідні для приготування страв і прийміть рішення що і коли ви будете їсти чи готувати. Підлаштуйте меню під свій графік життя, беріть лоточки на роботу. Меню не дасть результатів якщо ви відкриєте його вже голодними.</li>

                    <li><strong>Дні з меню можна повторювати.</strong> Страви можна переставляти місцями (тобто, те що на обід можна з’їсти ввечері чи на сніданок). Кожен прийом їжі збалансований і повноцінний, тому переставляйте їх як конструктор. Таким чином ви зможете харчуватись цілий сезон повторюючи страви та додаючи свої.</li>

                    <li><strong>Овочі можна замінювати іншими овочами</strong> (крім картоплі) або ж листовою зеленню, зважаючи на сезонні продукти та ваші смаки. Орієнтовно пів тарілки (21-22 см) мають складати у вас овочі. Вони можуть бути у різних формах обробки – квашені, сирі, запечені і всі у 1 тарілці, так щоб це не викликало дискомфорту зі сторони ШКТ. Можна додавати овочів та зелені в тарілку більше, ніж вказано у меню. Старайтесь щоб овочі чергувались, не лише огірок та помідор. Зараз найбагатший на овочі, фрукти та ягоди сезон – користуємось.</li>

                    <li><strong>Не забувайте про ягоди та фрукти.</strong> Це може бути перекус чи фінальний акорд трапези. Фрукт – будь-який, пріоритет фрукт, а не сухофрукт. Орієнтуємось на сезонність, літо це період смаку, яскравих кольорів і ароматів. У червні ще можна насолоджуватись полуницею, з’являється черешня, шовковиця, суниця, а також дозрівають перші абрикоси, смородина, лохина та аґрус. Ближче до липня — багато сортів персиків, нектаринів, слив, з’являються малина, ожина, чорниця. У серпні до цього розмаїття додаються ранні яблука та груші, дині, кавуни, виноград.</li>

                    <li><strong>Чому каші в сухому?</strong> Бо кожна господиня сама вирішує скільки додавати води до певної каші. І це не означає що ви маєте варити тільки по 50 г за 1 раз. Один раз відважили – зварили, подивились на приблизний об’єм і на нього орієнтуйтесь надалі.</li>

                    <li><strong>М’ясо та риба вказана переважно в сирому вигляді</strong>, щоб легше було важити і готувати.</li>

                    <li><strong>Порції зменшувати не бажано.</strong> Але якщо дана калорійність вам завелика – відмовтесь чи зменште порцію якогось компоненту, або усіх рівноцінно. Старайтесь не виключати тільки білки, вуглеводи чи жири з тарілки, це призведе до порушення балансу.</li>

                    <li><strong>Не бійтесь додавати смаку в страви.</strong> Приправте за смаком у рецептах означає додайте улюблені чи нові спеції. Пам’ятайте що часто смак продуктів та страв залежить виключно від спецій що туди додали (чи не додали). Тому перш ніж відмовлятись від рецептів/продуктів/страв – спробуйте нові смачні спеції. Це не тільки пряні трави чи копчена паприка. Це могут бути будь-які готові суміші що можна знайти в супермаркеті, навіть зі сіллю чи глутаматом натрію. Це не зло, і нічого небезпечного у них немає — якщо ви не будете зловживати кількістю. Експериментуйте зі спеціями і звичайні продукти заграють новими барвами та швидше приживуться у вашому раціоні.</li>

                    <li><strong>Ідеально прорахувати ми не можемо.</strong> Адже яблуко зелене і яблуко червоне має різні ккал. Стегно може бути жирніше чи менш жирне. Коренеплоди теж в залежності від сезону мають різну кількість крохмалю. Тому не женіться за прорахунками до граминочки. На цьому меню чудово можна схуднути і не бути голодним (теж важливо).</li>

                    <li><strong>Кашу можна змінювати на іншу кашу.</strong> Крупи мають бути довгого приготування, цільні, а не подрібнені. Кус-кус, поленту, манку, пластівці, тощо – вживаємо інколи. Якщо ви якусь крупу не любите – змініть у тих самих грамах на іншу крупу, але рекомендую аби в раціоні було хоча б 3 види різних круп.</li>

                    <li><strong>Білки також можна змінювати</strong>, наприклад ви не любите оселедець – надаєте перевагу скумбрії чи лососю – будь-ласка замінюйте на те що любите, але не лише 1 вид улюбленої риби до усіх прийомів їжі. Важливе різноманіття продуктів!</li>

                    <li><strong>Якщо в меню порахований певний продукт</strong> наприклад – йогурт 1.6% жирності, сметана 30%, чи цільнозерновий хліб, а у вас інші продукти, не з таким складом, калорійністю чи жирністю – це не біда, ви спокійно можете і маєте їх змінювати відповідно до можливостей і вподобань. Можете це робити просто в тих самих кількостях, чи змінювати їх на око. Або ж – подивіться на калорійність продукту що вказаний і визначте кількість вашого продукту що відповідає такій же калорійності. Це можна дізнатись на етикетці товару, або вбити обидва продукти в додатки для обрахунку калорій. (напр. Таблиця калорійності)</li>

                    <li><strong>Якщо досі ви звикли їсти менше</strong>, порції можуть здаватись великими. Їжте скільки зможете, поступово збільшуючи об’єм.</li>

                    <li><strong>Ми не набираємо вагу, якщо з’їмо вуглеводи ввечері.</strong> Важливо що ми з’їмо і скільки впродовж дня!</li>

                    <li><strong>Їмо по режиму</strong> – між кожним прийомом їжі має пройти 3-4 години. Не плутайте голод з апетитом. Коли хочеться чогось конкретного, навіть коли не голодні — це апетит. І навпаки коли пройшло вже 6 год і шлунок не дає про себе знати, то не значить що ви не голодні, напевно ви були зайняті і не помітили – поїжте. В проміжках між трапезами п’ємо чисту воду без додатків, чорну каву чи чай без додатків.</li>

                    <li><strong>Сміливо замінюйте продукти</strong> якщо вам доступні чи звичні інші. Свіжий шпинат на заморожений, в червні ще не доступна молода кукурудза – додаємо консервовану чи морожену, не любите мед – додавайте кленовий сироп чи цукор натомість. На смак не впливає, але полегшує підбір продуктів.</li>

                    <li><strong>Заморожуйте залишки продуктів чи овочі.</strong> Складно купити 50 г соленої риби чи 150 г гарбуза, але цілий гарбуз чи 200 г риби – легко нарізати порційно і заморозити. Вони не втрачають смак, не змінюють текстури. Туди ж заморожуємо хліб, тортилью, кисломолочний сир, броколі, цвітну капусту, брюссельську капусту, кабачок та цукіні, баклажани, перець, порей, усі бобові (варені чи консервовані), приготовані крупи тривалого приготування (перловка, кіноа).</li>

                    <li><strong>Додавайте планування і заготовки.</strong> Готуйте відразу кілька порції овочів на шакшуку і заморожуйте сирники, ліниві вареники, котлети, запіканки, паштети та навіть каші – все в пакети і у заморозку. Якщо не пам’ятаєте вміст морозильної камери – ведіть список продуктів які кладете і викреслюйте те, що дістаєте. Так вміст завжди на виду.</li>

                </ul>

                <h3 style=»margin-top:20px; color:#b45f2b; text-align:center;»>Смачного! ❤️</h3>

            </div>

        </div>

        <div id=»checklistsPane» class=»tab-pane»></div>

        <div id=»calendarPane» class=»tab-pane»></div>

        <div id=»dessertsPane» class=»tab-pane»></div>

        <div id=»notesPane» class=»tab-pane»><h2 style=»margin-bottom:16px;»>📝 Всі мої нотатки</h2><div id=»userNotesList»></div></div>

        <footer>© Олеся Добрянська · комерційне використання заборонено · Дані синхронізуються з WordPress</footer>

    </div>

     

    <div id=»authModal» class=»auth-modal»>

        <div class=»auth-card»>

            <h2>🍉 Ласкаво просимо!</h2>

            <p>Введіть номер телефону та код доступу (2026)</p>

            <input type=»tel» id=»authPhone» placeholder=»+380 XX XXX XXXX»>

            <input type=»text» id=»authCode» placeholder=»****»>

            <button id=»authSubmit»>Увійти</button>

        </div>

    </div>

     

    <div id=»profileModal» class=»full-modal»>

        <div class=»modal-comic»><span class=»close-modal» id=»closeProfile»>&times;</span><h3>👤 Кабінет</h3>

            <label style=»display:block; margin-bottom:5px; font-weight:bold;»>Ім’я:</label>

            <input type=»text» id=»profileNameInput» style=»width:100%;padding:12px;border:var(—border-sketch);border-radius:20px;margin-bottom:16px;»>

            <label style=»display:block; margin-bottom:5px; font-weight:bold;»>Телефон:</label>

            <input type=»tel» id=»profilePhoneInput» style=»width:100%;padding:12px;border:var(—border-sketch);border-radius:20px;margin-bottom:20px;»>

            <button id=»saveProfileBtn» class=»btn-outline» style=»width:100%;background:var(—marker-yellow);»>💾 Зберегти</button>

            <hr style=»margin:24px 0;»><button id=»logoutBtn» class=»btn-pdf» style=»width:100%;»>🚪 Вийти</button>

        </div>

    </div>

     

    <div id=»fullRecipeModal» class=»full-modal»>

        <div class=»modal-comic»><span class=»close-modal» id=»closeRecipeModal»>&times;</span><h3 id=»modalTitle»>Рецепт</h3><div id=»modalRecipeContent»></div>

            <div class=»notes-area»><label>📝 Мої нотатки до цього дня:</label><div class=»notes-input-wrapper»><textarea id=»dayNoteText» rows=»2″ placeholder=»Зміни, враження, ідеї…»></textarea><button id=»saveDayNoteBtn» class=»btn-icon»>💾</button></div></div>

            <div class=»btn-group»><button id=»sendRecipeWhatsApp» class=»btn-send whatsapp»>📱 WhatsApp</button><button id=»sendRecipeTelegram» class=»btn-send telegram»>✈️ Telegram</button><button id=»downloadRecipePDF» class=»btn-pdf»>📄 PDF</button></div>

        </div>

    </div>

     

    <script>

    // —————— НАЛАШТУВАННЯ ——————

    const WP_API_URL = ‘https://blog.doctordobra.com/wp-json/summer-menu/v1’;

     

    // —————— ПОВНІ РЕЦЕПТИ (Всі з PDF) ——————

    const fullRecipes = {

        // Тиждень 1

        «Авокадо-тост з сиром та яйцем»: «Відваріть круто 2 яйця, охолодіть та розімніть виделкою. Додайте 100 г кисломолочного сиру, сіль, перець, гранульований часник, сванську сіль або інші спеції за смаком і перемішайте до однорідної текстури (якщо сир сухий, додайте 1 ст. л. йогурту). Підсушіть 2 шматочки цільнозернового хліба (80 г), намажте авокадо (80 г), трохи посоліть, викладіть зверху яєчно-сирну масу та подавайте зі 100 г свіжих овочів на вибір.»,

        «Паста з кабачками і тунцем»: «60 г макаронів твердих сортів відваріть до готовності. 1 малий цукіні або кабачок наріжте тонкими кільцями, додайте туди 1 розчавлений зубчик часнику та лимонну цедру за смаком і обсмажте на сковорідці до готовності з допомогою 1 ч. л. оливкової олії. Як кабачки приготуються вимкніть вогонь і додайте 1 банку консервованого тунця у своєму соці (185 г), свіжий базилік, 20 г свіжого шпинату чи іншої зелені на вибір, 20 г розрізаних навпіл оливок, 20 г тертого пармезану, лимонний сік та сіль і перець за смаком, за бажанням додайте ще каперси. Перемішайте, скуштуйте, має бути кисленьке і подавайте. Полуниця 200 г»,

        «Лимонні гомілки з картоплею та спаржею»: «Ретельно вимийте 400 г молодої картоплі (дрібну залиште цілою, більшу розріжте навпіл), наріжте півкільцями 1 велику цибулину і скибочками половину лимона. У формі для запікання з’єднайте картоплю, цибулю, лимон та 4 курячі гомілки. Додайте 1 ст. л. вершкового масла або оливкової олії, 2 подрібнені зубчики часнику, 1 ч. л. сушених трав (орегано або чебрець), сіль та чорний перець за смаком, після чого все добре перемішайте. Розподіліть інгредієнти так, щоб гомілки лежали зверху і запікайте у розігрітій до 180°С духовці 40–45 хвилин до готовності. Візьміть 400 г спаржі, відламайте жорсткі нижні кінці стебел. Викладіть її у сковороду, додайте 1 ч. л. вершкового масла та 1–2 розчавлені зубчики часнику, дрібку солі та чорного перцю. Смажте 3-5 хв на середньому вогні, постійно перевертаючи, щоб вона трішки потемніла, але залишалась хрумкою.»,

        «Вівсянка з песто і яйцем»: «Зваріть 50 г плющеної вівсянки з 150–180 мл молока, посоліть. Паралельно приготуйте 2 яйця: відваріть або підсмажте на 5 г вершкового масла, залишивши жовток рідким. У кашу вмішайте 1 ч. л. соусу песто та 15 г тертого пармезану. Перекладіть у тарілку, зверху викладіть яйце, додайте 100 г помідорів чері, 30 г свіжого шпинату та кілька листочків свіжого базиліку.»,

        «Тушкована тріска з креветками в соусі карі»: «80 г булгуру – відваріть до готовності. Застеліть сковороду з товстим дном двома шарами пергаментного паперу. Викладіть туди 1 середню цибулину та 2–3 зубчики часнику, нарізаних соломкою, збризніть краплею оливкової олії та додайте дрібку солі, перемішайте. Далі викладіть шарами 300 г філе тріски, розрізаної на 2 шматочки, 150 г очищених креветок та 150 г помідорів чері. У мисці змішайте 65 мл несолодких кокосових чи звичайних вершків (20%), 1 ст. л. жовтої пасти карі, ½ ч. л. концентрованого курячого бульйону та 2 ст. л. води. Вилийте цю суміш на морепродукти, зверху посипте 1 ст. л. подрібненої кінзи чи петрушки. Накрийте кришкою і поставте на середній вогонь. Як тільки рідина почне тихо кипіти, зменште вогонь до мінімуму та готуйте 15–20 хвилин до готовності риби та креветок. Перед подачею збризніть соком лайма.»,

        «Яєчний млинець з сиром і ягодами»: «Відокремте жовток 2 яєць від білка. До жовтків додайте 1 ст. л. йогурту, підсолоджувач за смаком, 1 ст. л. крохмалю або борошна і ¼ ч. л. розпушувача, перемішайте. Білки збийте до стійких піків та обережно вмішайте в масу. Випечіть млинець на сковороді під кришкою на мінімальному вогні з обох боків. Для крему змішайте 100 г кисломолочного сиру, ванілін та 1 ст. л. згущеного молока або іншого підсолоджувача, збийте до однорідності. Якщо сир сухий, додайте ще ложку йогурту. Змастіть млинець сирною масою, додайте 150 г черешні або інші ягоди і згорніть навпіл.»,

        «Локшина з яловичиною та овочами»: «Наріжте тонкими смужками 300 г яловичини, замаринуйте у 2 ст. л. соєвого соусу, 1 ч. л. меду, 1 ч. л. лимонного соку, 1 ч. л. кунжутної олії, додайте трішки гранульованого часнику та паприки. 150 г брюссельської капусти відваріть 3–4 хв у киплячій воді й перекладіть у холодну, розріжте навпіл. Окремо відваріть 80 г локшини удобн чи соба за інструкцією на упаковці. Обсмажте м’ясо на сковороді до готовності, перекладіть у тарілку. У тій же сковороді обсмажте з допомогою 1 ч. л. оливкової олії нарізані соломкою солодкий перець, невелику моркву і цибулю та 1 зубчик часнику. Поверніть м’ясо, додайте броколі й локшину. Для соусу змішайте 100 мл води, 2 ст. л. соєвого соусу, 1 ч. л. меду, 1 ч. л. вустерширського соусу (за бажанням), 1 ст. л. рисового оцту та 1 ч. л. крохмалю. Влийте соус і тушкуйте все разом 2–3 хв до загустіння. Посипте зеленою цибулею.»,

        «Яєчний салат з в’яленими томатами»: «Відваріть 2 яйця рівно 6,5 хвилин, після чого занурте їх у холодну воду. Паралельно підсушіть 80 г цільнозернового хліба. Очищені яйця покладіть на велику дошку, додайте трохи кропу, зеленої цибулі, свіжого базиліку, 30 г шпинату та 3-4 шт в’ялених помідорів. Поріжте все ножем, перетворюючи в пасту. Перекладіть у посуд і додайте 1 ст. л. йогурту, 30 г фети, сіль та перець за смаком. Ретельно перемішайте і викладіть готову масу на тости.»,

        «Медово-кунжутна курка з рисом та овочами»: «Змішайте 20 г кукурудзяного крохмалю, ½ ч. л. часникового порошку, ¼ ч. л. солі, дрібку перцю та обваляйте в цьому 400 г курячого філе, нарізаного кубиками, после чого обсмажте м’ясо на 1 ст. л. рафінованої олії та перекладіть на серветку. Далі обсмажте на 1 ч. л. олії овочі до рум’яності: 200 г горошку в стручках (попередньо прибравши хвостики), 150 г половинок брюссельської капусти та 50 г тертої моркви, приправивши їх ¼ ч. л. часникового порошку, ¼ ч. л. солі та дрібкою перцю. Паралельно зваріть 100 г рису басматі, заправивши його в кінці 0,5 ч. л. соку лайма. Для соусу з’єднайте 1 ч. л. меду, ½ ст. л. томатного соусу, 1 ст. л. соєвого соусу, 1 ч. л. кунжутної олії, ¼ ч. л. меленого імбиру, дрібку солі й 5 г насіння кунжуту. Викладіть у тарілку рису, курки та овочів, полийте соусом, прикрасьте зеленою цибулею.»,

        «Птітім з лососем та пореєм»: «100 г птітіму – відваріть у великій кількості води до готовності. Приправте 300 г філе лосося сіллю та перцем, обсмажте на 1 ч. л. оливкової олії з обох боків до золотистої скоринки, вийміть та відкладіть. У ту саму сковороду додайте 1 ст. л. масла гхі, нарізану кільцями цибулю порей і повільно обсмажуйте її до м’якості й карамелізації. Тоді додайте 150 г спаржі (нарізаної шматочками) та 2 зубчики часнику, обсмажуйте все разом 1–2 хвилини. Влийте 60 мл вершків 20% і дайте соусу злегка закипіти. Додайте птітім і ретельно перемішайте. Розділіть пасту по тарілках, додайте шматочки лосося, збризніть лимонним соком, посипте зеленою цибулею.»,

        «Сирники з черешнею»: «Візьміть 200 г кисломолочного сиру 5%, 1 жовток, 20 г манки чи цільнозернового борошна, цукрозамінник за потреби. Все змішайте, сформуйте сирники, обваляйте їх в додатковій муці та посмажте на сковороді, змащеній маслом (не більше 1 ч. л.) або запечіть в духовці при 180°С до золотистої скоринки. Додайте 100 г несолодкого йогурту 1,5-2,5% та 150 г черешні чи інші ягоди за смаком.»,

        «Тушкований язик з овочами та гречкою»: «100 г гречки – відваріть. 1 невеликий телячий язик (300г) відваріть до готовності охолодіть і почистьте. Наріжте півкільцями 1 цибулину, солодкий перець та невеликий кабачок (200 г), 1 моркву та цибулю порей (100 г) – тоненькими кільцями. Протушкуйте на 1 ст. л. олії чи масла цибулю та моркву, додайте всі інші овочі, ½ бульйонного кубика та трішки води, перемішайте і тушкуйте під кришкою 5-7 хв, поки кабачки трішки зм’якнуть, але ще будуть хрумкими, додайте порізаний соломкою язик, ще раз перемішайте і зніміть з вогню. Залиште овочі під кришкою на 10 хв. Подавайте з гречкою та свіжою зеленню.»,

        «Тортилья з омлетом та тунцем»: «Вилийте на розігріту сковороду 1 збите яйце з 20 г тертого пармезану, а коли омлет почне схоплюватися, накрийте його зверху 1 листом тортильї, притисніть, переверніть усе разом з допомогою тарілки і зніміть із вогню. Окремо змішайте велику банку консервованого тунця у власному соку (185 г) з 2 ст. л. грецького йогурту, гірчицею за смаком, додайте лимонний сік, трішки меду, сіль та перець. Викладіть на сторону омлету 30 г свіжого шпинату, нарізану півкільцями половину червоної цибулі, шматочки огірка та заправленого тунця. Щільно загорніть у рол, за бажанням злегка підсушіть на сухій сковороді до хрусткої скоринки.»,

        «Боул з нутом та спаржею»: «Відваріть 60 г кіноа. 100 г спаржі очистьте, відламайте тверду частину стебла та наріжте шматками по 2 см. Обсмажте з допомогою 5 г масла гхі – 3-5 хв до готовності (спаржа має залишатись злегка хрумкою). За бажанням додайте часник. Наріжте на четвертинки 100 г томатів чері та 60 г міні моцарели. Змішайте в глибокій тарілці половину кіноа з ½ банки консервованого нуту, спаржею, томатами, додайте 10 г кедрових горішків або насіння. Заправте боул соусом: 1 ч. л. оливкової олії змішайте з 1 ст. л. бальзамічного оцту, ½ ч. л. меду чи сиропу, ½ ч. л. гірчиці, додайте сіль на перець за смаком.»,

        

        // Тиждень 2

        «Вівсянка з беконом, фетою і перцем»: «Відваріть 50 г плющеної вівсянки в підсоленій воді до готовності. На сухій сковороді обсмажте 30 г бекону, перекладіть його на паперовий рушник. Приготуйте 1 яйце зручним способом. У тарілку викладіть вівсянку, додайте бекон, яйце, 30 г фети, порцію маринованого перцю, що залишилась, та 20 г руколи. Полийте 1 ч. л. бальзамічного оцту та оливкової олії.»,

        «Маринована скумбрія з картоплею»: «Для маринаду змішайте 500 г кип’яченої води, 35 г цукру, 30 г солі, 1 лавровий листок та 3 шт перцю горошком до повного розчинення, після чого залиште суміш охолонути повністю. Патрану й нарізану шматочками скумбрію (300 г) залийте холодним маринадом і залиште в холодильнику на добу. Після цього злийте весь маринад, додайте до риби нарізану звичайну та зелену цибулю, 1 ст. л. яблучного оцту та 1 ч. л. оливкової олії, ретельно перемішайте і залиште в холодильнику ще мінімум на 30 хвилин перед подачею. Почистьте 400 г молодої картоплі та відваріть до готовності. Злийте воду і заправте 1 ст. л. вершкового масла, зубчиком розчавленого часнику та кропом.»,

        «Скляна локшина з телятиною»: «300 г яловичого стейку чи вирізки наріжте тонкими смужками, змішайте з 1 ч. л. коричневого цукру, 1 ст. л. соєвого соусу та 3 подрібненими зубчиками часнику. Залиште маринуватися на 30 хвилин. 60 г скляної локшини (фунчоза) приготуйте згідно з інструкцією, промийте холодною водою, змішайте з 1 ч. л. кунжутної олії. 100 г шпинату бланшуйте 30 секунд в окропі, охолодіть і відтисніть зайву воду. 1 цибулю, 1 болгарський перець, 1 моркву та 3 зелені цибулини наріжте соломкою. 7-8 середніх печериць розріжте на 4 частини. На 1 ч. л. олії обсмажте цибулю, моркву, гриби, болгарський перець по 1-2 хвилини, щоб залишились аль денте, відкладіть. Додайте ще 1 ч. л. олії, обсмажте яловичину до готовності. У великій мисці змішайте вермішель, овочі, яловичину, 1 ст. л. соєвого соусу, 1 ч. л. коричневого цукру, перець і 1 ч. л. кунжуту. Перемішайте, подавайте холодним.»,

        «Томатна вівсянка з куркою»: «Дрібно наріжте ½ цибулини та 1 зубчик часнику й обсмажте на середньому вогні до золотистості на 0,5 ч. л. масла гхі. Додайте 2 невеликі томати без шкірки (або 200 г консервованих у власному соку), влийте 100 г води, накрийте кришкою і протушіть 3–5 хвилин, після чого розімніть томати виделкою до стану грубого пюре. Насипте у каструлю 50 г плющеної вівсянки, додайте 100 г води, сіль, перець та базилік за смаком. Перемішайте, накрийте кришкою і варіть на слабкому вогні 15–20 хвилин до м’якості. Паралельно приготуйте 1 яйце пашот або яєчню. Викладіть готову томатну вівсянку в тарілку, зверху додайте шматочки приготованого курячого філе (з 8 дня), яйце, 20 г пармезану, 20 г салатної зелені та 100 г маринованих овочів.»,

        «Шакшука з креветками»: «На 10 г вершкового масла обсмажте до напівготовності 120 г очищених і нарізаних шматочками сирих креветок, ½ кабачка та ½ цибулини. Додайте 40 г свіжого шпинату й 120 г консервованого нуту, вичавіть 1 зубчик часнику і готуйте все разом ще 2 хвилини. Зменште вогонь до мінімуму, обережно вбийте в овочеву суміш 2 яйця та полийте зверху 20 г вершків 20%. Приправте сванською сіллю й перцем за смаком, рівномірно посипте 10 г пармезану, накрийте кришкою і тушкуйте 3–5 хвилин до бажаної готовності яєць. Перед подачею посипте кріспі цибулею.»,

        «Сирні вафлі з тунцем та авокадо»: «Змішайте до однорідності 100 г кисломолочного сиру, 1 яєчний жовток, 30 г натертої моцарели та 1 ст. л. борошна. Добре розігрійте вафельницю чи гриль та змастіть її панелі краплею олії. Викладіть тісто і випікайте вафлі до насиченого золотисто-коричневого кольору. Тим часом, розімніть виделкою 1 невелике стигле авокадо разом із маленькою банкою консервованого тунця у своєму соці, додайте дрібно нарізану цибулю, посоліть та перемішайте. Доповніть страву жменею руколи або шпинату, кількома томатами чері та маринованою цибулею.»,

        «Курка з едамаме і арахісовим соусом»: «100 г рису басматі – відваріть до готовності. Наріжте середніми кубиками 300 г курячого філе, а 100 г печериць, 1 цибулину, 1 моркву, ½ баклажану та 100 г порею — невеликими кубиками. Обсмажте курку на сковороді з 1 ч. л. олії до золотистості (5–7 хвилин), потім додайте всі овочі та 100 г бобів едамаме, вичавіть зубчик часнику, влийте 2 ст. л. соєвого соусу й тушкуйте ще кілька хвилин до готовності. Дістаньте курку з овочами, а у сік, що залишився на дні, додайте 1 ст. л. арахісової пасти, 1 ч. л. меду та 1 ч. л. оцту. Прогрійте на мінімальному вогні 1–2 хвилини, помішуючи до густої емульсії.»,

        «Омлет з баклажанами і моцарелою»: «2-3 скибочки баклажану, нарізаного вздовж слайсами товщиною 2-3 мм, посоліть та приправте за бажанням. Викладіть на дно невеликої сковорідки в 1 шар і злегка обсмажте з 2 сторін до золотистості. Яйце, білок що залишився зі вчора та трохи молока збийте, додайте 10 г тертого пармезану, посоліть і вилийте до баклажанів. Решту моцарели, 2 шматочки в’ялених томатів та зелень поріжте довільними шматками і посипте омлет, додайте пряні трави. Накрийте кришкою і готуйте на маленькому вогні до готовності. Складіть омлет навпіл і покладіть в тарілку.»,

        «Хек у місо-масці з бататом та броколі»: «Наріжте кубиками 600 г батату, додайте сванську сіль, викладіть на деко, збризніть олією і запікайте 15 хвилин при 200°С. Тим часом змішайте 1 ст. л. розм’якшеного вершкового масла, 1 ст. л. білої пасти місо, 1 ч. л. рисового оцту та 1 ст. л. коричневого цукру до однорідності. Промокніть паперовим рушником 400 г філе хека і щедро змастіть підготовленим місо-маслом. Посуньте батат на деку, звільнивши місце для риби та 400 г розібраної на суцвіття броколі. Броколі збризніть 1 ч. л. оливкової олії, сіллю та перцем. Запікайте все разом ще 10 хвилин, после чого увімкніть режим конвекції на 3–4 хвилини до карамелізації риби. Перед подачею посипте броколі кунжутом.»,

        «Сирна галета з абрикосами і мигдалем»: «Змішайте 1 брикет кисломолочного сиру (180-200г) (або перебийте зернистий блендером), 1 яйце, 2 ст. л. цукрозамінника або цукру, дрібку солі та ваніліну, 2 ст. л. борошна з гіркою (60 г) та 1 ч. л. розпушувача до однорідності. Вийде рідке тісто консистенції сметани, викладіть його на пергамент і сформуйте тонке коло з бортиками. Всередину викладіть 300 г нарізаного дольками абрикосів, змішаних із 1 ч. л. кукурудзяного крохмалю. Притрусіть фрукти 30 г нарізаного мигдалю, полийте 1 ч. л. меду та викладіть зверху 20 г вершкового масла, нарізаного тонкими скибками. Запікайте при 180°С протягом 20–25 хвилин до золотистої скоринки.»,

        «Тофу з булгуром і битими огірками»: «Розріжте 400 г тофу навпіл уздовж, зменшуючи його товщину. Покладіть по боках шматків палички для суші (щоб не прорізати наскрізь) і зробіть часті надрізи вздовж завтовшки 2–3 мм. Викладіть тофу у форму з пергаментом. Змішайте по 1 ст. л. соусу теріякі і соку лайма а також по 1 ч. л. меду, натертого часнику, імбиру та цедри лайма. Ретельно змастіть цим соусом тофу, заходячи в усі жолобки, та запікайте 15 хвилин при 180°С у режимі конвекції. Паралельно відваріть 100 г булгуру. 4-5 огірків розріжте навпіл уздовж, покладіть у пакет і злегка відбийте. Додайте в пакет по 1 ст. л. кунжутної олії, яблучного оцту й соєвого соусу, 1 ч. л. меду та 2 розчавлені зубчики часнику. Струсіть пакет і залиште в холодильнику на 30 хвилин.»,

        «Салат-паста з куркою»: «Змішайте 500 г курячого філе, розрізаного навпіл з 1 ч. л. оливкової олії, 0,5 ч. л. середземноморських трав та ¼ ч. л. солі й перцю. Обсмажте м’ясо на сковороді або грилі по 6–7 хвилин з кожного боку до готовності, дайте відпочити та наріжте кубиками. Паралельно відваріть 100 г пасти. У велику миску викладіть пасту, 2/3 курки, 1 нарізаний кубиками огірок, 150 г помідорів чері розрізаних навпіл, 1 червону цибулю — півкільцями, 40 г оливок, 40 г подрібненої фети, 30-40 г руколи та 10 г свіжого кропу. Окремо змішайте для заправки 1 ст. л. оливкової олії, 1 ст. л. лимонного соку, по 1 ч. л. діжонської гірчиці й води та ¼ ст. л. середземноморських трав.»,

        «Йогурт-боул з нутом та яйцем»: «Промийте 120 г (½ банки) консервованого нуту, ретельно висушіть паперовим рушником, змішайте з 0,5 ч. л. оливкової олії та спеціями (сіль, часник, копчена паприка) і запікайте на деку при 200°С протягом 20–25 хвилин до золотистої скоринки. Окремо відваріть 1 яйце так, щоб жовток залишився рідким. Для бази боула змішайте 150 г густого йогурту 1,5-3% з дрібкою солі, перцю та цедрою лимона й викладіть на дно глибокої тарілки. Зверху на йогуртову подушку викладіть запечений нут, 30 г фети, 20 г руколи, 1 нарізаний шматочками огірок, жменю половинок томатів чері, мариновану цибулю та розрізане навпіл яйце.»,

        

        // Тиждень 3

        «Салат-паста з куркою та квасолею»: «Зваріть 80 г макаронів твердих сортів до стану al dente. На сковороді з 1 ч. л. оливкової олії обсмажте нарізані кубиками 1 солодкий перець і половину червоної цибулини, 300 г курячого філе й готуйте до повної готовності. Перекладіть макарони у велику миску, додайте 150 г нарізаних помідорів, 40 г свіжого шпинату, по половині банки червоної квасолі та кукурудзи, а також курку з овочами. Приправте мексиканською приправою чи сванською сіллю, додайте сіль, перець, паприку, 1 ч. л. оливкової та 50 г рікоти і ретельно перемішайте.»,

        «Боул з бататом та яйцями»: «Приготуйте 2 яйця на сковороді на ваш смак (яєчня або скрембл), приправивши сіллю та спеціями. У глибоку тарілку викладіть окремими купками жменю свіжої руколи, розігрітий запечений батат (з 13 дня), помідори чері 150 г, мариновану цибулю та нарізане слайсами авокадо. Туди ж викладіть 100 г сиру рікота та готові гарячі яйця.»,

        «Дорадо з овочами і картоплею»: «Змішайте 1 ст. л. оливкової олії з соком ½ лимона, сіллю, перцем та приправою до риби. На очищеній дорадо (500–600 г) зробіть кілька косих надрізів по боках, ретельно змастіть маринадом і залиште на 15–20 хвилин. Наріжте шматочками 400 г картоплі, 1 моркву, 1 цибулину та 1 цукіні, викладіть у форму, злегка приправте сіллю, перцем, збризніть 1 ч. л. олії і перемішайте. Зверху на овочеву подушку викладіть замариновану рибу та запікайте в розігрітій до 180°С духовці 35–40 хвилин до готовності.»,

        «Авокадо-тост з омлетом»: «Підсушіть 80 г цільнозернового хліба і намажте його м’яким авокадо (80-100 г), чи приготуйте гуакамоле. Приготуйте омлет із 2 яєць, посоліть за смаком. Викладіть на тости порізаний слайсами огірок, томат, 20 г улюбленої зелені (рукола, маш, мікс). Зверху покладіть омлет, посипте його 10 г натертого пармезану та дрібно порізаною зеленою цибулею чи кропом.»,

        «Боул з фрикадельками та запеченим перцем»: «Змішайте 300 г нежирного яловичого фаршу, 0.5 ст. л. соусу шрірача, 2 подрібнені зубчики часнику, 1.5 ст. л. нарізаної петрушки, 2 ст. л. сухарів, жовток яйця, по 0.5 ч. л. орегано та солі. Сформуйте невеликі кульки й викладіть на деко з пергаментом. Поруч викладіть 4-5 солодкі червоні перці, розрізані навпіл. Запікайте фрикадельки 15 хвилин, а перець — 25–30 хвилин до м’якості. Зваріть 80 г кіноа. Для соусу цацикі з’єднайте 200 г грецького йогурту, 1 натертий невеликий огірок, 1 вичавлений зубчик часнику, 1 ч. л. оливкової олії, 0.5 ст. л. білого оцту, сік та цедру ½ лимона, 0.5 ст. л. кропу та ¼ ч. л. солі. 4 половинки запеченого перцю очистьте від шкірок і наріжте смужками, змішайте в мисці зі 50 г подрібненої фети, 1 ч. л. оливкової олії, 0.5 ч. л. орегано та дрібкою солі.»,

        «Рол у тортильї з тунцем та норі»: «Дрібно наріжте 1 огірок та кілька пір’їн зеленої цибулі, розімніть банку тунця у власному соці. Для заправки змішайте 50 г рікоти, трохи густого йогурту, соєвого соусу та соусу теріякі за смаком. З’єднайте в мисці огірок, цибулю, тунець, кукурудзу що залишилась (½ банки) і заправку, ретельно все перемішайте. Викладіть на тортилью листки норі, зверху рівномірно розподіліть приготовану начинку, притрусіть кунжутом і щільно загорніть у рол.»,

        «Запечені ньоки з квасолею, овочами і фетою»: «½ банки консервованої квасолі, що залишилась – промийте, та відцідіть. 1 баклажан, 1 солодкий перець та 1 червону цибулю – наріжте кубиками. У формі для запікання змішайте квасолю з 200 г ньокі, цибулею і баклажаном, додайте 300 г томатів чері, 4 зубчика натертого часнику. Заправте 2 ст. л. оливкової олії, ½ ч. л. чебрецю та орегано, посоліть за смаком. Запікайте при 180°С приблизно 25 хв. Посипте 50 г фети, 20 г порізаних оливок та кількома листками базиліку.»,

        «Ліниві вареники з персиками-гриль і рікотою»: «Перетріть вилкою чи блендером 300 г сиру, додайте 1 яйце, 3 ст. л. борошна, дрібку солі (+ дрібка, щоб додати в воду при варінні) підсолоджувач за смаком, 3 г цедри лимона. Вимішайте усе, сформуйте ковбаску і поріжте або сформуйте кульки. Відваріть у злегка підсоленій воді, поки вони спливуть і дістаньте. Наріжте великий персик (150 г) скибочками, і обсмажте на розігрітій сковороді з 1 ч. л. масла по 2–3 хв з кожного боку, до рум’яності. Викладіть вареники, додайте 50 г рікоти, 1-2 ст. л йогурту і персики гриль.»,

        «Запечена картопля з куркою та фенхелем»: «Натріть сіллю 2 курячих стегна і залиште на кілька хвилин. Змішайте до однорідності 1 ст. л. місо-пасти та 1 ст. л. розм’якшеного вершкового масла. Наріжте великими шматочками ½ цибулини-порею, 400 г молодої картоплі та 1 невелику цибулину фенхелю, викладіть їх на деко разом із 5-6 зубчиками часнику, збризніть оливковою олією, посоліть і перемішайте. Промокніть курку насухо паперовим рушником, щедро змастіть підготовленим місо-маслом і викладіть зверху на овочеву подушку поруч із половиною лимона. Запікайте 1 годину при 190°С, вливши на деко 100 мл курячого бульйону на середині приготування.»,

        «Сирний омлет зі шпинатом»: «100 г кисломолочного сиру викладіть на розігріту сковорідку з невеликою кількістю вершкового масла і прогрійте 3–4 хвилини. Додайте нарізаний помідор і 50 г шпинату. 2 яйця збийте з сіллю, перцем та середземноморськими травами за смаком, вилийте до сковороди і трохи розімніть лопаткою. Посипте 20 г натертого пармезану, накрийте кришкою і готуйте до готовності. Подавайте з тостом цільнозернового хліба (40 г), та порцією маринованого перцю.»,

        «Фунчоза з креветками і грейпфрутом»: «Наріжте соломкою 1 огірок, подрібніть 1 зелену цибулю, розріжте навпіл 100 г помідорів чері, очистіть від плівок і розділіть на дольки ¼ грейпфруту та ¼ апельсину. Подрібніть ножем або в комбайні 400 г очищених креветок разом із 1 ч. л пасти том-ям, 1 ст. л. рибного соусу та 1 зубчиком часнику. Обсмажте цю масу на сковороді з 1 ч. л. олії протягом 4–5 хвилин, постійно розмішуючи лопаткою до готовності, та відставте. Для заправки змішайте 1 ч. л. пасти том-ям, 1 зубчик часнику, 1 ст. л. соєвого соусу, соком ½ лайма та 1 ч. л. цукру. Зваріть у киплячій воді 60 г фунчози за інструкцією, злийте воду та промийте холодною водою, заправте ½ ч. л. кунжутної олії. З’єднайте локшину, цитрусові, огірок, чері, цибулю, додайте обсмажені креветки, полийте заправкою і ретельно перемішайте.»,

        «Тости з фетою, яйцями і маринованим перцем»: «Підсушіть 80 г цільнозернового хліба. На пательні, розігрівши 5 г олії гхі, приготуйте 2 яйця на ваш смак (яєчню або скрембл) та приправте сіллю за смаком. Намажте тости 50 г фети, додайте шматочки маринованого перцю та мариновану цибулю. В тарілку викладіть смажені яйця і додайте 20 г руколи.»,

        «Запечений лосось з бататом і овочами»: «Наріжте свіжу моркву (300 г) економкою на довгі тонкі стрічки. Добре помніть їх руками в мисці, щоб вони пустили сік і стали м’якими. Для заправки окремо змішайте 1 ст. л. соєвого соусу, 1 ст. л. лимонного соку, 1 ч. л. меду та 0.5 ч. л. солодкої гірчиці. Полийте цим соусом морквяні стрічки та ретельно перемішайте, відставте маринуватись. Наріжте кубиками 400 г батату, збризніть оливковою олією, приправте сванською сіллю і викладіть на деко. Запікайте 15 хвилин при 180°С. Тим часом розділіть на суцвіття 200 г броколі, а 400 г філе лосося наріжте на 3 порційні частини (посоліть та поперчіть рибу). Додайте броколі та лосось на деко до батату і запікайте все разом ще 15-20 хвилин до готовності.»,

        

        // Тиждень 4

        «Запечений омлет з сиром та кабачками»: «У формі для запікання змішайте 4 яйця, 100 г кисломолочного сиру, 200 г кабачка, ½ баклажану та 200 г томату нарізного кубиками, 40 мл вершків 20%, перемішайте, посоліть та додайте спеції за смаком. Зверху посипте 100 г натертої моцарели і запечіть при 180°С 30-40 хв, до готовності. Подавайте разом зі шматком цільнозернового хліба, свіжими овочами та зеленню.»,

        «Намазка з лосося, авокадо та огірка на тостах»: «Візьміть порцію запеченого лосося (з 21 дня) та ретельно розімніть його виделкою на дрібні шматочки. Додайте 1 невелике стигле авокадо, 60 г грецького йогурту 1,5-3% жирності, нарізаний дрібними кубиками огірок та сік ½ лимона. Приправте сіллю і чорним перцем за смаком, після чого ретельно перемішайте все до стану густої, однорідної пасти. Окремо підсушіть 2 шматочки цільнозернового хліба, викладіть на них готову рибну пасту та подавайте разом з 30 г зелені на вибір.»,

        «Пюре з бефстроганов та маринованими кабачками»: «Наріжте 500 г кабачків тонкими слайсами, додайте ½ ч. л. солі, залиште на 10 хвилин і злийте сік, що виділився. Додайте до них дрібно нарізаний кріп, 1–2 вичавлені зубчики часнику, 1 ч. л. меду, 1 ст. л. оцту, 1 ч. л. оливкової олії, цедру 1 лимона, перемішайте та приберіть у холодильник на кілька годин. Відваріть 400 г очищеної картоплі в підсоленій воді, злийте воду, додайте 20 г вершкового масла, 70–100 мл теплого молока і розімніть у гладке пюре. Для бефстроганов наріжте 300 г яловичини тонкими смужками, посоліть, поперчіть і швидко обсмажте на сильному вогні з 1 ст. л. олії по 2–3 хвилини, після чого перекладіть на тарілку. На тій же сковорідці обсмажте нарізану цибулину та 200 г печериць, всипте 1 ч. л. борошна, перемішайте і додайте 80 мл вершків 20%, 1 ч. л. гірчиці, поверніть яловичину и тушкуйте під кришкою на слабкому вогні 5–7 хвилин.»,

        «Тушкований хек з томатами і булгур»: «100 г булгуру – відваріть, додайте 10 г вершкового масла. 2 шматки (400 г) філе білої риби (тріска, хек, дорадо), приправте сіллю, перцем і збризніть 1 ч. л. оливкової олії, злегка втираючи її, відставте. Сковороду змастіть олією, додайте дрібно нарізану червону цибулину, 2 грубо подрібнені зубчики часнику та дрібку солі, готуйте на середньому вогні 3 хвилини до м’якості. Додайте 1 ст. л. каперсів, 400 г помідорів чері, нарізаний смужками болгарський перець та 1 ч. л. сушеного орегано. Зменште вогонь і тушкуйте, розминаючи помідори виделкою до густого рагу, потім покладіть 20 г оливок. Викладіть рибу поверх соусу, накрийте кришкою й тушкуйте 10–12 хвилин на слабкому вогні.»,

        «Запечений батат з грибами, шпинатом та моцарелою»: «Добре помийте 2 картоплини батату (по 200 г кожна), зробіть дірочки виделкою, натріть 1 ч. л. оливкової олії та посоліть. Загорніть їх у фольгу і запікайте до м’якості при 180°С протягом 30–40 хвилин. Тим часом наріжте 300 г печериць, 1 цибулину та 2 зубчики часнику, після чого підсмажте все на 1 ст. л. оливкової олії до готовності. Наприкінці додайте до грибів 50 г шпинату, 10 г вершкового масла, приправте за смаком і тушкуйте ще 2 хвилини. Для соусу змішайте 50 г густого йогурту, цедру половини лимона, лимонний сік за смаком, 20 г дрібно порізаного кропу та сіль. Викладіть на тарілку гарячий батат, зробіть повздовжній розріз, обережно розімніть м’якоть виделкою, змастіть йогуртовим соусом, викладіть зверху гриби зі шпинатом та додайте половинку кульки моцарели.»,

        «Холодна паста з куркою та песто»: «Наріжте кубиками 400 г курячого філе та посоліть його. На пательню викладіть 40 г нарізаного бекону, додайте шматочки курки та обсмажте все разом на виділеному з бекону жирі до готовності. Відваріть 120 грамів сухої пасти в підсоленій воді до готовності, злийте воду і залиште остигати. Наріжте 50 грамів свіжого шпинату, 1 червону цибулю, 20 грамів зеленої цибулі, 2 огірки, 1 солодкий перець та 60 грамів зелених оливок. З’єднайте пасту, овочі, курку з беконом, сік ¼ лимона та 1 ст. л. червоного песто з в’ялених томатів. Посоліть, поперчіть і ретельно перемішайте. Подавайте охолодженим.»,

        «Нічна вівсянка з малиною та арахісовою пастою»: «Змішайте в банці 40 г плющеної вівсянки, 10 г чіа (1 ст. л.), 150 г молока, 100 г йогурту, 5-10 г ванільного екстракту чи цукру і 1 ч. л. меду (можна замінити на будь-який інший рідкий підсолоджувач). Струсіть все і перемішайте. Закрийте вівсянку кришкою і поставте в холодильник на ніч. Зранку вівсянка готова до споживання, перед подачею додайте 150 г ягід малини і 20 г арахісової пасти.»,

        «Боул з лососем і хрумким рисом»: «Промийте та відваріть 50 г рису басматі, змішайте його з 1 ч. л. олії чилі, ½ ст. л. соєвого соусу, викладіть на деко та запікайте в духовці при 190°С близько 30 хвилин, періодично помішуючи. Намажте 200 г сирого лосося сумішшю з ½ ч. л. солі, перцю, 1 ч. л. часникового порошку і запікайте в духовці або аерогрилі при 190°С протягом 13–16 хвилин, після чого розберіть на шматочки. Заморожені очищені боби едамаме (150 г) залийте окропом на 2–3 хвилини, після чого злийте воду і промийте холодною водою. Для заправки ретельно змішайте 100 г густого йогурту, 1 ст. л. легкого майонезу, 1 ч. л. кунжутної олії, ½ ст. л. рисового оцту, 2 ст. л. соєвого соусу, 1 ч. л. меду, по ½ ч. л. меленого імбиру й часникового порошку, сіль та перець. З’єднайте половину запеченого рису, едамаме та лосося, нарізаний огірок, ½ подрібненої червоної цибулі, 100 г нашаткованої червоної капусти, полийте все половиною соусу, перемішайте та посипте зеленою цибулею і насінням кунжуту.»,

        «Лаймова курка з салатом з квасолі»: «Замаринуйте 300 г нарізаного смужками філе курячої грудки в суміші соку ½ лимона, 1 ч. л. меду, по ½ ч. л. копченої паприки, гранульованого часнику, орегано, оливкової олії, солі та перцю на 15–20 хвилин, после чого обсмажте на пательні до готовності. Для салату збризніть лимонним соком ½ тонко нарізаної червоної цибулини, залиште на пару хвилин, потім додайте 1 банку зцідженої великої квасолі, 200 г помідорів чері, 1 нарізаний огірок, 30 г подрібненої петрушки та 40 г оливок. Збризніть оливковою олією і 1–2 ч. л. червоного винного оцту. Збийте блендером 80 г фети, 150 г грецького йогурту, 1 ч. л. меду, дрібку солі до кремового стану. Розкладіть салат у тарілку, зверху викладіть курку, додайте порцію соусу.»,

        «Яблучні вафлі»: «Перебийте блендером 100 г кисломолочного сиру, 1 яйце, 50 г йогурту, 0.5 ч. л. розпушувача та 50 г борошна до однорідності. Додайте 100 г тертих яблук, дрібку солі, цукор за потреби, ретельно перемішайте та випікайте у вафельниці (або обсмажте на сковорідці як оладки) протягом 5–7 хвилин. Подавайте готові вафлі з 50 г несолодкого йогурту та 15 г арахісової пасти.»,

        «Яєчний салат з йогуртом та авокадо»: «Зваріть круто 2 яйця. 1 пагін селери, 2-3 пагони зеленої цибулі, огірок і 1 невелике авокадо, яйця, наріжте кубиками та складіть у миску. Додайте 1 ст. л. грецького йогурту, 1 ч. л. гірчиці, сіль і перець, додайте заправку до нарізаних інгредієнтів і обережно перемішайте до кремової текстури. 80 г цільнозернового хліба підсмажте до хрумкої скоринки та подавайте з салатом.»,

        «Тако-боул з яловичиною та картоплею»: «Батат (200 г) та картоплю (200 г) наріжте кільцями, середньої товщини, додайте улюблені спеції, посоліть і збризніть 1 ч. л. оливкової олії. Перекладіть на деко, застелене пергаментним папером тонким шаром і запечіть при 180°С 15-25 хв, до готовності. 300 г нежирного яловичого фаршу обсмажте на сковорідці разом з 1 дрібно нарізаною цибулиною до готовності, додайте сванську сіль та відставте. Наріжте дрібним кубиком 1 невеликий солодкий перець, 1 томат та ½ червоної цибулі, посоліть і заправте 1 ч. л. оливкової олії, збризніть лимонним соком. Викладіть в тарілку картоплю, фарш та салат.»,

        «Салат з квасолею, куркою та кукурудзою»: «Наріжте 300 г курячого філе шматочками, приправте за смаком і обсмажте на сковорідці з 1 ч. л. олії до готовності. Попередньо відваріть 2 середні качани кукурудзи в солоній воді та зріжте з них зерна ножем. Приготуйте соус: збийте 1 ст. л. оливкової олії, 1 ст. л. яблучного оцту, 1 ст. л. соку лайма, 1 ч. л. меду, по ½ ч. л. копченої паприки та часникового порошку, ½ ч. л. солі й ¼ ч. л. чорного перцю. У велику миску викладіть 1 банку промитої та зцідженої квасолі, зрізану кукурудзу, дрібно нарізаний солодкий перець, 150 г розрізаних навпіл помідорів чері, дрібно нарізану червону цибулину, подрібнену кінзу або петрушку та шматочки готової курки. Полийте салат заправкою, ретельно перемішайте та поставте в холодильник щонайменше на 30 хвилин перед подачею.»

    };

     

    function getRecipeByTitle(title) { 

        return fullRecipes[title] || «Інгредієнти та спосіб приготування: свіжі продукти згідно з меню. Дотримуйтесь інструкції «здорової тарілки».»; 

    }

     

    // —————— ДАНІ МЕНЮ (КОРЗИНИ, КАЛЕНДАР, ДЕСЕРТИ) ——————

    const basketsData = {

       1: { title: «🛒 Корзина тиждень 1», items: { 

            «🥬 Овочі»: [«160г листової зелені», «Пряні трави (цибуля, кріп, петрушка, базилік)», «2 солодких перці», «600г томатів чері», «1 огірок», «750г спаржі», «2 маленькі кабачки», «400г молодої картоплі», «2 морквини», «300г брюссельської капусти», «400г цибулі порей», «200г горошку в стручках», «1 авокадо», «300г інших овочів»], 

            «🌾 Крупи»: [«Вівсянка плющена», «Гречка», «Булгур», «Кіноа чорна», «Рис Басматі», «Птітім», «Макарони твердих сортів», «Удон або соба»],

            «🍗 М’ясо/Риба»: [«400г курячого філе», «4 курячі гомілки», «300г яловичої вирізки», «300г філе лосося», «150г креветок», «300г філе тріски», «300г яловичого язика»], 

            «🥛 Молочні»: [«400г сиру 5%», «300г йогурту», «70г фети», «180мл молока», «60-125мл вершків 20%», «55г пармезану», «1 уп. моцарели міні»], 

            «🍓 Фрукти»: [«900г лохини», «750г черешні», «600г полуниці»], 

            «🥫 Консерви»: [«2 банки тунця», «1 банка нуту», «65г кокосових вершків»], 

            «🥚 Інше»: [«11 яєць», «30г насіння», «160г хліба», «1 тортилья», «оливки», «в’ялені томати»]    } },

        2: { 

            title: «🛒 Корзина тиждень 2», 

            items: { 

                «🥬 Овочі»: [«100г зелені», «Пряні трави (цибуля, кріп)», «140г шпинату», «2 перці», «650г томатів», «9 огірків», «1 кабачок», «1 баклажан», «400г картоплі», «600г батату», «3 морквини», «1 буряк», «300г печериць», «400г броколі», «100г порею», «100г едамаме», «1 авокадо»], 

                «🌾 Крупи»: [«Вівсянка плющена», «Булгур», «Рис Басматі», «Макарони твердих сортів», «Фунчоза 60г»],

                «🍗 М’ясо/Риба»: [«800г курки», «300г яловичини», «120г креветок», «400г хека», «1 скумбрія 300г»], 

                «🥛 Молочні»: [«300г сиру», «150г йогурту», «140г фети», «300мл молока», «20мл вершків», «40г пармезану», «100г моцарели», «400г кефіру»], 

                «🍑 Фрукти»: [«150г полуниці», «600г лохини», «900г черешні», «600г абрикосів»], 

                «🥫 Консерви»: [«1 банка тунця 80г», «1 банка нуту», «400г тофу»], 

                «🥚 Інше»: [«10 яєць», «70г горіхів», «160г хліба», «10г кунжуту», «оливки»] 

            } 

        },

       3: { 

            title: «🛒 Корзина тиждень 3», 

            items: { 

                «🥬 Овочі»:[«100г салату», «Пряні трави (цибуля, кріп, петрушка)», «90г шпинату»,»7 перців»,»1000г томатів»,»5 огірків»,»1 кабачок»,»1 баклажан»,»800г картоплі»,»400г батату»,»4 морквини»,»200г броколі»,»1 порей»,»1 фенхель»,»2 авокадо»], 

                «🌾 Крупи»: [«Кіноа чорна», «Макарони твердих сортів», «Фунчоза 60г», «Ньоки 200г»],

                «🍗 М’ясо/Риба»:[«300г курки»,»2 стегна»,»300г фаршу»,»400г креветок»,»500-600г дорадо»,»400г лосося»], 

                «🥛 Молочні»:[«400г сиру»,»300г йогурту»,»200г фети»,»250г рікоти»,»30г пармезану»], 

                «🍊 Фрукти»:[«апельсин 1 шт»,»грейпфрут 1 шт»,»4 персика»,»300г лохини»,»250г малини»,»600г кавуна»], 

                «🥫 Консерви»:[«1 банка тунця 185г»,»1 банка квасолі»,»1 банка кукурудзи»], 

                «🥚 Інше»:[«10 яєць»,»10г горіхів»,»200г хліба»,»кунжут»,»оливки»,»норі 1 пачка»] 

            } 

        },

       4: { 

            title: «🛒 Корзина тиждень 4», 

            items: { 

                «🥬 Овочі»:[«100г зелені», «Пряні трави (цибуля, кріп, петрушка)», «100г шпинату»,»2 перці»,»900г томатів»,»6 огірків»,»100г редиски»,»700г кабачків»,»1 баклажан»,»700г картоплі»,»400г батату»,»1 морква»,»600г печериць»,»200г червоної капусти»,»150г едамаме»,»авокадо»], 

                «🌾 Крупи»: [«Вівсянка плющена», «Булгур», «Рис Басматі», «Макарони твердих сортів»],

                «🍗 М’ясо/Риба»:[«700г курки»,»300г індички»,»300г яловичини»,»400г хека»,»200г лосося»,»100г бекону»], 

                «🥛 Молочні»:[«100г сиру»,»600г йогурту»,»150г фети»,»200г моцарели»,»250мл молока»,»120мл вершків»,»500мл кефіру»], 

                «🍈 Фрукти»:[«4 персики»,»600г кавуна»,»600г дині»,»750г малини»], 

                «🥫 Консерви»:[«1 банка квасолі»], 

                «🥚 Інше»:[«7 яєць»,»10г горіхів»,»320г хліба»,»кунжут»,»чіа 1 ст.л.»,»оливки 140г»] 

            } 

        },

        extra: { title: «🛒 Корзина 29-31 дні», items:{ 

                «🥬 Овочі»:[«20г зелені», «Пряні трави (цибуля, петрушка, базилік)», «2 перці»,»150г томатів»,»2 огірки»,»1 стебло селери»,»1 кабачок»,»200г картоплі»,»200г батату»,»1 авокадо»], 

                «🌾 Крупи»: [«Вівсянка плющена», «Макарони твердих сортів»],

                «🍗 М’ясо/Риба»:[«300г курки»,»300г фаршу»], 

                «🥛 Молочні»:[«200г сиру»,»130г йогурту»,»200мл молока»,»20г пармезану»], 

                «🍉 Фрукти»:[«600г кавуна»,»400г дині»,»100г малини»], 

                «🥫 Консерви»:[«1 банка тунця 185г»,»1 банка квасолі»], 

                «🥚 Інше»:[«3 яйця»,»80г хліба»,»оливки»,»каперси»] 

            } }

    };

    const weeksCalendar = {

        1: { title: «Тиждень 1 · Освіжаючий», macro: «≈1620 ккал», days: [{dayName:»Понеділок 1″,breakfast:»Авокадо-тост з сиром та яйцем»,lunch:»Паста з кабачками і тунцем»,dinner:»Лимонні гомілки з картоплею та спаржею»},{dayName:»Вівторок 2″,breakfast:»Вівсянка з песто і яйцем»,lunch:»Лимонні гомілки з картоплею та спаржею»,dinner:»Тушкована тріска з креветками в соусі карі»},{dayName:»Середа 3″,breakfast:»Яєчний млинець з сиром і ягодами»,lunch:»Тушкована тріска з креветками в соусі карі»,dinner:»Локшина з яловичиною та овочами»},{dayName:»Четвер 4″,breakfast:»Яєчний салат з в’яленими томатами»,lunch:»Локшина з яловичиною та овочами»,dinner:»Медово-кунжутна курка з рисом та овочами»},{dayName:»П’ятниця 5″,breakfast:»Вівсянка з яйцем, фетою і томатами»,lunch:»Медово-кунжутна курка з рисом та овочами»,dinner:»Птітім з лососем та пореєм»},{dayName:»Субота 6″,breakfast:»Сирники з черешнею»,lunch:»Птітім з лососем та пореєм»,dinner:»Тушкований язик з овочами та гречкою»},{dayName:»Неділя 7″,breakfast:»Тортилья з омлетом та тунцем»,lunch:»Тушкований язик з овочами та гречкою»,dinner:»Боул з нутом та спаржею»}] },

        2: { title: «Тиждень 2 · Ягодно-пряний», macro: «≈1630 ккал», days: [{dayName:»Понеділок 8″,breakfast:»Вівсянка з беконом, фетою і перцем»,lunch:»Боул з нутом та спаржею»,dinner:»Салат-паста з куркою»},{dayName:»Вівторок 9″,breakfast:»Йогурт-боул з нутом та яйцем»,lunch:»Салат-паста з куркою»,dinner:»Маринована скумбрія з картоплею»},{dayName:»Середа 10″,breakfast:»Томатна вівсянка з куркою»,lunch:»Маринована скумбрія з картоплею»,dinner:»Скляна локшина з телятиною»},{dayName:»Четвер 11″,breakfast:»Шакшука з креветками»,lunch:»Холодник»,dinner:»Скляна локшина з телятиною»},{dayName:»П’ятниця 12″,breakfast:»Сирні вафлі з тунцем та авокадо»,lunch:»Холодник»,dinner:»Курка з едамаме і арахісовим соусом»},{dayName:»Субота 13″,breakfast:»Омлет з баклажанами і моцарелою»,lunch:»Курка з едамаме і арахісовим соусом»,dinner:»Хек у місо-масці з бататом та броколі»},{dayName:»Неділя 14″,breakfast:»Сирна галета з абрикосами і мигдалем»,lunch:»Хек у місо-масці з бататом та броколі»,dinner:»Тофу з булгуром і битими огірками»}] },

        3: { title: «Тиждень 3 · Середземномор’я», macro: «≈1635 ккал», days: [{dayName:»Понеділок 15″,breakfast:»Сирна галета з абрикосами і мигдалем»,lunch:»Тофу з булгуром і битими огірками»,dinner:»Салат-паста з куркою та квасолею»},{dayName:»Вівторок 16″,breakfast:»Боул з бататом та яйцями»,lunch:»Салат-паста з куркою та квасолею»,dinner:»Дорадо з овочами і картоплею»},{dayName:»Середа 17″,breakfast:»Авокадо-тост з омлетом»,lunch:»Дорадо з овочами і картоплею»,dinner:»Боул з фрикадельками та запеченим перцем»},{dayName:»Четвер 18″,breakfast:»Рол у тортильї з тунцем та норі»,lunch:»Боул з фрикадельками та запеченим перцем»,dinner:»Запечені ньоки з квасолею, овочами і фетою»},{dayName:»П’ятниця 19″,breakfast:»Ліниві вареники з персиками-гриль і рікотою»,lunch:»Запечені ньоки з квасолею, овочами і фетою»,dinner:»Запечена картопля з куркою та фенхелем»},{dayName:»Субота 20″,breakfast:»Сирний омлет зі шпинатом»,lunch:»Запечена картопля з куркою та фенхелем»,dinner:»Фунчоза з креветками і грейпфрутом»},{dayName:»Неділя 21″,breakfast:»Тости з фетою, яйцями і маринованим перцем»,lunch:»Фунчоза з креветками і грейпфрутом»,dinner:»Запечений лосось з бататом і овочами»}] },

        4: { title: «Тиждень 4 · Насичений», macro: «≈1640 ккал», days: [{dayName:»Понеділок 22″,breakfast:»Вівсянка з беконом, фетою і перцем»,lunch:»Запечений лосось з бататом і овочами»,dinner:»Лаймова курка з салатом з квасолі»},{dayName:»Вівторок 23″,breakfast:»Намазка з лосося, авокадо та огірка на тостах»,lunch:»Лаймова курка з салатом з квасолі»,dinner:»Пюре з бефстроганов та маринованими кабачками»},{dayName:»Середа 24″,breakfast:»Ліниві вареники з персиками-гриль і рікотою»,lunch:»Пюре з бефстроганов та маринованими кабачками»,dinner:»Тушкований хек з томатами і булгур»},{dayName:»Четвер 25″,breakfast:»Шакшука з грибами і баклажаном»,lunch:»Йогуртовий суп з індичкою»,dinner:»Тушкований хек з томатами і булгур»},{dayName:»П’ятниця 26″,breakfast:»Запечений омлет з сиром та кабачками»,lunch:»Йогуртовий суп з індичкою»,dinner:»Запечений батат з грибами, шпинатом та моцарелою»},{dayName:»Субота 27″,breakfast:»Запечений омлет з сиром та кабачками»,lunch:»Запечений батат з грибами, шпинатом та моцарелою»,dinner:»Холодна паста з куркою та песто»},{dayName:»Неділя 28″,breakfast:»Нічна вівсянка з малиною та арахісовою пастою»,lunch:»Холодна паста з куркою та песто»,dinner:»Боул з лососем і хрумким рисом»}] },

        extra: { title: «Фінал · 3 додаткові дні», macro: «≈1615 ккал», days: [{dayName:»Понеділок 29″,breakfast:»Яблучні вафлі»,lunch:»Боул з лососем і хрумким рисом»,dinner:»Салат з квасолею, куркою та кукурудзою»},{dayName:»Вівторок 30″,breakfast:»Яєчний салат з йогуртом та авокадо»,lunch:»Салат з квасолею, куркою та кукурудзою»,dinner:»Тако-боул з яловичиною та картоплею»},{dayName:»Середа 31″,breakfast:»Вівсянка з малиною та арахісовою пастою»,lunch:»Тако-боул з яловичиною та картоплею»,dinner:»Паста з кабачками і тунцем»}] }

    };

    const dessertsList = [

        { name:»Ліниві мочі», kcal:60, recipe:»У мисці ретельно змішайте 150 г густого йогурту 1,5-3%, 100 г рікоти, 15 г нежирного згущеного молока та ванільний екстракт до однорідного крему. Додайте в отриману масу дрібно нарізану свіжу полуницю (150 г) та обережно перемішайте. Намочіть 1 аркуш рисового паперу (всього потрібно 5 шт) теплою водою, щоб він став м’яким. Покладіть порцію полунично-кремової начинки в центр аркуша і акуратно складіть краї конвертиком. Посипте кожну порцію кукурудзяним крохмалем. Охолодіть 4 години.» },

        { name:»Мигдалеві цукерки», kcal:116, recipe:»Збийте блендером 100 г кисломолочного сиру 5% до однорідної маси. Додайте 100 г мигдалевого борошна, 60 г арахісової пасти, 20-30 г еритрітолу та дрібку солі, ретельно перемішайте. Сформуйте 10 кульок і відправте їх у морозильну камеру на 2-3 години. Розтопіть 45 г шоколаду із 1 ч.л. кокосової олії, покрийте кожну кульку глазур’ю і поверніть у холодильник на 30 хвилин.» },

        { name:»Пиріг з лохиною», kcal:125, recipe:»У великій мисці збийте 500 г густого йогурту 3%, 2 яйця, 40 г розтопленого вершкового масла, 60-80 г еритрітолу, 25 г кукурудзяного крохмалю і дрібку солі до однорідної маси. Вмішайте 150 г чорниці/лохини. Вилийте масу у форму з пергаментом і запікайте при 180°С на 40 хвилин. Залиште пиріг вистигати у вимкненій духовці. Розтопіть 45 г шоколаду із 1 ч.л. кокосової олії, полийте пиріг і поставте в холодильник.» },

        { name:»Морозиво з фініками», kcal:140, recipe:»Залийте 50 г фініків окропом на 10 хв і дістаньте кісточки. Збийте 200 г пастоподібного сиру з 50 г сметани 20% та фініками до однорідної текстури, додайте ванільний екстракт за смаком. Перекладіть у форму з кришкою і заморозьте протягом 3-4 годин. Перед подачею дайте йому трохи підтанути.» }

    ];

     

    // —————— ЛОГІКА СИНХРОНІЗАЦІЇ ТА РЕНДЕРУ ——————

    let currentUser = null, currentUserName = null, currentModalDayKey = null;

     

    function getStorageKey(type) { return `summer2026_${type}_${currentUser}`; }

     

    function loadCheckState() { 

        if(!currentUser) return {}; 

        const raw = localStorage.getItem(getStorageKey(‘checks’)); 

        let parsed = raw ? JSON.parse(raw) : {}; 

        return (Array.isArray(parsed) || typeof parsed !== ‘object’) ? {} : parsed; 

    }

    function loadCompletedState() { 

        if(!currentUser) return {}; 

        const raw = localStorage.getItem(getStorageKey(‘completed’)); 

        let parsed = raw ? JSON.parse(raw) : {}; 

        return (Array.isArray(parsed) || typeof parsed !== ‘object’) ? {} : parsed; 

    }

    function loadNotes() { 

        if(!currentUser) return {}; 

        const raw = localStorage.getItem(getStorageKey(‘notes’)); 

        let parsed = raw ? JSON.parse(raw) : {}; 

        return (Array.isArray(parsed) || typeof parsed !== ‘object’) ? {} : parsed; 

    }

     

    function saveCheckState(state) { if(!currentUser) return; localStorage.setItem(getStorageKey(‘checks’), JSON.stringify(state)); refreshAllBaskets(); syncToWordPress(); }

    function saveCompletedState(state) { if(!currentUser) return; localStorage.setItem(getStorageKey(‘completed’), JSON.stringify(state)); syncToWordPress(); }

    function saveNotes(notes) { if(!currentUser) return; localStorage.setItem(getStorageKey(‘notes’), JSON.stringify(notes)); renderNotesTab(); syncToWordPress(); }

     

    function saveNoteForDay(dayId,text){ let n=loadNotes(); if(text.trim()===»») delete n[dayId]; else n[dayId]=text; saveNotes(n); }

    function getNoteForDay(dayId){ return loadNotes()[dayId]||»»; }

     

    function refreshAllBaskets(){ if(document.querySelector(‘.tab-btn.active’)?.getAttribute(‘data-tab’)===’checklists’) renderChecklists(); for(let wk in basketsData) refreshWeekBasket(wk); }

     

    function refreshWeekBasket(weekKey){

        let cont=document.getElementById(`basket-content-${weekKey}`);

        if(!cont) return;

        let state=loadCheckState();

        let inner=»;

        for(let[cat,items] of Object.entries(basketsData[weekKey].items)){

            inner+=`<div class=»cat-group»><h4>${cat}</h4><div class=»items-grid»>`;

            items.forEach(it=>{

                let id=`${weekKey}_${it.replace(/[^a-zа-яії0-9]/gi,’_’)}`;

                let checked=(state[weekKey]&&state[weekKey][id])?’checked’:»;

                inner+=`<div class=»check-item»><input type=»checkbox» id=»${id}» data-week=»${weekKey}» data-item=»${it}» ${checked}> <label for=»${id}»>${it}</label></div>`;

            });

            inner+=`</div></div>`;

        }

        cont.innerHTML=inner;

        cont.querySelectorAll(‘.check-item input’).forEach(cb=>{

            cb.addEventListener(‘change’,()=>{

                let st=loadCheckState();

                if(!st[cb.dataset.week]) st[cb.dataset.week]={};

                st[cb.dataset.week][cb.id]=cb.checked;

                saveCheckState(st);

            });

        });

    }

     

    function getFullRecipeHTML(weekTitle, breakfast, lunch, dinner){ 

        return `<div class=»meal-card»><div class=»meal-title»>🥣 СНІДАНОК · ${breakfast}</div><div class=»meal-desc»><strong>Рецепт:</strong> ${getRecipeByTitle(breakfast)}</div></div>

                <div class=»meal-card»><div class=»meal-title»>🥗 ОБІД · ${lunch}</div><div class=»meal-desc»><strong>Рецепт:</strong> ${getRecipeByTitle(lunch)}</div></div>

                <div class=»meal-card»><div class=»meal-title»>🌙 ВЕЧЕРЯ · ${dinner}</div><div class=»meal-desc»><strong>Рецепт:</strong> ${getRecipeByTitle(dinner)}</div></div>`; 

    }

     

    function renderChecklists(){

        let container=document.getElementById(«checklistsPane»);

        let html='<div class=»baskets-container»>’;

        for(let wk in basketsData){

            let w=basketsData[wk];

            html+=`<div class=»basket-week» data-week=»${wk}»><div class=»basket-header»><span>🛒 ${w.title}</span><div class=»basket-actions»><span class=»send-icon» data-week=»${wk}»>📤</span><span class=»pdf-icon» data-week=»${wk}»>📄</span></div></div><div class=»basket-cats» id=»basket-${wk}»></div><button class=»clear-basket» data-week=»${wk}» style=»display:none;»>🗑️ Очистити</button></div>`;

        }

        html+='</div>’;

        container.innerHTML=html;

        for(let wk in basketsData){

            let catsDiv=document.getElementById(`basket-${wk}`);

            if(!catsDiv) continue;

            let inner=»;

            for(let[cat,items] of Object.entries(basketsData[wk].items)){

                inner+=`<div class=»cat-group»><h4>${cat}</h4>`;

                items.forEach(it=>{

                    let id=`${wk}_${it.replace(/[^a-zа-яії0-9]/gi,’_’)}`;

                    let checked=(loadCheckState()[wk]&&loadCheckState()[wk][id])?’checked’:»;

                    inner+=`<div class=»check-item»><input type=»checkbox» id=»${id}» data-week=»${wk}» data-item=»${it}» ${checked}> <label for=»${id}»>${it}</label></div>`;

                });

                inner+=`</div>`;

            }

            catsDiv.innerHTML=inner;

        }

        document.querySelectorAll(‘.check-item input’).forEach(cb=>{

            cb.addEventListener(‘change’,()=>{

                let st=loadCheckState();

                if(!st[cb.dataset.week]) st[cb.dataset.week]={};

                st[cb.dataset.week][cb.id]=cb.checked;

                saveCheckState(st);

            });

        });

        document.querySelectorAll(‘.basket-week’).forEach(wd=>{

            let cats=wd.querySelector(‘.basket-cats’), clearBtn=wd.querySelector(‘.clear-basket’), header=wd.querySelector(‘.basket-header’);

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

                if(e.target.closest(‘.send-icon’)||e.target.closest(‘.pdf-icon’)) return;

                cats.classList.toggle(‘open’);

                clearBtn.style.display=cats.classList.contains(‘open’)?’block’:’none’;

            });

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

                e.stopPropagation();

                let st=loadCheckState();

                delete st[wd.dataset.week];

                saveCheckState(st);

                renderChecklists();

            });

            wd.querySelector(‘.send-icon’)?.addEventListener(‘click’,(e)=>{

                e.stopPropagation();

                sendMessageVia(`Кошик: ${basketsData[wd.dataset.week].title}`,’telegram’);

            });

            wd.querySelector(‘.pdf-icon’)?.addEventListener(‘click’,(e)=>{

                e.stopPropagation();

                generateBasketPDF(wd.dataset.week,basketsData[wd.dataset.week].title);

            });

        });

    }

     

    function generateBasketPDF(weekKey,title){

        if(typeof html2pdf !== ‘function’) return alert(‘Помилка генерації PDF.’);

        let content=`<div style=»font-family:Georgia;»><h1>${title}</h1>`;

        for(let[cat,items] of Object.entries(basketsData[weekKey].items)) content+=`<h2>${cat}</h2><ul>${items.map(i=>`<li>${i}</li>`).join(»)}</ul>`;

        content+=`</div>`;

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

        el.innerHTML=content;

        document.body.appendChild(el);

        html2pdf().from(el).set({margin:0.5,filename:`${title}.pdf`}).save().then(()=>el.remove());

    }

     

    function sendMessageVia(text,platform){

        let plain=text.replace(/<[^>]*>?/gm,»).substring(0,2000);

        if(platform===’whatsapp’) window.open(`https://wa.me/?text=${encodeURIComponent(plain)}`,’_blank’);

        else window.open(`https://t.me/share/url?url=${encodeURIComponent(«Меню Літо 2026»)}&text=${encodeURIComponent(plain)}`,’_blank’);

    }

     

    function renderProgress() {

        let container = document.getElementById(«progressGridContainer»);

        if(!container) return;

        let completed = loadCompletedState();

        let html = ‘<div class=»progress-grid»>’;

        let dayNum = 1;

        // Iterate through calendar to generate days sequentially

        for(let idx in weeksCalendar){

            let week = weeksCalendar[idx];

            week.days.forEach((d, i) => {

                let key = `${idx}_${i}`;

                let isCompleted = completed[key];

                let classes = isCompleted ? ‘progress-day crossed-out’ : ‘progress-day’;

                html += `<div class=»${classes}» onclick=»if(!this.classList.contains(‘crossed-out’)) openDayFromGrid(‘${idx}’, ${i})»>${dayNum}</div>`;

                dayNum++;

            });

        }

        html += ‘</div>’;

        container.innerHTML = html;

    }

     

    window.openDayFromGrid = function(weekIdx, dayIdx) {

        document.querySelector(‘.tab-btn[data-tab=»calendar»]’).click();

        let weekCard = document.querySelector(`.week-card[data-week-idx=»${weekIdx}»]`);

        if(weekCard && weekCard.classList.contains(‘collapsed’)) {

            weekCard.classList.remove(‘collapsed’);

        }

        let dayEl = document.querySelector(`.day-mini[data-week=»${weekIdx}»][data-dayidx=»${dayIdx}»]`);

        if(dayEl) {

            dayEl.scrollIntoView({behavior: ‘smooth’, block: ‘center’});

            setTimeout(() => dayEl.click(), 300); // Activate modal

        }

    }

     

    function renderCalendar(){

        let container=document.getElementById(«calendarPane»);

        let completed=loadCompletedState();

        let html='<div class=»calendar-grid»>’;

        for(let idx in weeksCalendar){

            let week=weeksCalendar[idx];

            let displayTitle=week.title;

            html+=`<div class=»week-card collapsed» data-week-idx=»${idx}»><div class=»week-summary»><span><strong>${displayTitle}</strong> ${week.macro}</span><span class=»week-arrow»>▼</span></div><div class=»days-vertical»>`;

            week.days.forEach((d,i)=>{

                let match=d.dayName.match(/(.+?)\s+(\d+)/);

                let dName=match?match[1]:d.dayName, dNum=match?match[2]:»📅»;

                let isCompleted=completed[`${idx}_${i}`]?’checked’:»;

                let completedClass=isCompleted?’completed-day’:»;

                html+=`<div class=»day-mini ${completedClass}» data-week=»${idx}» data-dayidx=»${i}»><div class=»day-header»><div class=»day-num-icon»>${dNum}</div><div class=»day-name-text»>${dName}</div><input type=»checkbox» class=»day-completed-check» data-week=»${idx}» data-day=»${i}» ${isCompleted}></div><div class=»day-meals-preview»><div>🍳 ${d.breakfast}</div><div>🥗 ${d.lunch}</div><div>🌙 ${d.dinner}</div></div></div>`;

            });

            html+=`</div><div class=»week-basket»><div class=»basket-header-mini» data-week=»${idx}»><span>📦 Корзина тижня (сітка)</span><div class=»basket-actions-mini»><span class=»mini-send» data-week=»${idx}»>📤</span><span class=»mini-pdf» data-week=»${idx}»>📄</span><span class=»mini-toggle»>▶</span></div></div><div class=»basket-content» id=»basket-content-${idx}»></div></div></div>`;

        }

        html+='</div>’;

        container.innerHTML=html;

        for(let idx in basketsData) refreshWeekBasket(idx);

        document.querySelectorAll(‘.week-summary’).forEach(h=>h.addEventListener(‘click’,()=>h.parentElement.classList.toggle(‘collapsed’)));

        document.querySelectorAll(‘.day-mini’).forEach(el=>{

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

                if(e.target.closest(‘.day-completed-check’)) return;

                let week=el.dataset.week, dayIdx=parseInt(el.dataset.dayidx), day=weeksCalendar[week].days[dayIdx];

                let htmlRecipe=getFullRecipeHTML(weeksCalendar[week].title, day.breakfast, day.lunch, day.dinner);

                currentModalDayKey=`${weeksCalendar[week].title} · ${day.dayName}`;

                document.getElementById(«modalTitle»).innerHTML=`📅 ${currentModalDayKey}`;

                document.getElementById(«modalRecipeContent»).innerHTML=htmlRecipe;

                document.getElementById(«dayNoteText»).value=getNoteForDay(currentModalDayKey);

                document.getElementById(«fullRecipeModal»).classList.add(«active»);

            });

        });

        document.querySelectorAll(‘.day-completed-check’).forEach(chk=>{

            chk.addEventListener(‘change’,()=>{

                let week=chk.dataset.week, day=chk.dataset.day, state=loadCompletedState(), key=`${week}_${day}`;

                if(chk.checked) state[key]=true; else delete state[key];

                saveCompletedState(state);

                chk.closest(‘.day-mini’).classList.toggle(‘completed-day’, chk.checked);

                renderProgress(); // Update progress grid immediately!

            });

        });

        document.querySelectorAll(‘.basket-header-mini’).forEach(header=>{

            let weekIdx=header.dataset.week;

            let contentDiv=document.getElementById(`basket-content-${weekIdx}`);

            let toggleSpan=header.querySelector(‘.mini-toggle’);

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

                if(e.target.closest(‘.mini-send’)||e.target.closest(‘.mini-pdf’)) return;

                refreshWeekBasket(weekIdx);

                contentDiv.classList.toggle(‘open’);

                toggleSpan.innerHTML=contentDiv.classList.contains(‘open’)?’▼’:’▶’;

            });

            header.querySelector(‘.mini-send’)?.addEventListener(‘click’,(e)=>{

                e.stopPropagation();

                sendMessageVia(`Кошик тижня ${weekIdx}`,’telegram’);

            });

            header.querySelector(‘.mini-pdf’)?.addEventListener(‘click’,(e)=>{

                e.stopPropagation();

                generateBasketPDF(weekIdx,basketsData[weekIdx].title);

            });

        });

    }

     

    function renderDesserts(){

        let container=document.getElementById(«dessertsPane»);

        let html='<div class=»desserts-grid»>’;

        dessertsList.forEach(d=>{

            html+=`<div class=»dessert-card»><h3>${d.name}</h3><div>🔥 ${d.kcal} ккал</div><button class=»btn-outline dessert-recipe-btn» data-recipe=»${d.recipe.replace(/»/g,’&quot;’)}» data-name=»${d.name}» style=»width:100%;margin-top:8px;»>📖 Рецепт</button></div>`;

        });

        html+='</div>’;

        container.innerHTML=html;

        document.querySelectorAll(‘.dessert-recipe-btn’).forEach(btn=>btn.addEventListener(‘click’,()=>{

            document.getElementById(«modalTitle»).innerHTML=`🍰 ${btn.dataset.name}`;

            document.getElementById(«modalRecipeContent»).innerHTML=`<div class=»meal-card»><div class=»meal-desc»><strong>Приготування:</strong> ${btn.dataset.recipe}</div></div>`;

            

            // Робимо нотатки для десертів активними і робочими

            currentModalDayKey = `Десерт · ${btn.dataset.name}`;

            document.getElementById(«dayNoteText»).value = getNoteForDay(currentModalDayKey);

            

            document.getElementById(«fullRecipeModal»).classList.add(«active»);

        }));

    }

     

    function renderNotesTab(){

        let notes=loadNotes();

        let container=document.getElementById(«userNotesList»);

        if(Object.keys(notes).length) container.innerHTML=Object.entries(notes).map(([day,txt])=>`<div class=»note-card»><h4>📅 ${day.toLowerCase()}</h4><p style=»white-space:pre-wrap;»>${txt}</p></div>`).join(»);

        else container.innerHTML=»<p>У вас поки немає збережених нотаток 📝</p>»;

    }

     

    function updateUI(){

        document.getElementById(«userPhoneDisplay»).innerText=currentUser;

        document.getElementById(«userNameDisplay»).innerText=currentUserName||»Гість»;

        document.getElementById(«profileNameInput»).value=currentUserName||»»;

        document.getElementById(«profilePhoneInput»).value=currentUser;

    }

     

    async function loadFromWordPress() {

        if (!currentUser) return;

        try {

            const response = await fetch(`${WP_API_URL}/get-user-data`, {

                method: ‘POST’,

                headers: { ‘Content-Type’: ‘application/json’ },

                body: JSON.stringify({ user_phone: currentUser })

            });

            if (!response.ok) throw new Error(‘Помилка сервера’);

            const data = await response.json();

            if (data && !data.error) {

                let inChecks = (Array.isArray(data.checks) || !data.checks) ? {} : data.checks;

                let inComp = (Array.isArray(data.completed) || !data.completed) ? {} : data.completed;

                let inNotes = (Array.isArray(data.notes) || !data.notes) ? {} : data.notes;

                localStorage.setItem(getStorageKey(‘checks’), JSON.stringify(inChecks));

                localStorage.setItem(getStorageKey(‘completed’), JSON.stringify(inComp));

                localStorage.setItem(getStorageKey(‘notes’), JSON.stringify(inNotes));

                renderChecklists();

                renderCalendar();

                renderProgress(); // Оновлюємо прогрес після синхронізації

                renderNotesTab();

            }

        } catch (e) { console.warn(‘Не вдалося завантажити дані з WP’, e); }

    }

     

    async function syncToWordPress() {

        if (!currentUser) return;

        const checks = loadCheckState();

        const completed = loadCompletedState();

        const notes = loadNotes();

        try {

            await fetch(`${WP_API_URL}/save-user-data`, {

                method: ‘POST’,

                headers: { ‘Content-Type’: ‘application/json’ },

                body: JSON.stringify({

                    user_phone: currentUser,

                    checks: checks,

                    completed: completed,

                    notes: notes

                })

            });

            console.log(‘✅ Дані успішно збережено в хмару WordPress’);

        } catch (e) { console.warn(‘Помилка синхронізації з базою даних WP’, e); }

    }

     

    function initApp() {

        currentUser = localStorage.getItem(«user_phone»);

        currentUserName = localStorage.getItem(«user_name») || «»;

        if (currentUser) {

            document.getElementById(«authModal»).style.display = «none»;

            document.getElementById(«appRoot»).style.display = «flex»;

            updateUI();

            renderChecklists();

            renderCalendar();

            renderProgress();

            renderDesserts();

            renderNotesTab();

            loadFromWordPress(); 

        } else {

            document.getElementById(«authModal»).style.display = «flex»;

            document.getElementById(«appRoot»).style.display = «none»;

        }

    }

     

    document.getElementById(«authSubmit»).addEventListener(«click», () => {

        let phone = document.getElementById(«authPhone»).value.trim();

        let code = document.getElementById(«authCode»).value.trim();

        

        if (phone && code === «2026») {

            localStorage.setItem(«user_phone», phone);

            if (!localStorage.getItem(«user_name»)) localStorage.setItem(«user_name», «»);

            

            currentUser = phone;

            

            // МИТТЄВА СИНХРОНІЗАЦІЯ: відправляємо профіль у WordPress відразу після входу

            syncToWordPress().then(() => {

                initApp();

            });

            

        } else {

            alert(«Невірний код або номер»);

        }

    });

     

    document.getElementById(«headerProfileBtn»).addEventListener(«click»,()=>document.getElementById(«profileModal»).classList.add(«active»));

    document.getElementById(«closeProfile»).addEventListener(«click»,()=>document.getElementById(«profileModal»).classList.remove(«active»));

    document.getElementById(«saveProfileBtn»).addEventListener(«click»,()=>{

        let newName=document.getElementById(«profileNameInput»).value.trim();

        let newPhone=document.getElementById(«profilePhoneInput»).value.trim();

        if(newPhone && newPhone!==currentUser){

            alert(«Для зміни номера вийдіть та увійдіть заново.»);

            return;

        }

        if(newName){

            localStorage.setItem(«user_name»,newName);

            currentUserName=newName;

            updateUI();

        }

        document.getElementById(«profileModal»).classList.remove(«active»);

    });

    document.getElementById(«logoutBtn»).addEventListener(«click»,()=>{

        localStorage.removeItem(«user_phone»);

        localStorage.removeItem(«user_name»);

        initApp();

        document.getElementById(«profileModal»).classList.remove(«active»);

    });

    document.getElementById(«closeRecipeModal»).addEventListener(«click»,()=>document.getElementById(«fullRecipeModal»).classList.remove(«active»));

    document.getElementById(«sendRecipeWhatsApp»).addEventListener(«click»,()=>sendMessageVia(document.getElementById(«modalRecipeContent»).innerText,’whatsapp’));

    document.getElementById(«sendRecipeTelegram»).addEventListener(«click»,()=>sendMessageVia(document.getElementById(«modalRecipeContent»).innerText,’telegram’));

    document.getElementById(«downloadRecipePDF»).addEventListener(«click»,()=>{

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

        el.innerHTML=document.getElementById(«modalRecipeContent»).innerHTML;

        document.body.appendChild(el);

        if(typeof html2pdf === ‘function’) html2pdf().from(el).set({margin:0.5,filename:»recipe.pdf»}).save().then(()=>el.remove());

        else alert(‘PDF генерація тимчасово недоступна.’);

    });

    document.getElementById(«saveDayNoteBtn»).addEventListener(«click»,()=>{

        if(currentModalDayKey){

            saveNoteForDay(currentModalDayKey, document.getElementById(«dayNoteText»).value);

            let btn=document.getElementById(«saveDayNoteBtn»);

            btn.innerHTML=»✅»;

            setTimeout(()=>btn.innerHTML=»💾»,1500);

        }

    });

    document.querySelectorAll(‘.tab-btn’).forEach(btn=>{

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

            let tabId=btn.getAttribute(‘data-tab’);

            document.querySelectorAll(‘.tab-pane’).forEach(p=>p.classList.remove(‘active-pane’));

            document.querySelectorAll(‘.tab-btn’).forEach(b=>b.classList.remove(‘active’));

            btn.classList.add(‘active’);

            document.getElementById(tabId+’Pane’).classList.add(‘active-pane’);

            

            if(tabId===’notes’) renderNotesTab();

            if(tabId===’checklists’) renderChecklists();

            if(tabId===’calendar’) renderCalendar();

            if(tabId===’progress’) renderProgress();

        });

    });

     

    initApp();

    </script>

    </body>

    </html>

    «`

  • Spirit Hub — партнерский центр обработки и доставки напитков в ТТ

    • ​1. Текущий этап: : инициация,
    • ​2. Рабочее название: Spirit Hub 
    • ​3. Предыдущий этап:
    • ​4. Срок на данный этап: месяц

    ​Суть и триггеры

    • ​5. Мотивация / Причины инициации: получение энергиии для самоутверждения и демонстрации своего потенциала .
    • ​6. О проекте:  Ресурс который позволяет оптимизировать доставку сборку , консолидацию заказов и документов в торговые точки на региональтном и межгородском уровне. 

    ​Реализация

    • ​7. Метки достижения результатов: Структура взаимодействия, Перечень услуг, финмодель
  • @ Ya ## Стратегическая цель: Трансформация бизнеса в сервисную экосистему «Импорт как сервис» (Import-as-a-Service, IaaS)

     

     Перестроить операционную модель Spirit Trade Distribution с формата точечных логистических транзакций на предоставление комплексной импортной инфраструктуры для европейских поставщиков, обеспечивая сквозную предсказуемость через жесткие B2B SLA и максимизацию LTV партнеров.

    ### Подцель 1. Продажа готовой инфраструктуры (Юридический и операционный щит)

    Создание ценностного предложения для европейских производителей (винодельни Испании, пивоварни Бельгии и др.), позволяющего им бесшовно выходить на рынок Казахстана и Центральной Азии без капитальных затрат.

     * **Статус Importer of Record:** Полное взятие на себя юридической, финансовой и таможенной ответственности на территории РК. Поставщик работает на привычных условиях (FCA/EXW), не вникая в локальное законодательство.

     * **Регуляторный комплаенс под ключ:** Автоматизация и администрирование всех специфических процессов: заказ и учет акцизных марок, маркировка на транзитных складах в Европе, разработка контр-этикеток и сертификация ЕАЭС.

     * **Устранение барьеров:** Позиционирование Spirit Trade как «готовой дочерней компании» для зарубежного партнера, минимизирующей его риски при входе в регион.

    ### Подцель 2. Переход на управление LTV через жесткие B2B SLA

    Смещение фокуса с разовых импортных контрактов на долгосрочные сервисные соглашения (Service Level Agreement) с целью стабилизации прогнозируемого дохода и удержания клиентов.

     * **Гарантированный Lead Time:** Фиксация четких таймлайнов на каждом этапе цепи поставок (консолидация в Европе, транзит до Алматы, прохождение таможни) с финансовой ответственностью за отклонения.

     * **Безошибочность оформления (Customs Compliance):** Оцифровка процессов онбординга документов поставщика для полного исключения простоев на границе и штрафов.

     * **Информационная прозрачность:** Предоставление партнерам доступа к движению грузов и статусам документов в режиме реального времени, что снижает нагрузку на клиентский сервис.

    ### Ключевые метрики эффективности (KPI) для мониторинга цели:

    | Метрика | Текущий статус | Целевой показатель |

    |—|—|—|

    | **Доля долгосрочных контрактов (SLA)** | Разовые / рамочные заявки | \ge 80\% от общего объема портфеля |

    | **Средний LTV партнера** | Базовый уровень | Рост на 35\% за счет расширения SKU |

    | **Точность Lead Time (On-Time In-Full)** | Вариативный | \ge 95\% соблюдения сроков доставки |

    | **Время онбординга нового SKU** | Ручной контроль | Сокращение в 2 раза через готовые шаблоны |