Вот HTML-код с новыми реквизитами. Он также растянут на всю ширину экрана и использует текстовые поля для удобного копирования значений:
«`html
| Наименование: | |
| БИН: | |
| ИИК: | |
| Банк: | |
| БИК: |
| Наименование: | |
| БИН: | |
| ИИК: | |
| Банк: | |
| БИК: |
| Сумма: | |
| КНП: | |
| Назначение: |
«`
Техническая документация, интеграции и API. Охватывает всё, что связано с разработкой, настройкой и поддержкой функциональности системы, включая модули Dev & Integrations.
Вот HTML-код с новыми реквизитами. Он также растянут на всю ширину экрана и использует текстовые поля для удобного копирования значений:
«`html
| Наименование: | |
| БИН: | |
| ИИК: | |
| Банк: | |
| БИК: |
| Наименование: | |
| БИН: | |
| ИИК: | |
| Банк: | |
| БИК: |
| Сумма: | |
| КНП: | |
| Назначение: |
«`
| Наименование: | |
| БИН: | |
| ИИК: | |
| Банк: | |
| БИК: |
| Наименование: | |
| БИН: | |
| ИИК: | |
| КБе: | |
| Банк: | |
| БИК: |
| Сумма: | |
| КНП: | |
| Назначение: |
<?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. БАЗА ЗНАНИЙ:
// ==============================================================================
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’)»>×</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’)»>×</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’)»>×</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-ПОИСК: ВЫГРУЗКА АНКЕТ И ШОРТКОД
// ==============================================================================
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 ШОРТКОД ТАБЛИЦЫ
| Фото | Позиция ↕ | Категория ↕ | Цена ↕ | Действия | |
|---|---|---|---|---|---|
| Загрузка данных… | |||||
// ==============================================================================
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 ШОРТКОД РЕДАКТОРА
// ==============================================================================
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();
} В меню под Обновить данные с сервера сделай кнопку сгенерировать все досупные фильтрыю При нажатии вывобится окно с перечнем возмодно=ых фильров и при отметке мнодественный выблор и нажатии ок открывается модальное окно с сггенерроваными фильтрами и возможностью выбратьи применить настройки и при применении запуск фильтрации