2026-09-13 single-teacher.php
This commit is contained in:
parent
78b04cd31c
commit
1efcbb4616
54
eslint.config.js
Normal file
54
eslint.config.js
Normal file
@ -0,0 +1,54 @@
|
||||
// ESLint flat config (ESLint 10.x) — dekart theme JS assets
|
||||
// WordPress-контекст: скрипты темы — обычные <script>, не ES-модули.
|
||||
// jQuery-обёртки, vendor-файлы и админ-скрипты обработаны отдельно.
|
||||
'use strict';
|
||||
|
||||
const js = require('@eslint/js');
|
||||
const globals = require('globals');
|
||||
|
||||
module.exports = [
|
||||
// 1. Глобальный ignore — vendor, node_modules, сборки.
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'wp-content/themes/dekart/assets/js/owl.carousel.min.js',
|
||||
'wp-content/themes/dekart/assets/vendor/**',
|
||||
],
|
||||
},
|
||||
|
||||
// 2. Базовые правила для всех JS.
|
||||
js.configs.recommended,
|
||||
|
||||
// 3. Frontend-скрипты темы.
|
||||
{
|
||||
files: ['wp-content/themes/dekart/assets/js/**/*.js'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'script',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.jquery,
|
||||
// Внешние скрипты: Яндекс.Карты, Метрика, Swiper.
|
||||
ymaps: 'readonly',
|
||||
yaCounter99835482: 'readonly',
|
||||
Swiper: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
},
|
||||
},
|
||||
|
||||
// 4. Админ-скрипты (admin-*.js) — отдельный контекст, без jQuery-глобалов.
|
||||
{
|
||||
files: ['wp-content/themes/dekart/assets/js/admin-*.js'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'script',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
1007
package-lock.json
generated
Normal file
1007
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,5 +2,10 @@
|
||||
"name": "dekart-site",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Dekart private school WordPress theme assets"
|
||||
"description": "Dekart private school WordPress theme assets",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"eslint": "^10.10.0",
|
||||
"globals": "^17.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,4 +14,4 @@ parameters:
|
||||
ignoreErrors:
|
||||
- '#Call to an undefined method Carbon_Fields\\Field\\Field::#'
|
||||
- '#Call to an undefined method Carbon_Fields\\Container\\Container::#'
|
||||
- '#Function (remove_meta_box|submit_button) not found#'
|
||||
- '#Function (remove_meta_box|submit_button|get_current_screen) not found#'
|
||||
|
||||
@ -134,6 +134,11 @@ add_filter( 'plugin_action_links_memcached/object-cache.php', function ( $action
|
||||
return $actions;
|
||||
} );
|
||||
|
||||
// Админ-уведомление «Memcached / PHP-расширение memcache» СКРЫТО навсегда (2026-09-09).
|
||||
// Причина: рабочее кеширование уже настроено, повторное напоминание мешает.
|
||||
// Логика плагина (re-salt, сброс версии кеша, WP-CLI) не тронута.
|
||||
// Вернуть уведомление: раскомментировать блок ниже.
|
||||
/*
|
||||
add_action( 'admin_notices', function () {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
@ -176,6 +181,7 @@ add_action( 'admin_notices', function () {
|
||||
<strong>Не активируйте плагин</strong> — он работает как drop-in.'
|
||||
);
|
||||
} );
|
||||
*/
|
||||
|
||||
// ============================================================
|
||||
// 6. WP-CLI
|
||||
|
||||
978
wp-content/object-cache.php.disabled-20260909
Normal file
978
wp-content/object-cache.php.disabled-20260909
Normal file
@ -0,0 +1,978 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
Plugin Name: Memcached
|
||||
Description: Memcached backend for the WP Object Cache.
|
||||
Version: 4.0.0
|
||||
Plugin URI: https://wordpress.org/plugins/memcached/
|
||||
Author: Ryan Boren, Denis de Bernardy, Matt Martz, Andy Skelton
|
||||
|
||||
Install this file to wp-content/object-cache.php
|
||||
*/
|
||||
|
||||
// Users with setups where multiple installs share a common wp-config.php or $table_prefix
|
||||
// can use this to guarantee uniqueness for the keys generated by this object cache
|
||||
if ( ! defined( 'WP_CACHE_KEY_SALT' ) ) {
|
||||
define( 'WP_CACHE_KEY_SALT', '' );
|
||||
}
|
||||
|
||||
function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
|
||||
global $wp_object_cache;
|
||||
|
||||
return $wp_object_cache->add( $key, $data, $group, $expire );
|
||||
}
|
||||
|
||||
function wp_cache_incr( $key, $n = 1, $group = '' ) {
|
||||
global $wp_object_cache;
|
||||
|
||||
return $wp_object_cache->incr( $key, $n, $group );
|
||||
}
|
||||
|
||||
function wp_cache_decr( $key, $n = 1, $group = '' ) {
|
||||
global $wp_object_cache;
|
||||
|
||||
return $wp_object_cache->decr( $key, $n, $group );
|
||||
}
|
||||
|
||||
function wp_cache_close() {
|
||||
global $wp_object_cache;
|
||||
|
||||
return $wp_object_cache->close();
|
||||
}
|
||||
|
||||
function wp_cache_delete( $key, $group = '' ) {
|
||||
global $wp_object_cache;
|
||||
|
||||
return $wp_object_cache->delete( $key, $group );
|
||||
}
|
||||
|
||||
function wp_cache_flush() {
|
||||
global $wp_object_cache;
|
||||
|
||||
return $wp_object_cache->flush();
|
||||
}
|
||||
|
||||
function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
|
||||
global $wp_object_cache;
|
||||
|
||||
$value = apply_filters( 'pre_wp_cache_get', false, $key, $group, $force );
|
||||
if ( false !== $value ) {
|
||||
$found = true;
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $wp_object_cache->get( $key, $group, $force, $found );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve multiple cache entries
|
||||
*
|
||||
* @param array $groups Array of arrays, of groups and keys to retrieve
|
||||
* @return mixed
|
||||
*/
|
||||
function wp_cache_get_multi( $groups ) {
|
||||
global $wp_object_cache;
|
||||
|
||||
return $wp_object_cache->get_multi( $groups );
|
||||
}
|
||||
|
||||
function wp_cache_init() {
|
||||
global $wp_object_cache;
|
||||
|
||||
$wp_object_cache = new WP_Object_Cache();
|
||||
}
|
||||
|
||||
function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
|
||||
global $wp_object_cache;
|
||||
|
||||
return $wp_object_cache->replace( $key, $data, $group, $expire );
|
||||
}
|
||||
|
||||
function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
|
||||
global $wp_object_cache;
|
||||
|
||||
if ( defined( 'WP_INSTALLING' ) == false ) {
|
||||
return $wp_object_cache->set( $key, $data, $group, $expire );
|
||||
} else {
|
||||
return $wp_object_cache->delete( $key, $group );
|
||||
}
|
||||
}
|
||||
|
||||
function wp_cache_switch_to_blog( $blog_id ) {
|
||||
global $wp_object_cache;
|
||||
|
||||
return $wp_object_cache->switch_to_blog( $blog_id );
|
||||
}
|
||||
|
||||
function wp_cache_add_global_groups( $groups ) {
|
||||
global $wp_object_cache;
|
||||
|
||||
$wp_object_cache->add_global_groups( $groups );
|
||||
}
|
||||
|
||||
function wp_cache_add_non_persistent_groups( $groups ) {
|
||||
global $wp_object_cache;
|
||||
|
||||
$wp_object_cache->add_non_persistent_groups( $groups );
|
||||
}
|
||||
|
||||
class WP_Object_Cache {
|
||||
var $global_groups = array( 'WP_Object_Cache_global' );
|
||||
|
||||
var $no_mc_groups = array();
|
||||
|
||||
var $cache = array();
|
||||
var $mc = array();
|
||||
var $default_mcs = array();
|
||||
var $stats = array();
|
||||
var $group_ops = array();
|
||||
|
||||
var $flush_group = 'WP_Object_Cache';
|
||||
var $global_flush_group = 'WP_Object_Cache_global';
|
||||
var $flush_key = "flush_number_v4";
|
||||
var $old_flush_key = "flush_number";
|
||||
var $flush_number = array();
|
||||
var $global_flush_number = null;
|
||||
|
||||
var $cache_enabled = true;
|
||||
var $default_expiration = 0;
|
||||
var $max_expiration = 2592000; // 30 days
|
||||
|
||||
var $stats_callback = null;
|
||||
|
||||
var $connection_errors = array();
|
||||
|
||||
var $time_total = 0;
|
||||
var $size_total = 0;
|
||||
var $slow_op_microseconds = 0.005; // 5 ms
|
||||
|
||||
function add( $id, $data, $group = 'default', $expire = 0 ) {
|
||||
$key = $this->key( $id, $group );
|
||||
|
||||
if ( is_object( $data ) ) {
|
||||
$data = clone $data;
|
||||
}
|
||||
|
||||
if ( in_array( $group, $this->no_mc_groups ) ) {
|
||||
$this->cache[ $key ] = [
|
||||
'value' => $data,
|
||||
'found' => false,
|
||||
];
|
||||
|
||||
return true;
|
||||
} elseif ( isset( $this->cache[ $key ][ 'value' ] ) && false !== $this->cache[ $key ][ 'value' ] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mc =& $this->get_mc( $group );
|
||||
|
||||
$expire = intval( $expire );
|
||||
if ( 0 === $expire || $expire > $this->max_expiration ) {
|
||||
$expire = $this->default_expiration;
|
||||
}
|
||||
|
||||
$size = $this->get_data_size( $data );
|
||||
$this->timer_start();
|
||||
$result = $mc->add( $key, $data, false, $expire );
|
||||
$elapsed = $this->timer_stop();
|
||||
|
||||
$comment = '';
|
||||
if ( isset( $this->cache[ $key ] ) ) {
|
||||
$comment .= ' [lc already]';
|
||||
}
|
||||
if ( false === $result ) {
|
||||
$comment .= ' [mc already]';
|
||||
}
|
||||
|
||||
$this->group_ops_stats( 'add', $key, $group, $size, $elapsed, $comment );
|
||||
|
||||
if ( false !== $result ) {
|
||||
$this->cache[ $key ] = [
|
||||
'value' => $data,
|
||||
'found' => true,
|
||||
];
|
||||
} else if ( false === $result && true === isset( $this->cache[$key][ 'value' ] ) && false === $this->cache[$key][ 'value' ] ) {
|
||||
/*
|
||||
* Here we unset local cache if remote add failed and local cache value is equal to `false` in order
|
||||
* to update the local cache anytime we get a new information from remote server. This way, the next
|
||||
* cache get will go to remote server and will fetch recent data.
|
||||
*/
|
||||
unset( $this->cache[$key] );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function add_global_groups( $groups ) {
|
||||
if ( ! is_array( $groups ) ) {
|
||||
$groups = (array) $groups;
|
||||
}
|
||||
|
||||
$this->global_groups = array_merge( $this->global_groups, $groups );
|
||||
$this->global_groups = array_unique( $this->global_groups );
|
||||
}
|
||||
|
||||
function add_non_persistent_groups( $groups ) {
|
||||
if ( ! is_array( $groups ) ) {
|
||||
$groups = (array) $groups;
|
||||
}
|
||||
|
||||
$this->no_mc_groups = array_merge( $this->no_mc_groups, $groups );
|
||||
$this->no_mc_groups = array_unique( $this->no_mc_groups );
|
||||
}
|
||||
|
||||
function incr( $id, $n = 1, $group = 'default' ) {
|
||||
$key = $this->key( $id, $group );
|
||||
$mc =& $this->get_mc( $group );
|
||||
|
||||
$incremented = $mc->increment( $key, $n );
|
||||
|
||||
$this->cache[ $key ] = [
|
||||
'value' => $incremented,
|
||||
'found' => false !== $incremented,
|
||||
];
|
||||
|
||||
return $this->cache[ $key ][ 'value' ];
|
||||
}
|
||||
|
||||
function decr( $id, $n = 1, $group = 'default' ) {
|
||||
$key = $this->key( $id, $group );
|
||||
$mc =& $this->get_mc( $group );
|
||||
|
||||
$decremented = $mc->decrement( $key, $n );
|
||||
$this->cache[ $key ] = [
|
||||
'value' => $decremented,
|
||||
'found' => false !== $decremented,
|
||||
];
|
||||
|
||||
return $this->cache[ $key ][ 'value' ];
|
||||
}
|
||||
|
||||
function close() {
|
||||
foreach ( $this->mc as $bucket => $mc ) {
|
||||
$mc->close();
|
||||
}
|
||||
}
|
||||
|
||||
function delete( $id, $group = 'default' ) {
|
||||
$key = $this->key( $id, $group );
|
||||
|
||||
if ( in_array( $group, $this->no_mc_groups ) ) {
|
||||
unset( $this->cache[ $key ] );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$mc =& $this->get_mc( $group );
|
||||
|
||||
$this->timer_start();
|
||||
$result = $mc->delete( $key );
|
||||
$elapsed = $this->timer_stop();
|
||||
|
||||
$this->group_ops_stats( 'delete', $key, $group, null, $elapsed );
|
||||
|
||||
if ( false !== $result ) {
|
||||
unset( $this->cache[ $key ] );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Gets number from all default servers, replicating if needed
|
||||
function get_max_flush_number( $group ) {
|
||||
$key = $this->key( $this->flush_key, $group );
|
||||
|
||||
$values = array();
|
||||
$size = 19; // size of microsecond timestamp serialized
|
||||
foreach ( $this->default_mcs as $i => $mc ) {
|
||||
$flags = false;
|
||||
$this->timer_start();
|
||||
$values[ $i ] = $mc->get( $key, $flags );
|
||||
$elapsed = $this->timer_stop();
|
||||
|
||||
if ( empty( $values[ $i ] ) ) {
|
||||
$this->group_ops_stats( 'get_flush_number', $key, $group, null, $elapsed, 'not_in_memcache' );
|
||||
} else {
|
||||
$this->group_ops_stats( 'get_flush_number', $key, $group, $size, $elapsed, 'memcache' );
|
||||
}
|
||||
}
|
||||
|
||||
$max = max( $values );
|
||||
|
||||
if ( ! $max > 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Replicate to servers not having the max.
|
||||
$expire = 0;
|
||||
foreach ( $this->default_mcs as $i => $mc ) {
|
||||
if ( $values[ $i ] < $max ) {
|
||||
$this->timer_start();
|
||||
$mc->set( $key, $max, false, $expire );
|
||||
$elapsed = $this->timer_stop();
|
||||
$this->group_ops_stats( 'set_flush_number', $key, $group, $size, $elapsed, 'replication_repair' );
|
||||
}
|
||||
}
|
||||
|
||||
return $max;
|
||||
}
|
||||
|
||||
function set_flush_number( $value, $group ) {
|
||||
$key = $this->key( $this->flush_key, $group );
|
||||
$expire = 0;
|
||||
$size = 19;
|
||||
foreach ( $this->default_mcs as $i => $mc ) {
|
||||
$this->timer_start();
|
||||
$mc->set( $key, $value, false, $expire );
|
||||
$elapsed = $this->timer_stop();
|
||||
$this->group_ops_stats( 'set_flush_number', $key, $group, $size, $elapsed, 'replication' );
|
||||
}
|
||||
}
|
||||
|
||||
function get_flush_number( $group ) {
|
||||
$flush_number = $this->get_max_flush_number( $group );
|
||||
// What if there is no v4 flush number?
|
||||
if ( empty( $flush_number ) ) {
|
||||
// Look for the v3 flush number key.
|
||||
$flush_number = intval( $this->get( $this->old_flush_key, $group ) );
|
||||
if ( !empty( $flush_number ) ) {
|
||||
// Found v3 flush number. Upgrade to v4 with replication.
|
||||
$this->set_flush_number( $flush_number, $group );
|
||||
// Delete v3 key so we can't later restore it and find stale keys.
|
||||
} else {
|
||||
// No flush number found anywhere. Make a new one. This flushes the cache.
|
||||
$flush_number = $this->new_flush_number();
|
||||
$this->set_flush_number( $flush_number, $group );
|
||||
}
|
||||
}
|
||||
|
||||
return $flush_number;
|
||||
}
|
||||
|
||||
function get_global_flush_number() {
|
||||
if ( ! isset( $this->global_flush_number ) ) {
|
||||
$this->global_flush_number = $this->get_flush_number( $this->global_flush_group );
|
||||
}
|
||||
return $this->global_flush_number;
|
||||
}
|
||||
|
||||
function get_blog_flush_number() {
|
||||
if ( ! isset( $this->flush_number[ $this->blog_prefix ] ) ) {
|
||||
$this->flush_number[ $this->blog_prefix ] = $this->get_flush_number( $this->flush_group );
|
||||
}
|
||||
return $this->flush_number[ $this->blog_prefix ];
|
||||
}
|
||||
|
||||
function flush() {
|
||||
// Do not use the memcached flush method. It acts on an
|
||||
// entire memcached server, affecting all sites.
|
||||
// Flush is also unusable in some setups, e.g. twemproxy.
|
||||
// Instead, rotate the key prefix for the current site.
|
||||
// Global keys are rotated when flushing on any network's
|
||||
// main site.
|
||||
$this->cache = array();
|
||||
|
||||
$flush_number = $this->new_flush_number();
|
||||
|
||||
$this->rotate_site_keys( $flush_number );
|
||||
|
||||
if ( is_main_site() ) {
|
||||
$this->rotate_global_keys( $flush_number );
|
||||
}
|
||||
}
|
||||
|
||||
function rotate_site_keys( $flush_number = null ) {
|
||||
if ( is_null( $flush_number ) ) {
|
||||
$flush_number = $this->new_flush_number();
|
||||
}
|
||||
|
||||
$this->set_flush_number( $flush_number, $this->flush_group );
|
||||
|
||||
$this->flush_number[ $this->blog_prefix ] = $flush_number;
|
||||
}
|
||||
|
||||
function rotate_global_keys( $flush_number = null ) {
|
||||
if ( is_null( $flush_number ) ) {
|
||||
$flush_number = $this->new_flush_number();
|
||||
}
|
||||
|
||||
$this->set_flush_number( $flush_number, $this->global_flush_group );
|
||||
|
||||
$this->global_flush_number = $flush_number;
|
||||
}
|
||||
|
||||
function new_flush_number() {
|
||||
return intval( microtime( true ) * 1e6 );
|
||||
}
|
||||
|
||||
function get( $id, $group = 'default', $force = false, &$found = null ) {
|
||||
$key = $this->key( $id, $group );
|
||||
$mc =& $this->get_mc( $group );
|
||||
$found = true;
|
||||
|
||||
if ( isset( $this->cache[ $key ] ) && ( ! $force || in_array( $group, $this->no_mc_groups ) ) ) {
|
||||
if ( isset( $this->cache[ $key ][ 'value' ] ) && is_object( $this->cache[ $key ][ 'value' ] ) ) {
|
||||
$value = clone $this->cache[ $key ][ 'value' ];
|
||||
} else {
|
||||
$value = $this->cache[ $key ][ 'value' ];
|
||||
}
|
||||
$found = $this->cache[ $key ][ 'found' ];
|
||||
|
||||
$this->group_ops_stats( 'get_local', $key, $group, null, null, 'local' );
|
||||
} else if ( in_array( $group, $this->no_mc_groups ) ) {
|
||||
$this->cache[ $key ] = [
|
||||
'value' => $value = false,
|
||||
'found' => false,
|
||||
];
|
||||
|
||||
$found = false;
|
||||
|
||||
$this->group_ops_stats( 'get_local', $key, $group, null, null, 'not_in_local' );
|
||||
} else {
|
||||
$flags = false;
|
||||
$this->timer_start();
|
||||
$value = $mc->get( $key, $flags );
|
||||
$elapsed = $this->timer_stop();
|
||||
|
||||
// Value will be unchanged if the key doesn't exist.
|
||||
if ( false === $flags ) {
|
||||
$found = false;
|
||||
$value = false;
|
||||
}
|
||||
|
||||
$this->cache[ $key ] = [
|
||||
'value' => $value,
|
||||
'found' => $found,
|
||||
];
|
||||
|
||||
if ( is_null( $value ) || $value === false ) {
|
||||
$this->group_ops_stats( 'get', $key, $group, null, $elapsed, 'not_in_memcache' );
|
||||
} else if ( 'checkthedatabaseplease' === $value ) {
|
||||
$this->group_ops_stats( 'get', $key, $group, null, $elapsed, 'checkthedatabaseplease' );
|
||||
} else {
|
||||
$size = $this->get_data_size( $value );
|
||||
$this->group_ops_stats( 'get', $key, $group, $size, $elapsed, 'memcache' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( 'checkthedatabaseplease' === $value ) {
|
||||
unset( $this->cache[ $key ] );
|
||||
|
||||
$found = false;
|
||||
$value = false;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function get_multi( $groups ) {
|
||||
/*
|
||||
format: $get['group-name'] = array( 'key1', 'key2' );
|
||||
*/
|
||||
$return = array();
|
||||
$return_cache = array(
|
||||
'value' => false,
|
||||
'found' => false,
|
||||
);
|
||||
|
||||
foreach ( $groups as $group => $ids ) {
|
||||
$mc =& $this->get_mc( $group );
|
||||
$keys = array();
|
||||
$this->timer_start();
|
||||
|
||||
foreach ( $ids as $id ) {
|
||||
$key = $this->key( $id, $group );
|
||||
$keys[] = $key;
|
||||
|
||||
if ( isset( $this->cache[ $key ] ) ) {
|
||||
if ( is_object( $this->cache[ $key ][ 'value'] ) ) {
|
||||
$return[ $key ] = clone $this->cache[ $key ][ 'value'];
|
||||
$return_cache[ $key ] = [
|
||||
'value' => clone $this->cache[ $key ][ 'value'],
|
||||
'found' => $this->cache[ $key ][ 'found'],
|
||||
];
|
||||
} else {
|
||||
$return[ $key ] = $this->cache[ $key ][ 'value'];
|
||||
$return_cache[ $key ] = [
|
||||
'value' => $this->cache[ $key ][ 'value' ],
|
||||
'found' => $this->cache[ $key ][ 'found' ],
|
||||
];
|
||||
}
|
||||
|
||||
continue;
|
||||
} else if ( in_array( $group, $this->no_mc_groups ) ) {
|
||||
$return[ $key ] = false;
|
||||
$return_cache[ $key ] = [
|
||||
'value' => false,
|
||||
'found' => false,
|
||||
];
|
||||
|
||||
continue;
|
||||
} else {
|
||||
$fresh_get = $mc->get( $key );
|
||||
$return[ $key ] = $fresh_get;
|
||||
$return_cache[ $key ] = [
|
||||
'value' => $fresh_get,
|
||||
'found' => false !== $fresh_get,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$elapsed = $this->timer_stop();
|
||||
$this->group_ops_stats( 'get_multi', $keys, $group, null, $elapsed );
|
||||
}
|
||||
|
||||
$this->cache = array_merge( $this->cache, $return_cache );
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
function flush_prefix( $group ) {
|
||||
if ( $group === $this->flush_group || $group === $this->global_flush_group ) {
|
||||
// Never flush the flush numbers.
|
||||
$number = '_';
|
||||
} elseif ( false !== array_search( $group, $this->global_groups ) ) {
|
||||
$number = $this->get_global_flush_number();
|
||||
} else {
|
||||
$number = $this->get_blog_flush_number();
|
||||
}
|
||||
return $number . ':';
|
||||
}
|
||||
|
||||
function key( $key, $group ) {
|
||||
if ( empty( $group ) ) {
|
||||
$group = 'default';
|
||||
}
|
||||
|
||||
$prefix = $this->key_salt;
|
||||
|
||||
$prefix .= $this->flush_prefix( $group );
|
||||
|
||||
if ( false !== array_search( $group, $this->global_groups ) ) {
|
||||
$prefix .= $this->global_prefix;
|
||||
} else {
|
||||
$prefix .= $this->blog_prefix;
|
||||
}
|
||||
|
||||
return preg_replace( '/\s+/', '', "$prefix:$group:$key" );
|
||||
}
|
||||
|
||||
function replace( $id, $data, $group = 'default', $expire = 0 ) {
|
||||
$key = $this->key( $id, $group );
|
||||
$expire = intval( $expire );
|
||||
if ( 0 === $expire || $expire > $this->max_expiration ) {
|
||||
$expire = $this->default_expiration;
|
||||
}
|
||||
$mc =& $this->get_mc( $group );
|
||||
|
||||
if ( is_object( $data ) ) {
|
||||
$data = clone $data;
|
||||
}
|
||||
|
||||
$size = $this->get_data_size( $data );
|
||||
$this->timer_start();
|
||||
$result = $mc->replace( $key, $data, false, $expire );
|
||||
$elapsed = $this->timer_stop();
|
||||
$this->group_ops_stats( 'replace', $key, $group, $size, $elapsed );
|
||||
|
||||
if ( false !== $result ) {
|
||||
$this->cache[ $key ] = [
|
||||
'value' => $data,
|
||||
'found' => true,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function set( $id, $data, $group = 'default', $expire = 0 ) {
|
||||
$key = $this->key( $id, $group );
|
||||
|
||||
if ( isset( $this->cache[ $key ] ) && ( 'checkthedatabaseplease' === $this->cache[ $key ][ 'value' ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( is_object( $data ) ) {
|
||||
$data = clone $data;
|
||||
}
|
||||
|
||||
$this->cache[ $key ] = [
|
||||
'value' => $data,
|
||||
'found' => false, // Set to false as not technically found in memcache at this point.
|
||||
];
|
||||
|
||||
if ( in_array( $group, $this->no_mc_groups ) ) {
|
||||
$this->group_ops_stats( 'set_local', $key, $group, null, null );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$expire = intval( $expire );
|
||||
if ( 0 === $expire || $expire > $this->max_expiration ) {
|
||||
$expire = $this->default_expiration;
|
||||
}
|
||||
|
||||
$mc =& $this->get_mc( $group );
|
||||
|
||||
$size = $this->get_data_size( $data );
|
||||
$this->timer_start();
|
||||
$result = $mc->set( $key, $data, false, $expire );
|
||||
$elapsed = $this->timer_stop();
|
||||
$this->group_ops_stats( 'set', $key, $group, $size, $elapsed );
|
||||
|
||||
// Update the found cache value with the result of the set in memcache.
|
||||
$this->cache[ $key ][ 'found' ] = $result;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function switch_to_blog( $blog_id ) {
|
||||
global $table_prefix;
|
||||
|
||||
$blog_id = (int) $blog_id;
|
||||
|
||||
$this->blog_prefix = ( is_multisite() ? $blog_id : $table_prefix );
|
||||
}
|
||||
|
||||
function colorize_debug_line( $line, $trailing_html = '' ) {
|
||||
$colors = array(
|
||||
'get' => 'green',
|
||||
'get_local' => 'lightgreen',
|
||||
'get_multi' => 'fuchsia',
|
||||
'set' => 'purple',
|
||||
'set_local' => 'orchid',
|
||||
'add' => 'blue',
|
||||
'delete' => 'red',
|
||||
'delete_local' => 'tomato',
|
||||
'slow-ops' => 'crimson',
|
||||
);
|
||||
|
||||
$cmd = substr( $line, 0, strpos( $line, ' ' ) );
|
||||
|
||||
// Start off with a neutral default color...
|
||||
$color_for_cmd = 'brown';
|
||||
// And if the cmd has a specific color, use that instead
|
||||
if ( isset( $colors[ $cmd ] ) ) {
|
||||
$color_for_cmd = $colors[ $cmd ];
|
||||
}
|
||||
|
||||
$cmd2 = "<span style='color:" . esc_attr( $color_for_cmd ) . "; font-weight: bold;'>" . esc_html( $cmd ) . "</span>";
|
||||
|
||||
return $cmd2 . esc_html( substr( $line, strlen( $cmd ) ) ) . "$trailing_html\n";
|
||||
}
|
||||
|
||||
function js_toggle() {
|
||||
echo "
|
||||
<script>
|
||||
function memcachedToggleVisibility( id, hidePrefix ) {
|
||||
var element = document.getElementById( id );
|
||||
if ( ! element ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide all element with `hidePrefix` if given. Used to display only one element at a time.
|
||||
if ( hidePrefix ) {
|
||||
var groupStats = document.querySelectorAll( '[id^=\"' + hidePrefix + '\"]' );
|
||||
groupStats.forEach(
|
||||
function ( element ) {
|
||||
element.style.display = 'none';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Toggle the one we clicked.
|
||||
if ( 'none' === element.style.display ) {
|
||||
element.style.display = 'block';
|
||||
} else {
|
||||
element.style.display = 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
";
|
||||
}
|
||||
|
||||
function stats() {
|
||||
$this->js_toggle();
|
||||
|
||||
echo '<h2><span>Total memcache query time:</span>' . number_format_i18n( sprintf( '%0.1f', $this->time_total * 1000 ), 1 ) . ' ms</h2>';
|
||||
echo "\n";
|
||||
echo '<h2><span>Total memcache size:</span>' . esc_html( size_format( $this->size_total, 2 ) ) . '</h2>';
|
||||
echo "\n";
|
||||
|
||||
foreach ( $this->stats as $stat => $n ) {
|
||||
if ( empty( $n ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
echo '<h2>';
|
||||
echo $this->colorize_debug_line( "$stat $n" );
|
||||
echo '</h2>';
|
||||
}
|
||||
|
||||
echo "<ul class='debug-menu-links' style='clear:left;font-size:14px;'>\n";
|
||||
$groups = array_keys( $this->group_ops );
|
||||
usort( $groups, 'strnatcasecmp' );
|
||||
|
||||
$active_group = $groups[0];
|
||||
// Always show `slow-ops` first
|
||||
if ( in_array( 'slow-ops', $groups ) ) {
|
||||
$slow_ops_key = array_search( 'slow-ops', $groups );
|
||||
$slow_ops = $groups[ $slow_ops_key ];
|
||||
unset( $groups[ $slow_ops_key ] );
|
||||
array_unshift( $groups, $slow_ops );
|
||||
$active_group = 'slow-ops';
|
||||
}
|
||||
|
||||
$total_ops = 0;
|
||||
$group_titles = array();
|
||||
foreach ( $groups as $group ) {
|
||||
$group_name = $group;
|
||||
if ( empty( $group_name ) ) {
|
||||
$group_name = 'default';
|
||||
}
|
||||
$group_ops = count( $this->group_ops[ $group ] );
|
||||
$group_size = size_format( array_sum( array_map( function ( $op ) { return $op[2]; }, $this->group_ops[ $group ] ) ), 2 );
|
||||
$group_time = number_format_i18n( sprintf( '%0.1f', array_sum( array_map( function ( $op ) { return $op[3]; }, $this->group_ops[ $group ] ) ) * 1000 ), 1 );
|
||||
$total_ops += $group_ops;
|
||||
$group_title = "{$group_name} [$group_ops][$group_size][{$group_time} ms]";
|
||||
$group_titles[ $group ] = $group_title;
|
||||
echo "\t<li><a href='#' onclick='memcachedToggleVisibility( \"object-cache-stats-menu-target-" . esc_js( $group_name ) . "\", \"object-cache-stats-menu-target-\" );'>" . esc_html( $group_title ) . "</a></li>\n";
|
||||
}
|
||||
echo "</ul>\n";
|
||||
|
||||
echo "<div id='object-cache-stats-menu-targets'>\n";
|
||||
foreach ( $groups as $group ) {
|
||||
$group_name = $group;
|
||||
if ( empty( $group_name ) ) {
|
||||
$group_name = 'default';
|
||||
}
|
||||
$current = $active_group == $group ? 'style="display: block"' : 'style="display: none"';
|
||||
echo "<div id='object-cache-stats-menu-target-" . esc_attr( $group_name ) . "' class='object-cache-stats-menu-target' $current>\n";
|
||||
echo '<h3>' . esc_html( $group_titles[ $group ] ) . '</h3>' . "\n";
|
||||
echo "<pre>\n";
|
||||
foreach ( $this->group_ops[ $group ] as $index => $arr ) {
|
||||
printf( '%3d ', $index );
|
||||
echo $this->get_group_ops_line( $index, $arr );
|
||||
}
|
||||
echo "</pre>\n";
|
||||
echo "</div>";
|
||||
}
|
||||
|
||||
echo "</div>";
|
||||
}
|
||||
|
||||
function get_group_ops_line( $index, $arr ) {
|
||||
// operation
|
||||
$line = "{$arr[0]} ";
|
||||
|
||||
// key
|
||||
$json_encoded_key = json_encode( $arr[1] );
|
||||
$line .= $json_encoded_key . " ";
|
||||
|
||||
// comment
|
||||
if ( ! empty( $arr[4] ) ) {
|
||||
$line .= "{$arr[4]} ";
|
||||
}
|
||||
|
||||
// size
|
||||
if ( isset( $arr[2] ) ) {
|
||||
$line .= '(' . size_format( $arr[2], 2 ) . ') ';
|
||||
}
|
||||
|
||||
// time
|
||||
if ( isset( $arr[3] ) ) {
|
||||
$line .= '(' . number_format_i18n( sprintf( '%0.1f', $arr[3] * 1000 ), 1 ) . ' ms)';
|
||||
}
|
||||
|
||||
// backtrace
|
||||
$bt_link = '';
|
||||
if ( isset( $arr[6] ) ) {
|
||||
$key_hash = md5( $index . $json_encoded_key );
|
||||
$bt_link = " <small><a href='#' onclick='memcachedToggleVisibility( \"object-cache-stats-debug-$key_hash\" );'>Toggle Backtrace</a></small>";
|
||||
$bt_link .= "<pre id='object-cache-stats-debug-$key_hash' style='display:none'>" . esc_html( $arr[6] ) . "</pre>";
|
||||
}
|
||||
|
||||
return $this->colorize_debug_line( $line, $bt_link );
|
||||
}
|
||||
|
||||
function &get_mc( $group ) {
|
||||
if ( isset( $this->mc[ $group ] ) ) {
|
||||
return $this->mc[ $group ];
|
||||
}
|
||||
|
||||
return $this->mc['default'];
|
||||
}
|
||||
|
||||
function failure_callback( $host, $port ) {
|
||||
$this->connection_errors[] = array(
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
);
|
||||
}
|
||||
|
||||
function salt_keys( $key_salt ) {
|
||||
if ( strlen( $key_salt ) ) {
|
||||
$this->key_salt = $key_salt . ':';
|
||||
} else {
|
||||
$this->key_salt = '';
|
||||
}
|
||||
}
|
||||
|
||||
function __construct() {
|
||||
$this->stats = array(
|
||||
'get' => 0,
|
||||
'get_local' => 0,
|
||||
'get_multi' => 0,
|
||||
'set' => 0,
|
||||
'set_local' => 0,
|
||||
'add' => 0,
|
||||
'delete' => 0,
|
||||
'delete_local' => 0,
|
||||
'slow-ops' => 0,
|
||||
);
|
||||
|
||||
global $memcached_servers;
|
||||
|
||||
if ( isset( $memcached_servers ) ) {
|
||||
$buckets = $memcached_servers;
|
||||
} else {
|
||||
$buckets = array( '127.0.0.1:11211' );
|
||||
}
|
||||
|
||||
reset( $buckets );
|
||||
|
||||
if ( is_int( key( $buckets ) ) ) {
|
||||
$buckets = array( 'default' => $buckets );
|
||||
}
|
||||
|
||||
foreach ( $buckets as $bucket => $servers ) {
|
||||
$this->mc[ $bucket ] = new Memcache();
|
||||
|
||||
foreach ( $servers as $i => $server ) {
|
||||
if ( 'unix://' == substr( $server, 0, 7 ) ) {
|
||||
$node = $server;
|
||||
$port = 0;
|
||||
} else {
|
||||
list ( $node, $port ) = explode( ':', $server );
|
||||
|
||||
if ( ! $port ) {
|
||||
$port = ini_get( 'memcache.default_port' );
|
||||
}
|
||||
|
||||
$port = intval( $port );
|
||||
|
||||
if ( ! $port ) {
|
||||
$port = 11211;
|
||||
}
|
||||
}
|
||||
|
||||
$this->mc[ $bucket ]->addServer( $node, $port, true, 1, 1, 15, true, array( $this, 'failure_callback' ) );
|
||||
$this->mc[ $bucket ]->setCompressThreshold( 20000, 0.2 );
|
||||
|
||||
// Prepare individual connections to servers in default bucket for flush_number redundancy
|
||||
if ( 'default' === $bucket ) {
|
||||
$this->default_mcs[ $i ] = new Memcache();
|
||||
$this->default_mcs[ $i ]->addServer( $node, $port, true, 1, 1, 15, true, array( $this, 'failure_callback' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
global $blog_id, $table_prefix;
|
||||
|
||||
$this->global_prefix = '';
|
||||
$this->blog_prefix = '';
|
||||
|
||||
if ( function_exists( 'is_multisite' ) ) {
|
||||
$this->global_prefix = ( is_multisite() || defined( 'CUSTOM_USER_TABLE' ) && defined( 'CUSTOM_USER_META_TABLE' ) ) ? '' : $table_prefix;
|
||||
$this->blog_prefix = ( is_multisite() ? $blog_id : $table_prefix );
|
||||
}
|
||||
|
||||
$this->salt_keys( WP_CACHE_KEY_SALT );
|
||||
|
||||
$this->cache_hits =& $this->stats['get'];
|
||||
$this->cache_misses =& $this->stats['add'];
|
||||
}
|
||||
|
||||
function increment_stat( $field, $num = 1 ) {
|
||||
if ( ! isset( $this->stats[ $field ] ) ) {
|
||||
$this->stats[ $field ] = $num;
|
||||
} else {
|
||||
$this->stats[ $field ] += $num;
|
||||
}
|
||||
}
|
||||
|
||||
function group_ops_stats( $op, $keys, $group, $size, $time, $comment = '' ) {
|
||||
$this->increment_stat( $op );
|
||||
|
||||
// we have no use of the local ops details for now
|
||||
if ( strpos( $op, '_local' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->size_total += $size;
|
||||
|
||||
$keys = $this->strip_memcached_keys( $keys );
|
||||
|
||||
if ( $time > $this->slow_op_microseconds && 'get_multi' !== $op ) {
|
||||
$this->increment_stat( 'slow-ops' );
|
||||
$backtrace = null;
|
||||
if ( function_exists( 'wp_debug_backtrace_summary' ) ) {
|
||||
$backtrace = wp_debug_backtrace_summary();
|
||||
}
|
||||
$this->group_ops['slow-ops'][] = array( $op, $keys, $size, $time, $comment, $group, $backtrace );
|
||||
}
|
||||
|
||||
$this->group_ops[ $group ][] = array( $op, $keys, $size, $time, $comment );
|
||||
}
|
||||
|
||||
/**
|
||||
* Key format: key_salt:flush_number:table_prefix:key_name
|
||||
*
|
||||
* We want to strip the `key_salt:flush_number` part to not leak the memcached keys.
|
||||
* If `key_salt` is set we strip `'key_salt:flush_number`, otherwise just strip the `flush_number` part.
|
||||
*/
|
||||
function strip_memcached_keys( $keys ) {
|
||||
if ( ! is_array( $keys ) ) {
|
||||
$keys = [ $keys ];
|
||||
}
|
||||
|
||||
foreach ( $keys as $key => $value ) {
|
||||
$offset = 0;
|
||||
if ( ! empty( $this->key_salt ) ) {
|
||||
$offset = strpos( $value, ':' ) + 1;
|
||||
}
|
||||
|
||||
$start = strpos( $value, ':', $offset );
|
||||
$keys[ $key ] = substr( $value, $start + 1 );
|
||||
}
|
||||
|
||||
if ( 1 === count( $keys ) ) {
|
||||
return $keys[0];
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
function timer_start() {
|
||||
$this->time_start = microtime( true );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function timer_stop() {
|
||||
$time_total = microtime( true ) - $this->time_start;
|
||||
$this->time_total += $time_total;
|
||||
|
||||
return $time_total;
|
||||
}
|
||||
|
||||
function get_data_size( $data ) {
|
||||
if ( is_string( $data ) ) {
|
||||
return strlen( $data );
|
||||
}
|
||||
|
||||
$serialized = serialize( $data );
|
||||
|
||||
return strlen( $serialized );
|
||||
}
|
||||
}
|
||||
120
wp-content/themes/dekart/assets/css/admin-branch-tabs.css
Normal file
120
wp-content/themes/dekart/assets/css/admin-branch-tabs.css
Normal file
@ -0,0 +1,120 @@
|
||||
/* ════════════════════════════════════════════
|
||||
* Админ: Настройки филиала — вертикальные вкладки
|
||||
* (колонка слева, контент справа; тёмная тема CF сохранена)
|
||||
* ════════════════════════════════════════════ */
|
||||
|
||||
.dekart-branch-tabs {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
max-width: 1100px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dekart-branch-tabs__nav {
|
||||
flex: 0 0 260px;
|
||||
position: sticky;
|
||||
top: 46px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid var(--cf-border, #c3c4c7);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dekart-branch-tabs__nav ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.dekart-branch-tabs__nav button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 9px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
font-weight: 600;
|
||||
color: var(--cf-text-muted, #646970);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.dekart-branch-tabs__nav button:hover {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
color: var(--cf-text, #1d2327);
|
||||
}
|
||||
|
||||
body.branch-settings_page_branch-settings .dekart-branch-tabs__nav button:hover,
|
||||
body.toplevel_page_branch-settings .dekart-branch-tabs__nav button:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dekart-branch-tabs__nav button.is-active {
|
||||
background: var(--wp-admin-theme-color, #2271b1);
|
||||
color: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.dekart-branch-tabs__content {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Панели-секции: одна видна, остальные скрыты. */
|
||||
.dekart-branch-tabs__content .branch-section {
|
||||
display: none;
|
||||
max-width: none;
|
||||
}
|
||||
.dekart-branch-tabs__content .branch-section.is-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Внутри панели заголовок-аккордеон не нужен — он стал вкладкой слева. */
|
||||
.dekart-branch-tabs__content .branch-section h2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ГЕО (первая секция): 14 полей — в 2 столбца, чтобы блок был компактным. */
|
||||
.dekart-branch-tabs__content .branch-section:first-child .branch-section-body {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
column-gap: 32px;
|
||||
align-items: start;
|
||||
}
|
||||
.dekart-branch-tabs__content .branch-section:first-child .branch-field:last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* Узкие экраны: колонка сверху и сворачивается в список. */
|
||||
@media (max-width: 782px) {
|
||||
.dekart-branch-tabs {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.dekart-branch-tabs__nav {
|
||||
flex: none;
|
||||
position: static;
|
||||
width: 100%;
|
||||
}
|
||||
.dekart-branch-tabs__nav ul {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.dekart-branch-tabs__nav button {
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* ГЕО на мобильной админке — снова один столбец. */
|
||||
.dekart-branch-tabs__content .branch-section:first-child .branch-section-body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
36
wp-content/themes/dekart/assets/css/admin-teacher-tabs.css
Normal file
36
wp-content/themes/dekart/assets/css/admin-teacher-tabs.css
Normal file
@ -0,0 +1,36 @@
|
||||
/* ════════════════════════════════════════════
|
||||
* Админ: вертикальные вкладки CF (преподаватель)
|
||||
* Только экран редактирования teacher. Нативный layout
|
||||
* CF 'tabbed-vertical' (колонка 300px слева) + мелкие правки.
|
||||
* ════════════════════════════════════════════ */
|
||||
|
||||
body.post-type-teacher .cf-container__tabs--tabbed-vertical .cf-container__tabs-list .cf-container__tabs-item--current {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
body.post-type-teacher .cf-container__tabs--tabbed-vertical .cf-container__tabs-list .cf-container__tabs-item--current button {
|
||||
font-weight: 700;
|
||||
color: #1d2327;
|
||||
box-shadow: inset 3px 0 0 var(--wp-admin-theme-color, #2271b1);
|
||||
}
|
||||
|
||||
body.post-type-teacher .cf-container__fields {
|
||||
padding: 16px 18px 20px;
|
||||
}
|
||||
|
||||
body.post-type-teacher.dekart-cf-tabs-broken .postbox {
|
||||
outline: 2px dashed #d63638;
|
||||
outline-offset: -6px;
|
||||
}
|
||||
|
||||
/* На узких экранах (мобильная админка) колонка не помещается — стек. */
|
||||
@media (max-width: 782px) {
|
||||
body.post-type-teacher .cf-container--tabbed-vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
body.post-type-teacher .cf-container__tabs--tabbed-vertical {
|
||||
width: 100%;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #e2e4e7;
|
||||
}
|
||||
}
|
||||
@ -46,9 +46,6 @@
|
||||
.stories {
|
||||
background-image: radial-gradient(circle 180px at 0% 30%, rgba(37,99,235,0.05) 0%, transparent 70%);
|
||||
}
|
||||
.reviews-front-section {
|
||||
background-image: radial-gradient(circle 250px at 80% 0%, rgba(37,99,235,0.07) 0%, transparent 70%);
|
||||
}
|
||||
.contacts {
|
||||
background-image: radial-gradient(circle 200px at 50% 100%, rgba(37,99,235,0.05) 0%, transparent 70%);
|
||||
}
|
||||
@ -965,182 +962,6 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* ---- Секция отзывов на главной (reviews-front) ---- */
|
||||
.reviews-front-section {
|
||||
position: relative;
|
||||
padding: 48px 0;
|
||||
}
|
||||
.reviews-front-section .section-header--centered {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.reviews-front__grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.reviews-front__card {
|
||||
width: 100%;
|
||||
background: linear-gradient(90deg, #BF568E, #006DB7);
|
||||
border-radius: 21px;
|
||||
padding: 2px;
|
||||
}
|
||||
.reviews-front__card-inner {
|
||||
padding: 24px 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
border-radius: 19px;
|
||||
background: #FFFFFF;
|
||||
height: 100%;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.reviews-front__card {
|
||||
width: calc(50% - 10px);
|
||||
}
|
||||
.reviews-front__card:last-child:nth-child(odd) {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.reviews-front__card {
|
||||
width: calc(33.33% - 14px);
|
||||
}
|
||||
}
|
||||
.reviews-front__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.reviews-front__avatar {
|
||||
width: 57px;
|
||||
height: 57px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
font-size: 1.375rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
.reviews-front__avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.reviews-front__avatar-letter {
|
||||
line-height: 1;
|
||||
}
|
||||
.reviews-front__client-data {
|
||||
flex: 1;
|
||||
}
|
||||
.reviews-front__name {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: var(--color-text, #0F172A);
|
||||
}
|
||||
.reviews-front__stars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.reviews-front__stars svg {
|
||||
display: block;
|
||||
}
|
||||
.reviews-front__date {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-light, #64748B);
|
||||
margin-left: auto;
|
||||
}
|
||||
.reviews-front__teacher {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted, #475569);
|
||||
margin: 0;
|
||||
}
|
||||
.reviews-front__teacher span {
|
||||
font-weight: 600;
|
||||
color: var(--color-text, #0F172A);
|
||||
}
|
||||
.reviews-front__text {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-muted, #475569);
|
||||
flex: 1;
|
||||
}
|
||||
.reviews-front__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #059669;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.reviews-front__badge svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.reviews-front__all-link {
|
||||
text-align: center;
|
||||
}
|
||||
.reviews-front__scroll-hint {
|
||||
display: none;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted, #475569);
|
||||
text-align: right;
|
||||
margin-bottom: 8px;
|
||||
animation: comparison-fade 3s ease-out forwards;
|
||||
animation-delay: 1s;
|
||||
opacity: 0;
|
||||
}
|
||||
.ds-hero__rating-text {
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.ds-hero__rating-badge {
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.reviews-front-section {
|
||||
padding: 32px 0;
|
||||
}
|
||||
.reviews-front__scroll-hint {
|
||||
display: block;
|
||||
}
|
||||
.reviews-front__wrap {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 2px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-primary, #2563EB) var(--color-border, #E2E8F0);
|
||||
}
|
||||
.reviews-front__wrap::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
.reviews-front__wrap::-webkit-scrollbar-track {
|
||||
background: var(--color-border, #E2E8F0);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.reviews-front__wrap::-webkit-scrollbar-thumb {
|
||||
background: var(--color-primary, #2563EB);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.reviews-front__grid {
|
||||
flex-wrap: nowrap;
|
||||
width: max-content;
|
||||
gap: 16px;
|
||||
}
|
||||
.reviews-front__card {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.reviews-front__card-inner {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- CTA-секция (cta-consultation) ---- */
|
||||
.cta-consultation {
|
||||
@ -2061,72 +1882,17 @@
|
||||
contain-intrinsic-size: 350px;
|
||||
}
|
||||
|
||||
/* reviews-front-section — 3 карточки отзывов */
|
||||
.reviews-front-section {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: 700px;
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════ */
|
||||
/* Reviews Link CTA (moved from main.css) */
|
||||
/* ════════════════════════════════════════════ */
|
||||
|
||||
.reviews-link {
|
||||
background: var(--color-bg-alt);
|
||||
border-radius: var(--radius-lg);
|
||||
margin: 40px auto;
|
||||
padding: 48px 24px;
|
||||
}
|
||||
.reviews-link .bento__full {
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.reviews-link .bento-p {
|
||||
max-width: 65ch;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.reviews-link .reviews-link__btn {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.reviews-link__btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 32px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm, 12px);
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-on-dark);
|
||||
cursor: pointer;
|
||||
background: var(--pksh-gradient-btn-bright);
|
||||
text-decoration: none;
|
||||
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
|
||||
box-shadow: var(--pksh-btn-shadow-bright);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.reviews-link__btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--pksh-btn-shadow-bright-hover);
|
||||
}
|
||||
.reviews-link__btn:focus-visible {
|
||||
outline: 3px solid var(--color-primary);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* ── Чередование фонов секций (в конце — переопределяет ранние правила) ── */
|
||||
.services,
|
||||
.faq-front,
|
||||
.video-tour,
|
||||
.stories,
|
||||
.reviews-front-section {
|
||||
.stories {
|
||||
background-color: var(--color-bg-alt, #F8FAFC);
|
||||
}
|
||||
.comparison,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -194,6 +194,17 @@
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text, #1E293B);
|
||||
}
|
||||
.teachers-card__about {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-muted, #475569);
|
||||
margin: 0;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.teachers-card__rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
1174
wp-content/themes/dekart/assets/css/single-teacher.css
Normal file
1174
wp-content/themes/dekart/assets/css/single-teacher.css
Normal file
File diff suppressed because it is too large
Load Diff
@ -87,7 +87,7 @@ document.addEventListener("click", function (event) {
|
||||
});
|
||||
|
||||
try {
|
||||
var swiperPopularSubjects = new Swiper(".swiper-popular-subjects", {
|
||||
new Swiper(".swiper-popular-subjects", {
|
||||
spaceBetween: 10,
|
||||
slidesPerView: 1,
|
||||
speed: 400,
|
||||
@ -99,10 +99,10 @@ try {
|
||||
},
|
||||
navigation: { nextEl: ".subject-next", prevEl: ".subject-prev" }
|
||||
});
|
||||
} catch(e) { /* skip — swiper not on this page */ }
|
||||
} catch { /* skip — swiper not on this page */ }
|
||||
|
||||
try {
|
||||
var swiperFoto = new Swiper(".swiper-foto", {
|
||||
new Swiper(".swiper-foto", {
|
||||
spaceBetween: 10,
|
||||
slidesPerView: 1,
|
||||
speed: 400,
|
||||
@ -111,15 +111,15 @@ try {
|
||||
navigation: { nextEl: ".foto-next", prevEl: ".foto-prev" },
|
||||
pagination: { el: ".swiper-pagination", clickable: true }
|
||||
});
|
||||
} catch(e) { /* skip — swiper not on this page */ }
|
||||
} catch { /* skip — swiper not on this page */ }
|
||||
|
||||
try { var swiperNews = new Swiper(".swiper-news", { spaceBetween: 10, slidesPerView: 1, speed: 400, preventInteractionOnTransition: true, keyboard: true, breakpoints: { 680: { slidesPerView: 2, speed: 400 }, 860: { slidesPerView: 3, speed: 400 } }, navigation: { nextEl: ".news-next", prevEl: ".news-prev" } }); } catch(e) { /* skip */ }
|
||||
try { new Swiper(".swiper-news", { spaceBetween: 10, slidesPerView: 1, speed: 400, preventInteractionOnTransition: true, keyboard: true, breakpoints: { 680: { slidesPerView: 2, speed: 400 }, 860: { slidesPerView: 3, speed: 400 } }, navigation: { nextEl: ".news-next", prevEl: ".news-prev" } }); } catch { /* skip */ }
|
||||
|
||||
try { var swiperCertificates = new Swiper(".certificates-owl", { spaceBetween: 10, slidesPerView: 1, speed: 400, loop: true, preventInteractionOnTransition: true, keyboard: true, navigation: { nextEl: ".certificates-owl-next", prevEl: ".certificates-owl-prev" } }); } catch(e) { /* skip */ }
|
||||
try { new Swiper(".certificates-owl", { spaceBetween: 10, slidesPerView: 1, speed: 400, loop: true, preventInteractionOnTransition: true, keyboard: true, navigation: { nextEl: ".certificates-owl-next", prevEl: ".certificates-owl-prev" } }); } catch { /* skip */ }
|
||||
|
||||
try { var swiperReviews = new Swiper(".reviews-owl", { spaceBetween: 40, speed: 400, slidesPerView: 1, keyboard: true, breakpoints: { 860: { slidesPerView: 2, speed: 400 } }, navigation: { nextEl: ".reviews-owl-next", prevEl: ".reviews-owl-prev" } }); } catch(e) { /* skip */ }
|
||||
try { new Swiper(".reviews-owl", { spaceBetween: 40, speed: 400, slidesPerView: 1, keyboard: true, breakpoints: { 860: { slidesPerView: 2, speed: 400 } }, navigation: { nextEl: ".reviews-owl-next", prevEl: ".reviews-owl-prev" } }); } catch { /* skip */ }
|
||||
|
||||
try { var swiperTeachers = new Swiper(".teachers-owl", { spaceBetween: 30, speed: 400, slidesPerView: 1, keyboard: true, breakpoints: { 540: { slidesPerView: 2 }, 860: { slidesPerView: 4 }, 992: { slidesPerView: 4, spaceBetween: 36 } }, navigation: { nextEl: ".teachers-owl-next", prevEl: ".teachers-owl-prev" } }); } catch(e) { /* skip */ }
|
||||
try { new Swiper(".teachers-owl", { spaceBetween: 30, speed: 400, slidesPerView: 1, keyboard: true, breakpoints: { 540: { slidesPerView: 2 }, 860: { slidesPerView: 4 }, 992: { slidesPerView: 4, spaceBetween: 36 } }, navigation: { nextEl: ".teachers-owl-next", prevEl: ".teachers-owl-prev" } }); } catch { /* skip */ }
|
||||
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('.b24-form-content .b24-form-btn').forEach(btn => {
|
||||
@ -247,7 +247,7 @@ document.querySelectorAll('.header-menu').forEach(item => {
|
||||
|
||||
/* ── FAQ Accordion (mystyle-accordion) ── */
|
||||
document.querySelectorAll('.mystyle-accordion-trigger').forEach(trigger => {
|
||||
trigger.addEventListener('click', function(e) {
|
||||
trigger.addEventListener('click', function() {
|
||||
const item = this.closest('.mystyle-accordion-item');
|
||||
if (!item) return;
|
||||
const isOpen = item.classList.contains('active');
|
||||
|
||||
103
wp-content/themes/dekart/assets/js/admin-branch-tabs.js
Normal file
103
wp-content/themes/dekart/assets/js/admin-branch-tabs.js
Normal file
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Админ: Настройки филиала — вертикальные вкладки.
|
||||
*
|
||||
* Секции .branch-section превращаются в панели: слева список-навигация,
|
||||
* справа содержимое активной вкладки. Активная вкладка запоминается
|
||||
* (localStorage) и отражается в #hash — удобно для ссылок/перезагрузок.
|
||||
* Старый аккордеон (h2) остаётся в DOM, но скрыт.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var sections = document.querySelectorAll('.branch-section');
|
||||
var form = document.querySelector('form[action="options.php"]');
|
||||
|
||||
if (! form || sections.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
var list = Array.prototype.slice.call(sections);
|
||||
|
||||
// ── Каркас: навигация + контент ──
|
||||
var wrap = document.createElement('div');
|
||||
wrap.className = 'dekart-branch-tabs';
|
||||
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'dekart-branch-tabs__nav';
|
||||
nav.setAttribute('aria-label', 'Разделы настроек филиала');
|
||||
|
||||
var ul = document.createElement('ul');
|
||||
nav.appendChild(ul);
|
||||
|
||||
var content = document.createElement('div');
|
||||
content.className = 'dekart-branch-tabs__content';
|
||||
|
||||
// Названия вкладок — из скрытых h2 секций.
|
||||
var buttons = [];
|
||||
list.forEach(function (section, i) {
|
||||
var h2 = section.querySelector(':scope > h2');
|
||||
var label = h2 ? h2.textContent.trim() : ('Раздел ' + (i + 1));
|
||||
|
||||
var li = document.createElement('li');
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.textContent = label;
|
||||
btn.dataset.tab = String(i);
|
||||
li.appendChild(btn);
|
||||
ul.appendChild(li);
|
||||
buttons.push(btn);
|
||||
});
|
||||
|
||||
// Встраиваем каркас ПЕРЕД перемещением секций (form.insertBefore
|
||||
// с отвязанным узлом падал бы — секции оставались вне формы).
|
||||
form.insertBefore(wrap, list[0]);
|
||||
wrap.appendChild(nav);
|
||||
wrap.appendChild(content);
|
||||
|
||||
// Переносим секции в область контента уже после подключения каркаса.
|
||||
list.forEach(function (section) {
|
||||
section.classList.remove('collapsed');
|
||||
section.setAttribute('role', 'tabpanel');
|
||||
content.appendChild(section);
|
||||
});
|
||||
|
||||
var activate = function (i) {
|
||||
i = Math.max(0, Math.min(i, list.length - 1));
|
||||
list.forEach(function (section, j) {
|
||||
section.classList.toggle('is-active', j === i);
|
||||
});
|
||||
buttons.forEach(function (btn, j) {
|
||||
btn.classList.toggle('is-active', j === i);
|
||||
});
|
||||
try {
|
||||
localStorage.setItem('dekart_branch_tab', String(i));
|
||||
} catch { /* приватный режим — не критично */ }
|
||||
};
|
||||
|
||||
buttons.forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var i = parseInt(btn.dataset.tab, 10);
|
||||
activate(i);
|
||||
if (history.replaceState) {
|
||||
history.replaceState(null, '', '#tab=' + (i + 1));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Стартовая вкладка: #hash > localStorage > первая.
|
||||
var start = 0;
|
||||
var hashMatch = (location.hash || '').match(/tab=(\d+)/);
|
||||
if (hashMatch) {
|
||||
start = parseInt(hashMatch[1], 10) - 1;
|
||||
} else {
|
||||
try {
|
||||
var saved = parseInt(localStorage.getItem('dekart_branch_tab'), 10);
|
||||
if (! isNaN(saved) && saved >= 0 && saved < list.length) {
|
||||
start = saved;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// submit остаётся под контентом.
|
||||
activate(start);
|
||||
})();
|
||||
43
wp-content/themes/dekart/assets/js/admin-teacher-tabs.js
Normal file
43
wp-content/themes/dekart/assets/js/admin-teacher-tabs.js
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Админ: страховка вкладок Carbon Fields (преподаватель).
|
||||
*
|
||||
* Carbon строит вкладки через React не мгновенно — на короткое время поля
|
||||
* могут быть видны списком. Если после полной загрузки вкладки так и не
|
||||
* собрались (класс cf-container--tabbed есть, списка вкладок нет) — это
|
||||
* сбой инициализации CF: помечаем метабокс красной рамкой и логируем,
|
||||
* чтобы починить корень, а не камуфлировать.
|
||||
*
|
||||
* Никаких авто-перезагрузок: можно потерять несохранённые правки.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (! document.body.classList.contains('post-type-teacher')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var markBroken = function () {
|
||||
document.body.classList.add('dekart-cf-tabs-broken');
|
||||
if (window.console && console.warn) {
|
||||
console.warn('[dekart] Carbon Fields: вкладки преподавателя не собрались. Обновите страницу (если нет несохранённых правок) или проверьте консоль на ошибки.');
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('load', function () {
|
||||
var checks = 0;
|
||||
var timer = window.setInterval(function () {
|
||||
var tabbed = document.querySelector('.cf-container--tabbed');
|
||||
var nav = document.querySelector('.cf-container__tabs-list');
|
||||
|
||||
if (nav || ! tabbed) {
|
||||
window.clearInterval(timer);
|
||||
return;
|
||||
}
|
||||
checks += 1;
|
||||
if (checks >= 10) { // ~4 сек: CF так долго не собирает вкладки
|
||||
window.clearInterval(timer);
|
||||
markBroken();
|
||||
}
|
||||
}, 400);
|
||||
});
|
||||
})();
|
||||
@ -1,8 +1,8 @@
|
||||
// Gallery modal
|
||||
document.addEventListener('click', function(e) {
|
||||
var modal = document.getElementById('lw-modal-gallery');
|
||||
var thumb = e.target.closest('.lw-gallery__thumb');
|
||||
if (thumb) {
|
||||
var modal = document.getElementById('lw-modal-gallery');
|
||||
var img = modal.querySelector('.lw-modal__img');
|
||||
img.src = thumb.dataset.full;
|
||||
modal.classList.add('lw-modal--open');
|
||||
@ -11,7 +11,6 @@ document.addEventListener('click', function(e) {
|
||||
}
|
||||
var close = e.target.closest('.lw-modal__close, .lw-modal__overlay');
|
||||
if (close) {
|
||||
var modal = document.getElementById('lw-modal-gallery');
|
||||
modal.classList.remove('lw-modal--open');
|
||||
document.body.style.overflow = '';
|
||||
return;
|
||||
@ -33,10 +32,10 @@ const btnCloseModalMentors = document.querySelector('.btn-close-mentors');
|
||||
const modalPrograms = document.querySelector('.modal-programs');
|
||||
const modalMentors = document.querySelector('.modal-mentors');
|
||||
|
||||
btnCloseModalPrograms.addEventListener("click", function (event) {
|
||||
btnCloseModalPrograms.addEventListener("click", function () {
|
||||
modalPrograms.classList.remove('open');
|
||||
});
|
||||
btnCloseModalMentors.addEventListener("click", function (event) {
|
||||
btnCloseModalMentors.addEventListener("click", function () {
|
||||
modalMentors.classList.remove('open');
|
||||
});
|
||||
|
||||
|
||||
51
wp-content/themes/dekart/assets/js/single-teacher.js
Normal file
51
wp-content/themes/dekart/assets/js/single-teacher.js
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Личная страница преподавателя — кейсы (табы), sticky CTA (mobile).
|
||||
* Reveal-анимации глобальные (add.js), здесь только интерактив страницы.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ── Кейсы: табы 9/11 ──
|
||||
var tabs = document.querySelectorAll('.teacher-single__case-tab');
|
||||
var panels = document.querySelectorAll('.teacher-single__case-panel');
|
||||
|
||||
if (tabs.length && panels.length) {
|
||||
Array.prototype.forEach.call(tabs, function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
var value = tab.getAttribute('data-case-tab');
|
||||
Array.prototype.forEach.call(tabs, function (t) {
|
||||
var active = t === tab;
|
||||
t.classList.toggle('is-active', active);
|
||||
t.setAttribute('aria-selected', active ? 'true' : 'false');
|
||||
});
|
||||
Array.prototype.forEach.call(panels, function (panel) {
|
||||
panel.classList.toggle('is-active', panel.getAttribute('data-case-panel') === value);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Sticky CTA (mobile): показать после hero ──
|
||||
var sticky = document.querySelector('.teacher-single__sticky');
|
||||
if (sticky) {
|
||||
var hero = document.querySelector('.teacher-single__hero');
|
||||
if (hero) {
|
||||
var showSticky = function () {
|
||||
sticky.hidden = window.scrollY < hero.offsetHeight - 80;
|
||||
};
|
||||
showSticky();
|
||||
var ticking = false;
|
||||
window.addEventListener('scroll', function () {
|
||||
if (!ticking) {
|
||||
window.requestAnimationFrame(function () {
|
||||
showSticky();
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
}, { passive: true });
|
||||
} else {
|
||||
sticky.hidden = false;
|
||||
}
|
||||
}
|
||||
})();
|
||||
@ -766,6 +766,19 @@ function add_carbon()
|
||||
}
|
||||
}
|
||||
|
||||
// Программы из каталога филиала (Настройки → Филиал → «Программы и направления»).
|
||||
// Преподаватель только выбирает программу; цены/ссылки — из каталога.
|
||||
$teacher_program_options = array(
|
||||
'' => '— Выберите программу —',
|
||||
);
|
||||
$branch_programs_catalog = (array) get_option('branch_programs', array());
|
||||
foreach ($branch_programs_catalog as $bp_item) {
|
||||
$bp_name = trim((string) ( $bp_item['name'] ?? '' ));
|
||||
if ('' !== $bp_name && ! isset($teacher_program_options[ $bp_name ])) {
|
||||
$teacher_program_options[ $bp_name ] = $bp_name;
|
||||
}
|
||||
}
|
||||
|
||||
// Иконки для плашек hero — только из SVG-спрайта (use href="#icon-*")
|
||||
$school_hero_float_icons = array(
|
||||
'teacher' => '👨🏫 Учитель',
|
||||
@ -1460,43 +1473,60 @@ function add_carbon()
|
||||
// ====================================================
|
||||
Container::make('post_meta', 'Поля для преподавателя')
|
||||
->where('post_type', '=', 'teacher')
|
||||
->add_fields(array(
|
||||
->add_tab('🎬 Hero', array(
|
||||
Field::make('html', 'teacher_hero_tab_desc', '')
|
||||
->set_html('<p style="color:#64748b;font-size:13px;margin:0 0 12px;">Фото и текст под заголовком на личной странице преподавателя. Если текст пуст — подставляется автоматически (направление + мини-группа + экзамен + школа и город).</p>'),
|
||||
Field::make('image', 'teacher_avatar', 'Фото преподавателя (hero/карточка):')
|
||||
->set_width(30),
|
||||
Field::make('textarea', 'teacher_hero_subtitle', 'Текст под H1 (subtitle)')
|
||||
->set_width(100)
|
||||
->set_help_text('Пример: «Учитель начальных классов — мини-группы 2-6 чел. · ОГЭ/ЕГЭ · школа «Декарт» в Щёлково». Пусто — соберётся автоматически.'),
|
||||
Field::make('text', 'teacher_rating', 'Оценка преподавателя (для hero и микроразметки)')
|
||||
->set_width(25)
|
||||
->set_default_value('5')
|
||||
->set_help_text('Пример: <strong>5</strong> или <strong>4,5</strong>. Звёзды в hero и aggregateRating в Schema.org.'),
|
||||
Field::make('text', 'teacher_group_size', 'Размер мини-группы (для hero, курсов, шагов и CTA)')
|
||||
->set_width(25)
|
||||
->set_default_value('2-6')
|
||||
->set_help_text('Пример: <strong>2-6</strong>. Участвует в hero, курсах, шагах, финальном CTA и микроразметке — заполняйте всегда.'),
|
||||
))
|
||||
->add_tab('👤 Профиль и данные', array(
|
||||
Field::make('text', 'teacher_direction', 'Направление преподавателя (текст в под фото):'),
|
||||
Field::make('textarea', 'teacher_description', 'Дескрипшн преподавателя (коротко — для карточек)')
|
||||
->set_width(100)
|
||||
->set_help_text('Выводится в карточке преподавателя на главной и на странице «Наши преподаватели». Максимум вывода в карточке — 3 строки. Рекомендуемый размер: до 140 знаков. Более длинный текст в карточке обрезается многоточием.'),
|
||||
Field::make('text', 'teacher_badge', 'Бейдж на карточке (например: 👶 Работает с дошкольниками)')
|
||||
->set_width(30)
|
||||
->set_help_text('Текст бейджа на карточке преподавателя. Если пусто — бейдж скрыт.'),
|
||||
Field::make('image', 'teacher_avatar', 'Аватар:')
|
||||
->set_width(15),
|
||||
Field::make('text', 'teacher_start_year', 'Год начала работы (только год, например: 1987)')
|
||||
->set_width(30)
|
||||
->set_attribute('placeholder', '1987')
|
||||
->set_help_text('Стаж рассчитается автоматически на странице подготовки к школе.'),
|
||||
Field::make('textarea', 'teacher_description', 'Дескрипшн преподавателя')
|
||||
->set_width(55),
|
||||
Field::make('textarea', 'teacher_about', 'О себе:'),
|
||||
))
|
||||
->add_tab('📄 О преподавателе', array(
|
||||
Field::make('textarea', 'teacher_about', 'О себе:')
|
||||
->set_width(100)
|
||||
->help_text('Обычный текст, вёрстка не нужна — абзацы разделяйте пустой строкой (Enter + пустая строка). Рекомендуемый объём: 600–1200 знаков (2–4 коротких абзаца).'),
|
||||
Field::make('textarea', 'teacher_education', 'Образование:'),
|
||||
Field::make('textarea', 'teacher_add_education', 'Дополнительное образование:'),
|
||||
Field::make('text', 'teacher_rating', 'Оценка преподавателя')
|
||||
->set_width(10)
|
||||
->set_default_value('5'),
|
||||
Field::make('text', 'teacher_diploma', 'Учебное заведение (для микроразметки alumniOf)')
|
||||
->set_width(50)
|
||||
->help_text('Одно главное учебное заведение (напр. «Московский педагогический государственный университет (МПГУ)»). Используется в Schema.org для свойства alumniOf. Если пусто — свойство не выводится.'),
|
||||
Field::make('text', 'teacher_video', 'Ссылка на видеопрезентацию')
|
||||
->set_width(100)
|
||||
->help_text('Укажите ссылку на видео в youtube. Пример: <iframe src="https://vk.com/video_ext.php?oid=-43228812&id=456240170hd=2" width="853" height="480" allow="autoplay; encrypted-media; fullscreen; picture-in-picture;" frameborder="0" allowfullscreen></iframe>'),
|
||||
))
|
||||
->add_tab('📚 Предметы и документы', array(
|
||||
Field::make('complex', 'teacher_subjects', 'Предметы обучения')
|
||||
->set_layout('tabbed-horizontal')
|
||||
->add_fields(array(
|
||||
Field::make('text', 'teacher_subjects_title', 'Укажите заголовок предмета')
|
||||
Field::make('select', 'teacher_subjects_title', 'Программа (направление)')
|
||||
->set_options($teacher_program_options)
|
||||
->set_width(50)
|
||||
->set_required(true)
|
||||
->help_text('Например: Репетитор по математике'),
|
||||
Field::make('textarea', 'select_direction', 'Укажите все направления преподавателя:')
|
||||
->help_text('Пример: <span>Математика</span><span>Русский язык</span>'),
|
||||
Field::make('association', 'select_category', 'Укажите категрии')
|
||||
->set_width(100)
|
||||
->set_types(array(
|
||||
array(
|
||||
'type' => 'term',
|
||||
'taxonomy' => 'trainingCategory',
|
||||
)
|
||||
))
|
||||
->help_text('В списке указаны все возможные категрии в обучении, выберите необходимые'),
|
||||
->help_text('Выберите из каталога программ филиала (Настройки → Филиал → «Программы и направления»). Цена и ссылка на страницу подтянутся из каталога.'),
|
||||
Field::make('textarea', 'teacher_subjects_topics', 'Темы направления (каждая с новой строки)')
|
||||
->set_help_text('Кликабельный список: «Математика», «Русский язык»… Каждая тема — с новой строки. Пусто — блок тем скрыт.'),
|
||||
)),
|
||||
Field::make('select', 'teacher_category', 'Категория для фильтра на странице преподавателей')
|
||||
->set_width(50)
|
||||
@ -1508,10 +1538,106 @@ function add_carbon()
|
||||
Field::make('image', 'certificates_foto', 'Изображение')
|
||||
))
|
||||
->help_text('Добавьте изображение дипломов и сертификатов преподавателя'),
|
||||
Field::make('text', 'teacher_video', 'Ссылка на видеопрезентацию')
|
||||
))
|
||||
->add_tab('📊 Цифры и дефицит', array(
|
||||
Field::make('html', 'teacher_metrics_help')
|
||||
->set_html('<div style="background:#F8FAFC;border:1px solid #E2E8F0;border-radius:8px;padding:12px 14px;margin:0 0 14px;color:#334155;font-size:13px;line-height:1.6"><strong>Блок «Цифры»</strong> — карточки под hero (рейтинг, средний балл, цифра про систему, размер группы и любые другие факты). Кнопкой <strong>«+ Добавить»</strong> создаёте карточку, крестиком рядом с полями — удаляете. В каждой карточке два поля: <strong>цифра</strong> и <strong>подпись</strong>. Порядок карточек = порядок вывода на странице. Пустые карточки не выводятся. Если карточек нет — блок «Цифры» скрыт.</div>'),
|
||||
Field::make('complex', 'teacher_metrics', 'Карточки цифр')
|
||||
->set_layout('tabbed-horizontal')
|
||||
->add_fields(array(
|
||||
Field::make('text', 'metric_num', 'Цифра / факт')
|
||||
->set_width(25)
|
||||
->set_required(true)
|
||||
->set_help_text('Короткое значение-результат. Примеры: <strong>5/5</strong>, <strong>100%</strong>, <strong>2-6</strong>, <strong>4,7</strong>, <strong>45 минут</strong>, <strong>500+</strong>.'),
|
||||
Field::make('text', 'metric_text', 'Подпись под цифрой')
|
||||
->set_width(75)
|
||||
->set_help_text('Что означает цифра. Примеры: <strong>«Рейтинг преподавателя · 5 отзывов»</strong>, <strong>«учеников продолжают обучение на следующий учебный год»</strong>, <strong>«человек в мини-группе — внимание как на репетиторе»</strong>, <strong>«средний балл учеников»</strong>.'),
|
||||
))
|
||||
->set_help_text('«+ Добавить» — новая карточка (цифра + подпись). Нет карточек — блок «Цифры» на странице скрыт, используйте Hero-таб для рейтинга и размера группы.'),
|
||||
Field::make('html', 'teacher_scarcity_sep', '')
|
||||
->set_html('<hr><h3>⏳ Полоса дефицита</h3><p style="color:#64748B;font-size:12px;margin:4px 0 10px;">Жёлтая полоса под цифрами: счётчик дней до экзамена + места в группе + пояснение. Пустое поле — пункт скрыт.</p>'),
|
||||
Field::make('text', 'teacher_seats_left', 'Осталось мест в группе')
|
||||
->set_width(25)
|
||||
->set_default_value('4')
|
||||
->set_help_text('Пример: <strong>4</strong>. Полоса дефицита: «4 места в группе». Пусто/0 — пункт скрыт.'),
|
||||
Field::make('text', 'teacher_exam_label', 'Экзамен/формат')
|
||||
->set_width(25)
|
||||
->set_default_value('ОГЭ/ЕГЭ')
|
||||
->set_help_text('Пример: <strong>«ОГЭ/ЕГЭ»</strong>. Полоса дефицита: «дней до ОГЭ/ЕГЭ».'),
|
||||
Field::make('text', 'teacher_exam_date', 'Дата экзамена для счётчика (YYYY-MM-DD)')
|
||||
->set_width(25)
|
||||
->set_default_value('')
|
||||
->set_help_text('Пример: <strong>2027-05-24</strong>. Пусто — счётчик дней скрыт.'),
|
||||
Field::make('text', 'teacher_scarcity_note', 'Текст полосы дефицита')
|
||||
->set_width(100)
|
||||
->help_text('Укажите ссылку на видео в youtube. Пример: <iframe src="https://vk.com/video_ext.php?oid=-43228812&id=456240170hd=2" width="853" height="480" allow="autoplay; encrypted-media; fullscreen; picture-in-picture;" frameborder="0" allowfullscreen></iframe>'),
|
||||
Field::make('complex', 'questions', 'Блок вопросы и ответы')
|
||||
->set_default_value('Набор в группу идёт каждый месяц, но места ограничены.')
|
||||
->set_help_text('Пример: <strong>«Набор в группу идёт каждый месяц, но места ограничены»</strong>. Пусто — скрыто.'),
|
||||
))
|
||||
->add_tab('🎨 Сегменты, кейсы, шаги', array(
|
||||
Field::make('complex', 'teacher_segments', 'Сегменты учеников (карточки)')
|
||||
->set_layout('tabbed-horizontal')
|
||||
->add_fields(array(
|
||||
Field::make('text', 'segment_title', 'Заголовок карточки')
|
||||
->set_width(30),
|
||||
Field::make('textarea', 'segment_text', 'Текст карточки')
|
||||
->set_width(70),
|
||||
))
|
||||
->set_help_text('3 карточки «Для кого занятия». Пусто — покажутся 3 стандартные.'),
|
||||
Field::make('complex', 'teacher_cases', 'Кейсы учеников (табы 9/11 класс)')
|
||||
->set_layout('tabbed-horizontal')
|
||||
->add_fields(array(
|
||||
Field::make('text', 'case_tab', 'Таб (9 или 11)')
|
||||
->set_width(20),
|
||||
Field::make('textarea', 'case_text', 'История кейса')
|
||||
->set_width(80),
|
||||
))
|
||||
->set_help_text('Кейсы с результатом. tab: 9 или 11 класс.'),
|
||||
Field::make('complex', 'teacher_steps', 'Шаги «Как проходит первое занятие»')
|
||||
->set_layout('tabbed-horizontal')
|
||||
->add_fields(array(
|
||||
Field::make('text', 'step_title', 'Название шага')
|
||||
->set_width(30),
|
||||
Field::make('textarea', 'step_text', 'Описание шага')
|
||||
->set_width(70),
|
||||
))
|
||||
->set_help_text('Таймлайн: от заявки до обратной связи. Пусто — 5 стандартных шагов.'),
|
||||
))
|
||||
->add_tab('📈 Результаты учеников', array(
|
||||
Field::make('html', 'teacher_results_help')
|
||||
->set_html('<div style="background:#F8FAFC;border:1px solid #E2E8F0;border-radius:8px;padding:12px 14px;margin:0 0 14px;color:#334155;font-size:13px;line-height:1.6"><strong>Блок «Измеримые результаты»</strong> — отдельная секция на странице (не путать с «Кому подойдут»). 3–6 пунктов: короткий измеримый результат + пояснение. Пусто — блок соберётся из заполненных метрик преподавателя (рейтинг, стаж, средний балл, отзывы).</div>'),
|
||||
Field::make('complex', 'teacher_results', 'Измеримые результаты')
|
||||
->set_layout('tabbed-horizontal')
|
||||
->add_fields(array(
|
||||
Field::make('text', 'result_title', 'Короткий результат (цифра/факт)')
|
||||
->set_width(30)
|
||||
->set_required(true)
|
||||
->help_text('Например: «+2 балла», «100%», «45 минут».'),
|
||||
Field::make('textarea', 'result_text', 'Пояснение')
|
||||
->set_width(70)
|
||||
->help_text('Например: «средний рост успеваемости за полугодие».'),
|
||||
))
|
||||
->set_help_text('3–6 пунктов. Пусто — собирается из метрик преподавателя.'),
|
||||
Field::make('html', 'teacher_results_fallback_sep', '')
|
||||
->set_html('<hr><h3>⚙ Авто-пункты (если «Измеримые результаты» пустые)</h3><p style="color:#64748B;font-size:12px;margin:4px 0 10px;">Подставляются в секцию результатов, пока complex выше не заполнен. Заполните — complex перекроет авто-пункты.</p>'),
|
||||
Field::make('text', 'teacher_avg_score', 'Средний балл учеников')
|
||||
->set_width(25)
|
||||
->set_default_value('')
|
||||
->set_help_text('Авто-пункт. Пример: <strong>4,7</strong>. Пусто — пункт скрыт.'),
|
||||
Field::make('text', 'teacher_avg_label', 'Подпись среднего балла')
|
||||
->set_width(25)
|
||||
->set_default_value('Средний балл учеников')
|
||||
->set_help_text('Пример: <strong>«Средний балл учеников»</strong>.'),
|
||||
Field::make('text', 'teacher_school_num', 'Цифра про систему/школу')
|
||||
->set_width(25)
|
||||
->set_default_value('100%')
|
||||
->set_help_text('Авто-пункт. Пример: <strong>100%</strong>. Пусто — пункт скрыт.'),
|
||||
Field::make('text', 'teacher_school_label', 'Подпись к цифре про систему')
|
||||
->set_width(25)
|
||||
->set_default_value('учеников продолжают обучение на следующий учебный год')
|
||||
->set_help_text('Пример: <strong>«учеников продолжают обучение на следующий учебный год»</strong>.'),
|
||||
))
|
||||
->add_tab('❓ Вопросы и отзывы', array(
|
||||
Field::make('complex', 'questions', 'Блок вопросы и ответы (страница)')
|
||||
->set_layout('tabbed-horizontal')
|
||||
->add_fields(array(
|
||||
Field::make('textarea', 'questions_question', 'Текст вопроса')
|
||||
@ -1519,113 +1645,25 @@ function add_carbon()
|
||||
Field::make('textarea', 'questions_answer', 'Текст ответа')
|
||||
->set_width(70),
|
||||
)),
|
||||
// ═══════════════ SEO и конверсия личной страницы (Bento 2026) ═══════════════
|
||||
Field::make('html', 'teacher_conv_seo_desc', '')
|
||||
->set_html('<h3 style="margin:18px 0 6px;font-size:15px;">SEO-мета (title/description страницы преподавателя)</h3><p style="color:#64748b;font-size:13px;margin:0 0 12px;">Если title/description пустые — собираются автоматически: предмет, мини-группы 2-6 чел., год экзамена, средний балл, остаток мест.</p>'),
|
||||
Field::make('text', 'teacher_meta_subject', 'Предмет для мета и заголовков')
|
||||
->set_width(50)
|
||||
->set_default_value('')
|
||||
->set_help_text('Например: «Математика». Пусто — берётся «Направление преподавателя».'),
|
||||
Field::make('text', 'teacher_exam_label', 'Экзамен/формат (для title)')
|
||||
->set_width(25)
|
||||
->set_default_value('ОГЭ/ЕГЭ'),
|
||||
Field::make('text', 'teacher_exam_year', 'Год экзамена (для title)')
|
||||
->set_width(25)
|
||||
->set_default_value('')
|
||||
->set_help_text('Пусто — следующий год. Пример: 2027.'),
|
||||
Field::make('text', 'teacher_group_size', 'Размер мини-группы')
|
||||
->set_width(25)
|
||||
->set_default_value('2-6'),
|
||||
Field::make('text', 'teacher_seats_left', 'Осталось мест в группе')
|
||||
->set_width(25)
|
||||
->set_default_value('4'),
|
||||
Field::make('text', 'teacher_avg_score', 'Средний балл выпускников')
|
||||
->set_width(25)
|
||||
->set_default_value('')
|
||||
->set_help_text('Пример: 4,7. Пусто — блок скрыт.'),
|
||||
Field::make('textarea', 'teacher_meta_title_override', 'Свой title (перекроет авто)')
|
||||
->set_width(50),
|
||||
Field::make('textarea', 'teacher_meta_desc_override', 'Свой description (перекроет авто)')
|
||||
->set_width(50),
|
||||
// ── Hero: второй CTA и риск ──
|
||||
Field::make('html', 'teacher_conv_hero_desc', '')
|
||||
->set_html('<h3 style="margin:18px 0 6px;font-size:15px;">Hero: CTA и снятие риска</h3>'),
|
||||
Field::make('text', 'teacher_cta_primary', 'Текст главной кнопки')
|
||||
->set_width(50)
|
||||
->set_default_value('Записаться на диагностику'),
|
||||
Field::make('text', 'teacher_schedule_label', 'Текст кнопки «Расписание»')
|
||||
->set_width(50)
|
||||
->set_default_value('Посмотреть расписание групп'),
|
||||
Field::make('text', 'teacher_schedule_url', 'Ссылка «Расписание» (URL)')
|
||||
->set_width(50)
|
||||
->set_help_text('Например: https://dekart-school.ru/raspisanie/'),
|
||||
Field::make('text', 'teacher_risk_note', 'Микро-текст под кнопками')
|
||||
->set_width(100)
|
||||
->set_default_value('Перезвоним за 15 минут · Без обязательств'),
|
||||
// ── Цифры и школа ──
|
||||
Field::make('html', 'teacher_conv_numbers_desc', '')
|
||||
->set_html('<h3 style="margin:18px 0 6px;font-size:15px;">Цифры (преподаватель + система)</h3>'),
|
||||
Field::make('text', 'teacher_school_num', 'Цифра про систему/школу')
|
||||
->set_width(25)
|
||||
->set_default_value('100%'),
|
||||
Field::make('text', 'teacher_school_label', 'Подпись к цифре про систему')
|
||||
->set_width(75)
|
||||
->set_default_value('выпускников мини-групп поступили на бюджет'),
|
||||
Field::make('text', 'teacher_exam_date', 'Дата экзамена для счётчика (YYYY-MM-DD)')
|
||||
->set_width(50)
|
||||
->set_help_text('Пример: 2027-05-24. Пусто — блок дефицита без счётчика дней.'),
|
||||
Field::make('text', 'teacher_scarcity_note', 'Текст полосы дефицита')
|
||||
->set_width(100)
|
||||
->set_default_value('Набор в группу идёт каждый месяц, но места ограничены.'),
|
||||
// ── Сегменты / кейсы / шаги ──
|
||||
Field::make('html', 'teacher_conv_content_desc', '')
|
||||
->set_html('<h3 style="margin:18px 0 6px;font-size:15px;">Сегменты, кейсы, «Как проходит занятие»</h3>'),
|
||||
Field::make('complex', 'teacher_segments', 'Сегменты учеников (карточки)')
|
||||
->set_layout('tabbed-horizontal')
|
||||
->set_help_text('3 карточки «Для кого занятия». Пусто — покажутся 3 стандартные.')
|
||||
->add_fields(array(
|
||||
Field::make('text', 'segment_title', 'Заголовок карточки')
|
||||
->set_width(30),
|
||||
Field::make('textarea', 'segment_text', 'Текст карточки')
|
||||
->set_width(70),
|
||||
)),
|
||||
Field::make('complex', 'teacher_cases', 'Кейсы учеников (табы 9/11 класс)')
|
||||
->set_layout('tabbed-horizontal')
|
||||
->set_help_text('Кейсы с результатом. tab: 9 или 11 класс.')
|
||||
->add_fields(array(
|
||||
Field::make('text', 'case_tab', 'Таб (9 или 11)')
|
||||
->set_width(20),
|
||||
Field::make('textarea', 'case_text', 'История кейса')
|
||||
->set_width(80),
|
||||
)),
|
||||
Field::make('complex', 'teacher_steps', 'Шаги «Как проходит первое занятие»')
|
||||
->set_layout('tabbed-horizontal')
|
||||
->set_help_text('Таймлайн: от заявки до обратной связи. Пусто — 5 стандартных шагов.')
|
||||
->add_fields(array(
|
||||
Field::make('text', 'step_title', 'Название шага')
|
||||
->set_width(30),
|
||||
Field::make('textarea', 'step_text', 'Описание шага')
|
||||
->set_width(70),
|
||||
)),
|
||||
// ── FAQ / доверие ──
|
||||
Field::make('html', 'teacher_conv_faq_desc', '')
|
||||
->set_html('<h3 style="margin:18px 0 6px;font-size:15px;">FAQ и доверие</h3>'),
|
||||
Field::make('complex', 'teacher_faq_extra', 'Доп. вопросы (про мини-группы)')
|
||||
->set_layout('tabbed-horizontal')
|
||||
->set_help_text('Вопросы про формат: «Чем мини-группа лучше репетитора?» и т.п. Пусто — 2 стандартных.')
|
||||
->add_fields(array(
|
||||
Field::make('textarea', 'faq_q', 'Вопрос')
|
||||
->set_width(30),
|
||||
Field::make('textarea', 'faq_a', 'Ответ')
|
||||
->set_width(70),
|
||||
)),
|
||||
Field::make('textarea', 'teacher_license', 'Текст блока «Лицензия и доверие»')
|
||||
->set_default_value('Частная школа «Декарт» работает по лицензии на образовательную деятельность (бессрочная). Копия лицензии и реквизиты — по запросу у администратора. Рейтинг на Яндекс Картах и 2ГИС формируется из реальных отзывов родителей.')
|
||||
->set_help_text('Показывается в блоке доверия перед картой.'),
|
||||
))
|
||||
->set_help_text('Вопросы про формат: «Чем мини-группа лучше репетитора?» и т.п. Пусто — 2 стандартных.'),
|
||||
Field::make('checkbox', 'teacher_show_social_reviews', 'Показывать «Отзывы о преподавателе» на странице')
|
||||
->set_option_value('yes')
|
||||
->set_default_value('yes'),
|
||||
));
|
||||
))
|
||||
->add_tab('📜 Лицензия и доверие', array(
|
||||
Field::make('textarea', 'teacher_license', 'Текст блока «Лицензия и доверие»')
|
||||
->set_default_value('Частная школа «Декарт» работает по лицензии на образовательную деятельность (бессрочная). Копия лицензии и реквизиты — по запросу у администратора. Рейтинг на Яндекс Картах и 2ГИС формируется из реальных отзывов родителей.')
|
||||
->set_help_text('Показывается в блоке доверия перед картой.'),
|
||||
))->set_layout('tabbed-vertical');
|
||||
|
||||
|
||||
// ====================================================
|
||||
// Доп поля для страницы "Преподаватели" (Bento 2026)
|
||||
@ -2824,6 +2862,21 @@ function enqueue_template_specific_styles()
|
||||
true
|
||||
);
|
||||
}
|
||||
if (is_singular('teacher')) {
|
||||
wp_enqueue_style(
|
||||
'single-teacher-style',
|
||||
get_template_directory_uri() . '/assets/css/single-teacher.css',
|
||||
array(),
|
||||
filemtime(get_template_directory() . '/assets/css/single-teacher.css')
|
||||
);
|
||||
wp_enqueue_script(
|
||||
'single-teacher-script',
|
||||
get_template_directory_uri() . '/assets/js/single-teacher.js',
|
||||
array(),
|
||||
filemtime(get_template_directory() . '/assets/js/single-teacher.js'),
|
||||
true
|
||||
);
|
||||
}
|
||||
if (is_page_template('page-after-school.php')) {
|
||||
wp_enqueue_style(
|
||||
'template-after-style',
|
||||
@ -3131,29 +3184,55 @@ function dekart_teacher_review_counts(): array
|
||||
|
||||
foreach ($review_ids as $review_id) {
|
||||
$teacher_id = 0;
|
||||
foreach ($link_keys as $key) {
|
||||
$raw = get_post_meta((int) $review_id, $key, true);
|
||||
if (empty($raw)) {
|
||||
continue;
|
||||
}
|
||||
$value = maybe_unserialize($raw);
|
||||
$ids = array();
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $entry) {
|
||||
if (is_object($entry) && isset($entry->id)) {
|
||||
$ids[] = (int) $entry->id;
|
||||
} elseif (is_numeric($entry)) {
|
||||
$ids[] = (int) $entry;
|
||||
|
||||
// Основной путь: Carbon Fields Association (ключи вида _review_teacher|||0|id)
|
||||
if (function_exists('carbon_get_post_meta')) {
|
||||
$cached = carbon_get_post_meta((int) $review_id, 'review_teacher');
|
||||
if (is_array($cached)) {
|
||||
foreach ($cached as $entry) {
|
||||
if (is_array($entry) && ! empty($entry['id'])) {
|
||||
$teacher_id = (int) $entry['id'];
|
||||
break;
|
||||
}
|
||||
if (is_string($entry) && preg_match('/post:teacher:(\d+)/', $entry, $m)) {
|
||||
$teacher_id = (int) $m[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} elseif (is_numeric($value)) {
|
||||
$ids[] = (int) $value;
|
||||
}
|
||||
if (! empty($ids)) {
|
||||
$teacher_id = $ids[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy fallback: сырые ключи меты
|
||||
if (! $teacher_id) {
|
||||
foreach ($link_keys as $key) {
|
||||
$raw = get_post_meta((int) $review_id, $key, true);
|
||||
if (empty($raw)) {
|
||||
continue;
|
||||
}
|
||||
$value = maybe_unserialize($raw);
|
||||
$ids = array();
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $entry) {
|
||||
if (is_object($entry) && isset($entry->id)) {
|
||||
$ids[] = (int) $entry->id;
|
||||
} elseif (is_array($entry) && isset($entry['id'])) {
|
||||
$ids[] = (int) $entry['id'];
|
||||
} elseif (is_string($entry) && preg_match('/post:teacher:(\d+)/', $entry, $m)) {
|
||||
$ids[] = (int) $m[1];
|
||||
} elseif (is_numeric($entry)) {
|
||||
$ids[] = (int) $entry;
|
||||
}
|
||||
}
|
||||
} elseif (is_numeric($value)) {
|
||||
$ids[] = (int) $value;
|
||||
}
|
||||
if (! empty($ids)) {
|
||||
$teacher_id = $ids[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($teacher_id > 0) {
|
||||
$map[ $teacher_id ] = isset($map[ $teacher_id ]) ? $map[ $teacher_id ] + 1 : 1;
|
||||
}
|
||||
@ -3315,6 +3394,32 @@ function dekart_add_preload_hints()
|
||||
}
|
||||
add_action('wp_head', 'dekart_add_preload_hints', 1);
|
||||
|
||||
// ====================================================
|
||||
// ✅ Hardening: XML-RPC + REST user enumeration (пред-продакшен)
|
||||
// ====================================================
|
||||
|
||||
// XML-RPC полностью выключен (защита от pingback DDoS и брутфорса через system.multicall)
|
||||
add_filter('xmlrpc_enabled', '__return_false');
|
||||
|
||||
// Убрать ВСЕ XML-RPC методы (включая анонимные pingback.* / system.* — вектор амплификации).
|
||||
// WP 7.1: xmlrpc_enabled=false не блокирует анонимные методы, поэтому дублируем.
|
||||
add_filter('xmlrpc_methods', '__return_empty_array');
|
||||
|
||||
// Закрыть REST-эндпоинты пользователей для анонимов (wp/v2/users раскрывает логины)
|
||||
add_filter('rest_endpoints', static function (array $endpoints): array {
|
||||
if (is_user_logged_in()) {
|
||||
return $endpoints;
|
||||
}
|
||||
|
||||
foreach (array_keys($endpoints) as $route) {
|
||||
if (preg_match('#^/wp/v2/users(/|$)#', $route)) {
|
||||
unset($endpoints[$route]);
|
||||
}
|
||||
}
|
||||
|
||||
return $endpoints;
|
||||
});
|
||||
|
||||
/* ── Honest timer: end-of-month deadline ── */
|
||||
/**
|
||||
* Return end-of-current-month as ISO 8601 string.
|
||||
@ -3373,13 +3478,10 @@ function dekart_acf_delete_json($post_id)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Счетчики — централизованное управление (ТОП-меню)
|
||||
add_action('admin_menu', 'dekart_counters_menu');
|
||||
function dekart_counters_menu()
|
||||
{
|
||||
add_menu_page('Счетчики', 'Счетчики', 'manage_options', 'site-analytics-settings', 'dekart_counters_page', 'dashicons-chart-bar', 3);
|
||||
}
|
||||
|
||||
// Счетчики — перенесены во вкладку «Настройки филиала»
|
||||
// (ТОП-меню удалено; опции зарегистрированы в dekart_branch_group;
|
||||
// вывод на фронте — те же хуки wp_head/wp_body_open/wp_footer)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
add_action('admin_init', 'dekart_counters_settings');
|
||||
function dekart_counters_settings()
|
||||
{
|
||||
@ -3490,8 +3592,7 @@ function dekart_output_counters_footer()
|
||||
$screen = get_current_screen();
|
||||
if (! $screen) { return; }
|
||||
$is_carbon = strpos($screen->id, 'carbon_fields') !== false;
|
||||
$is_branch = strpos($screen->id, 'branch-settings') !== false;
|
||||
if (! $is_carbon && ! $is_branch) { return; }
|
||||
if (! $is_carbon) { return; }
|
||||
?>
|
||||
<style>
|
||||
:root {
|
||||
@ -3507,10 +3608,8 @@ function dekart_output_counters_footer()
|
||||
--cf-warning: #fbbf24;
|
||||
}
|
||||
/* Общий фон страницы */
|
||||
body.branch-settings_page_branch-settings,
|
||||
body.branch-settings_page_carbon_fields,
|
||||
body.toplevel_page_carbon_fields,
|
||||
body.toplevel_page_branch-settings {
|
||||
body.toplevel_page_carbon_fields {
|
||||
background: var(--cf-bg) !important;
|
||||
}
|
||||
#wpbody-content .wrap {
|
||||
@ -3710,6 +3809,8 @@ function dekart_branch_register_settings()
|
||||
'branch_email' => 'email',
|
||||
'branch_name' => 'text',
|
||||
'branch_price_range' => 'text',
|
||||
'branch_price' => 'text',
|
||||
'branch_price_note' => 'text',
|
||||
'branch_logistics_walk' => 'text',
|
||||
'branch_logistics_parking' => 'text',
|
||||
'branch_logistics_schools' => 'text',
|
||||
@ -3749,7 +3850,9 @@ function dekart_branch_register_settings()
|
||||
register_setting('dekart_branch_group', 'branch_messenger_max_on', array( 'sanitize_callback' => 'absint' ));
|
||||
register_setting('dekart_branch_group', 'branch_messenger_extra');
|
||||
register_setting('dekart_branch_group', 'branch_schema_description', array( 'sanitize_callback' => 'sanitize_textarea_field' ));
|
||||
register_setting('dekart_branch_group', 'branch_license', array( 'sanitize_callback' => 'sanitize_textarea_field' ));
|
||||
register_setting('dekart_branch_group', 'branch_area_served', array( 'sanitize_callback' => 'sanitize_text_field' ));
|
||||
register_setting('dekart_branch_group', 'branch_programs', array( 'sanitize_callback' => 'dekart_sanitize_branch_programs' ));
|
||||
register_setting('dekart_branch_group', 'branch_price_school', array( 'sanitize_callback' => 'sanitize_text_field' ));
|
||||
register_setting('dekart_branch_group', 'branch_price_preschool', array( 'sanitize_callback' => 'sanitize_text_field' ));
|
||||
register_setting('dekart_branch_group', 'branch_price_afterschool', array( 'sanitize_callback' => 'sanitize_text_field' ));
|
||||
@ -3768,6 +3871,13 @@ function dekart_branch_register_settings()
|
||||
// Поля branch_map_url, branch_founded, branch_photos — удалены.
|
||||
// Они дублировали CF таб «SEO и микроразметка» (schema_*).
|
||||
// Чтение: inc/schema.php → dekart_schema_option() → CF → Settings API (backward compat).
|
||||
|
||||
// Счётчики (перенесены из ТОП-меню «Счетчики» во вкладку филиала).
|
||||
// Регистрация в группе филиала, чтобы сохранялись одной кнопкой.
|
||||
// Старая группа dekart_counters_group остаётся для обратной совместимости.
|
||||
foreach (array( 'dekart_counter_yandex', 'dekart_counter_gtm_head', 'dekart_counter_gtm_body', 'dekart_counter_footer' ) as $counter_option) {
|
||||
register_setting('dekart_branch_group', $counter_option);
|
||||
}
|
||||
}
|
||||
|
||||
function dekart_branch_settings_page()
|
||||
@ -3890,6 +4000,11 @@ function dekart_branch_settings_page()
|
||||
<input type="text" name="branch_director_creds" value="<?php echo esc_attr(get_option('branch_director_creds', 'директор частной школы «Декарт», педагогический стаж 18 лет')); ?>">
|
||||
<p class="desc">Должность и достижения. Например: «директор частной школы «Декарт», педагогический стаж 18 лет»</p>
|
||||
</div>
|
||||
<div class="branch-field">
|
||||
<label>Лицензия (для Schema.org hasCredential)</label>
|
||||
<textarea name="branch_license" rows="2" style="width:100%;"><?php echo esc_textarea(get_option('branch_license', '')); ?></textarea>
|
||||
<p class="desc">Текст про лицензию на образовательную деятельность. Выводится в микроразметке (hasCredential) и в блоке «Работаем официально».</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4014,6 +4129,65 @@ function dekart_branch_settings_page()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="branch-section collapsed">
|
||||
<h2>🎓 ПРОГРАММЫ И НАПРАВЛЕНИЯ</h2>
|
||||
<div class="branch-section-body">
|
||||
<p class="desc">Единый каталог программ/направлений школы: <strong>название, цена, пояснение к цене, ссылка на страницу программы, теги каталога</strong>. Цены отсюда попадают в карточки преподавателей и в микроразметку Offer. Преподаватели <strong>выбирают</strong> программу из каталога (Настройки → преподаватель → «Предметы обучения») — свои теги направления у них задаются облаком тегов отдельно.</p>
|
||||
<div id="branch-programs-repeater">
|
||||
<?php
|
||||
$programs = get_option('branch_programs', array());
|
||||
if (! is_array($programs) || empty($programs)) {
|
||||
$programs = array( array( 'name' => '', 'price' => '', 'price_note' => '', 'url' => '', 'tags' => '' ) );
|
||||
}
|
||||
foreach ($programs as $i => $p) {
|
||||
echo '<div class="branch-program-row">'
|
||||
. '<input type="text" name="branch_programs[' . $i . '][name]" value="' . esc_attr($p['name'] ?? '') . '" placeholder="Название (напр. Нейропсихолог)">'
|
||||
. '<input type="text" name="branch_programs[' . $i . '][price]" value="' . esc_attr($p['price'] ?? '') . '" placeholder="Цена (напр. от 1 290 ₽)" style="max-width:170px;">'
|
||||
. '<input type="text" name="branch_programs[' . $i . '][price_note]" value="' . esc_attr($p['price_note'] ?? '') . '" placeholder="Пояснение к цене" style="max-width:200px;">'
|
||||
. '<input type="url" name="branch_programs[' . $i . '][url]" value="' . esc_attr($p['url'] ?? '') . '" placeholder="https://… (страница программы)" style="max-width:200px;">'
|
||||
. '<input type="text" name="branch_programs[' . $i . '][tags]" value="' . esc_attr($p['tags'] ?? '') . '" placeholder="Теги каталога (слова через пробел)" style="max-width:240px;">'
|
||||
. '<button type="button" class="button branch-program-remove">—</button>'
|
||||
. '</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<button type="button" class="button" id="branch-programs-add">+ Добавить программу</button>
|
||||
<style>
|
||||
.branch-program-row {
|
||||
display:flex; align-items:center; gap:8px; margin-bottom:6px; flex-wrap:wrap;
|
||||
}
|
||||
.branch-program-row input[type="text"],
|
||||
.branch-program-row input[type="url"] {
|
||||
min-width:160px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function() {
|
||||
var container = document.getElementById('branch-programs-repeater');
|
||||
var addBtn = document.getElementById('branch-programs-add');
|
||||
if (! addBtn) { return; }
|
||||
addBtn.addEventListener('click', function() {
|
||||
var idx = container.children.length;
|
||||
var row = document.createElement('div');
|
||||
row.className = 'branch-program-row';
|
||||
row.innerHTML = '<input type="text" name="branch_programs[' + idx + '][name]" placeholder="Название (напр. Нейропсихолог)">'
|
||||
+ '<input type="text" name="branch_programs[' + idx + '][price]" placeholder="Цена (напр. от 1 290 ₽)" style="max-width:170px;">'
|
||||
+ '<input type="text" name="branch_programs[' + idx + '][price_note]" placeholder="Пояснение к цене" style="max-width:200px;">'
|
||||
+ '<input type="url" name="branch_programs[' + idx + '][url]" placeholder="https://…" style="max-width:200px;">'
|
||||
+ '<input type="text" name="branch_programs[' + idx + '][tags]" placeholder="Теги каталога" style="max-width:240px;">'
|
||||
+ '<button type="button" class="button branch-program-remove">—</button>';
|
||||
container.appendChild(row);
|
||||
});
|
||||
container.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('branch-program-remove')) {
|
||||
e.target.parentElement.remove();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="branch-section collapsed">
|
||||
<h2>🏢 ОРГАНИЗАЦИЯ</h2>
|
||||
<div class="branch-section-body">
|
||||
@ -4031,6 +4205,16 @@ function dekart_branch_settings_page()
|
||||
<input type="text" name="branch_price_range" value="<?php echo esc_attr(get_option('branch_price_range', '$$')); ?>">
|
||||
<p class="desc">Для микроразметки. Например: "$", "$$", "$$$".</p>
|
||||
</div>
|
||||
<div class="branch-field">
|
||||
<label>Цена занятия (для страниц преподавателей)</label>
|
||||
<input type="text" name="branch_price" value="<?php echo esc_attr(get_option('branch_price', '')); ?>" style="width:100%;max-width:220px">
|
||||
<p class="desc">Стоимость занятия в мини-группе. Пример: «от 1 290 ₽». Выводится в блоке стоимости на странице преподавателя и в микроразметке Offer (price).<br><strong>Пусто</strong> — на странице преподавателя покажется «Стоимость — по запросу», в микроразметке price не выводится.</p>
|
||||
</div>
|
||||
<div class="branch-field">
|
||||
<label>Пояснение к цене</label>
|
||||
<input type="text" name="branch_price_note" value="<?php echo esc_attr(get_option('branch_price_note', 'за занятие 60 минут в мини-группе')); ?>" style="width:100%;max-width:420px">
|
||||
<p class="desc">Подпись под ценой. Пример: «за занятие 60 минут в мини-группе». Пусто — подпись скрыта.</p>
|
||||
</div>
|
||||
<div class="branch-field">
|
||||
<label>Средний рейтинг школы (для метрик)</label>
|
||||
<input type="text" name="branch_avg_rating" value="<?php echo esc_attr(get_option('branch_avg_rating', '4.9')); ?>" style="width:100%;max-width:120px">
|
||||
@ -4149,6 +4333,32 @@ function dekart_branch_settings_page()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="branch-section collapsed">
|
||||
<h2>📊 СЧЕТЧИКИ</h2>
|
||||
<div class="branch-section-body">
|
||||
<div class="branch-field">
|
||||
<label>Яндекс.Метрика (head)</label>
|
||||
<textarea name="dekart_counter_yandex" rows="8" style="width:100%;max-width:700px;font-family:monospace;font-size:13px"><?php echo esc_textarea(get_option('dekart_counter_yandex', '')); ?></textarea>
|
||||
<p class="desc">Код счётчика. Выводится в <code><head></code>.</p>
|
||||
</div>
|
||||
<div class="branch-field">
|
||||
<label>Google Tag Manager (head)</label>
|
||||
<textarea name="dekart_counter_gtm_head" rows="8" style="width:100%;max-width:700px;font-family:monospace;font-size:13px"><?php echo esc_textarea(get_option('dekart_counter_gtm_head', '')); ?></textarea>
|
||||
<p class="desc">Основной код GTM. Выводится в <code><head></code>.</p>
|
||||
</div>
|
||||
<div class="branch-field">
|
||||
<label>Google Tag Manager (body / noscript)</label>
|
||||
<textarea name="dekart_counter_gtm_body" rows="5" style="width:100%;max-width:700px;font-family:monospace;font-size:13px"><?php echo esc_textarea(get_option('dekart_counter_gtm_body', '')); ?></textarea>
|
||||
<p class="desc">Код <code><noscript></code>. Выводится сразу после открытия <code><body></code> через хук <code>wp_body_open</code>.</p>
|
||||
</div>
|
||||
<div class="branch-field">
|
||||
<label>Произвольные скрипты (footer)</label>
|
||||
<textarea name="dekart_counter_footer" rows="8" style="width:100%;max-width:700px;font-family:monospace;font-size:13px"><?php echo esc_textarea(get_option('dekart_counter_footer', '')); ?></textarea>
|
||||
<p class="desc">Для чатов, пикселей и прочих скриптов. Выводится в <code><footer></code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Сворачивание/разворачивание блоков
|
||||
document.querySelectorAll('.branch-section > h2').forEach(function(h2){
|
||||
@ -4410,3 +4620,89 @@ function dekart_ajax_flush_cache()
|
||||
*/
|
||||
|
||||
|
||||
|
||||
// ════════════════════════════════════════════
|
||||
// Админ: вертикальные стабильные вкладки CF для преподавателя
|
||||
// (нативный layout 'tabbed-vertical' + страховка от «списка вместо вкладок»)
|
||||
// ════════════════════════════════════════════
|
||||
function dekart_admin_teacher_tab_assets($hook)
|
||||
{
|
||||
if (! in_array($hook, array( 'post.php', 'post-new.php' ), true)) {
|
||||
return;
|
||||
}
|
||||
$screen = get_current_screen();
|
||||
if (! $screen || 'teacher' !== (string) $screen->post_type) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'dekart-admin-teacher-tabs',
|
||||
get_template_directory_uri() . '/assets/css/admin-teacher-tabs.css',
|
||||
array( 'carbon-fields-core' ),
|
||||
filemtime(get_template_directory() . '/assets/css/admin-teacher-tabs.css')
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'dekart-admin-teacher-tabs',
|
||||
get_template_directory_uri() . '/assets/js/admin-teacher-tabs.js',
|
||||
array(),
|
||||
filemtime(get_template_directory() . '/assets/js/admin-teacher-tabs.js'),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Санитайзер каталога программ филиала (branch_programs).
|
||||
* Каждая запись: name, price, price_note, url, tags.
|
||||
*/
|
||||
function dekart_sanitize_branch_programs($value)
|
||||
{
|
||||
if (! is_array($value)) {
|
||||
return array();
|
||||
}
|
||||
$out = array();
|
||||
foreach ($value as $item) {
|
||||
if (! is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$name = trim(sanitize_text_field((string) ( $item['name'] ?? '' )));
|
||||
if ('' === $name) {
|
||||
continue; // пустая строка — пропускаем
|
||||
}
|
||||
$out[] = array(
|
||||
'name' => $name,
|
||||
'price' => trim(sanitize_text_field((string) ( $item['price'] ?? '' ))),
|
||||
'price_note' => trim(sanitize_text_field((string) ( $item['price_note'] ?? '' ))),
|
||||
'url' => esc_url_raw(trim((string) ( $item['url'] ?? '' ))),
|
||||
'tags' => trim(sanitize_text_field((string) ( $item['tags'] ?? '' ))),
|
||||
);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
add_action('admin_enqueue_scripts', 'dekart_admin_teacher_tab_assets');
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Админ: Настройки филиала — вертикальные вкладки
|
||||
// ─────────────────────────────────────────────
|
||||
function dekart_admin_branch_tab_assets($hook)
|
||||
{
|
||||
if (! isset($_GET['page']) || $_GET['page'] !== 'branch-settings') { // phpcs:ignore WordPress.Security.NonceVerification
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'dekart-admin-branch-tabs',
|
||||
get_template_directory_uri() . '/assets/css/admin-branch-tabs.css',
|
||||
array(),
|
||||
filemtime(get_template_directory() . '/assets/css/admin-branch-tabs.css')
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'dekart-admin-branch-tabs',
|
||||
get_template_directory_uri() . '/assets/js/admin-branch-tabs.js',
|
||||
array(),
|
||||
filemtime(get_template_directory() . '/assets/js/admin-branch-tabs.js'),
|
||||
true
|
||||
);
|
||||
}
|
||||
add_action('admin_enqueue_scripts', 'dekart_admin_branch_tab_assets');
|
||||
|
||||
@ -132,7 +132,7 @@ if (is_array($class_faq_items) && ! empty($class_faq_items)) {
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($graph)) : ?>
|
||||
if (count($graph) > 0) : ?>
|
||||
<script type="application/ld+json">
|
||||
<?php echo wp_json_encode(array('@context' => 'https://schema.org', '@graph' => $graph), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -16,12 +16,18 @@ if (! defined('ABSPATH')) {
|
||||
|
||||
$post_id = get_the_ID();
|
||||
|
||||
// ── Автор: Settings API → fallback ──
|
||||
$author_name = get_option('branch_director_name', 'Елена Сергеевна Дегтярёва')
|
||||
// ── Поддержка $args (переопределение автора, напр. для single-teacher) ──
|
||||
$args = isset($args) && is_array($args) ? $args : array();
|
||||
$args = wp_parse_args($args, array('author_name' => '', 'author_creds' => ''));
|
||||
|
||||
// ── Автор: $args → Settings API → fallback ──
|
||||
$author_name = $args['author_name']
|
||||
?: get_option('branch_director_name', 'Елена Сергеевна Дегтярёва')
|
||||
?: carbon_get_post_meta($post_id, 'pksh_author_name')
|
||||
?: get_option('branch_og_site_name', 'Частная школа Декарт');
|
||||
|
||||
$author_creds = get_option('branch_director_creds', 'директор частной школы «Декарт», педагогический стаж 18 лет');
|
||||
$author_creds = $args['author_creds']
|
||||
?: get_option('branch_director_creds', 'директор частной школы «Декарт», педагогический стаж 18 лет');
|
||||
|
||||
// ── Даты (автоматически из WP) ──
|
||||
$publish_date = $post_id ? get_the_date('c', $post_id) : '';
|
||||
|
||||
@ -79,7 +79,7 @@ if (! $hero_src) {
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (! empty($hero_stats)) : ?>
|
||||
<?php if (count($hero_stats) > 0) : ?>
|
||||
<ul class="class-hero__stats">
|
||||
<?php foreach ($hero_stats as $stat) :
|
||||
$num = isset($stat['stat_number']) ? trim($stat['stat_number']) : '';
|
||||
|
||||
@ -110,7 +110,7 @@ if (! empty($extra_msgs) && is_array($extra_msgs)) {
|
||||
|
||||
<p class="ds-hero__subtitle"><strong>Небольшие классы до 15 человек</strong>, опытные педагоги и углублённый английский с 1‑го класса. Учебные занятия — в первую половину дня (<strong>до 13:00</strong>). После обеда — продлёнка с досуговыми занятиями, прогулками и помощью с домашними заданиями (<strong>до 19:00</strong>).<br><br>Можно забрать ребёнка после уроков или оставить на продлёнку. В продлёнку входят обед, полдник, помощь с ДЗ и кружки. Индивидуальный подход к каждому ученику. Открыт набор на <span class="ds-hero__year"><?php echo esc_html($school_year); ?></span> учебный год.</p>
|
||||
|
||||
<?php if (! empty($floats)) : ?>
|
||||
<?php if (count($floats) > 0) : ?>
|
||||
<div class="ds-hero__mobile-usps">
|
||||
<?php foreach ($floats as $float) : ?>
|
||||
<span class="ds-hero__mobile-usp">
|
||||
|
||||
@ -1,39 +1,125 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Секция отзывов на главной странице
|
||||
* Показывает заголовок + 3 отзыва из CF reviews_cards (страница 91 /otzyvy/) + ссылка на все
|
||||
* Стиль карточек — как на /otzyvy/
|
||||
* Секция отзывов «как на главной» (bento, карточки reviews-front__*).
|
||||
*
|
||||
* Без $args['teacher'] — берёт 3 последних отзыва из CF reviews_cards (стр. 91 /otzyvy/).
|
||||
* С $args['teacher'] — показывает последние 3 отзыва CPT reviews, привязанных
|
||||
* к преподавателю (meta review_teacher + review_type school/primary). Если у
|
||||
* преподавателя отзывов нет — секция не выводится.
|
||||
*
|
||||
* @package Dekart
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
if (! defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$reviews_page_id = 91;
|
||||
$reviews_cards = carbon_get_post_meta($reviews_page_id, 'reviews_cards');
|
||||
$has_reviews = ! empty($reviews_cards) && is_array($reviews_cards);
|
||||
$teacher_filter = ! empty($args['teacher']) ? (int) $args['teacher'] : 0;
|
||||
|
||||
// Берём 3 последних (в CF они в обратном порядке, но на всякий случай переворачиваем)
|
||||
if ($has_reviews) {
|
||||
$reviews_cards = array_reverse($reviews_cards);
|
||||
$reviews_cards = array_slice($reviews_cards, 0, 3);
|
||||
$head_label = 'Отзывы';
|
||||
$head_title = 'Узнайте, что говорят о нас родители и ученики';
|
||||
$head_desc = 'Более 190 оценок на внешних площадках — средний балл 4,9 из 5';
|
||||
|
||||
if ($teacher_filter) {
|
||||
// ── Отзывы о конкретном преподавателе (CPT reviews) ──
|
||||
$teacher_query = new WP_Query(array(
|
||||
'post_type' => 'reviews',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
'fields' => 'ids',
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => 'review_teacher',
|
||||
'value' => $teacher_filter,
|
||||
'compare' => 'LIKE',
|
||||
),
|
||||
array(
|
||||
'key' => 'review_type',
|
||||
'value' => array( 'school', 'primary' ),
|
||||
'compare' => 'IN',
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
$teacher_ids = $teacher_query->posts ?: array();
|
||||
$has_reviews = ! empty($teacher_ids);
|
||||
$reviews_cards = array();
|
||||
|
||||
if ($has_reviews) {
|
||||
$ratings = array();
|
||||
foreach ($teacher_ids as $rid) {
|
||||
$star = (int) carbon_get_post_meta($rid, 'review_star');
|
||||
if ($star >= 1 && $star <= 5) {
|
||||
$ratings[] = $star;
|
||||
}
|
||||
}
|
||||
|
||||
// Последние 3 отзыва (как на главной).
|
||||
foreach (array_slice($teacher_ids, 0, 3) as $rid) {
|
||||
$avatar_id = carbon_get_post_meta($rid, 'review_avatar');
|
||||
$photo_url = '';
|
||||
if ($avatar_id && is_numeric($avatar_id)) {
|
||||
$photo_url = (string) wp_get_attachment_image_url((int) $avatar_id, 'full');
|
||||
}
|
||||
|
||||
$reviews_cards[] = array(
|
||||
'review_author' => get_the_title($rid),
|
||||
'review_rating' => (int) ( carbon_get_post_meta($rid, 'review_star') ?: 5 ),
|
||||
'review_text' => (string) carbon_get_post_meta($rid, 'review_text'),
|
||||
'review_date' => get_the_date('d.m.Y', $rid),
|
||||
'review_photo' => $photo_url,
|
||||
);
|
||||
}
|
||||
|
||||
$total_cnt = count($teacher_ids);
|
||||
if ($ratings) {
|
||||
$avg = number_format(array_sum($ratings) / count($ratings), 1, '.', ' ');
|
||||
// Русские формы множественного числа (1 отзыв / 2 отзыва / 5 отзывов).
|
||||
$n10 = $total_cnt % 10;
|
||||
$n100 = $total_cnt % 100;
|
||||
if ($n10 === 1 && $n100 !== 11) {
|
||||
$word = 'проверенный отзыв';
|
||||
} elseif (in_array($n10, array( 2, 3, 4 ), true) && ! in_array($n100, array( 12, 13, 14 ), true)) {
|
||||
$word = 'проверенных отзыва';
|
||||
} else {
|
||||
$word = 'проверенных отзывов';
|
||||
}
|
||||
$head_desc = 'Средняя оценка ' . $avg . ' из 5 · ' . $total_cnt . ' ' . $word;
|
||||
} else {
|
||||
$head_desc = 'Реальные отзывы родителей и учеников о занятиях';
|
||||
}
|
||||
$head_title = 'Отзывы о преподавателе';
|
||||
}
|
||||
|
||||
wp_reset_postdata();
|
||||
} else {
|
||||
// ── Отзывы школы: 3 карточки из CF страницы /otzyvy/ (по умолчанию) ──
|
||||
$reviews_page_id = 91;
|
||||
$reviews_cards = carbon_get_post_meta($reviews_page_id, 'reviews_cards');
|
||||
$has_reviews = ! empty($reviews_cards) && is_array($reviews_cards);
|
||||
|
||||
if ($has_reviews) {
|
||||
$reviews_cards = array_slice(array_reverse($reviews_cards), 0, 3);
|
||||
}
|
||||
}
|
||||
|
||||
if (! $has_reviews) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<section class="reviews-front-section reveal" id="reviews" aria-label="Отзывы">
|
||||
<div class="container">
|
||||
|
||||
|
||||
<div class="section-header--centered">
|
||||
<span class="bento-label">Отзывы</span>
|
||||
<h2 class="bento-h2">Узнайте, что говорят о нас родители и ученики</h2>
|
||||
<p class="bento-p">Более 190 оценок на внешних площадках — средний балл 4,9 из 5</p>
|
||||
<span class="bento-label"><?php echo esc_html($head_label); ?></span>
|
||||
<h2 class="bento-h2"><?php echo esc_html($head_title); ?></h2>
|
||||
<p class="bento-p"><?php echo esc_html($head_desc); ?></p>
|
||||
</div>
|
||||
|
||||
<?php if ($has_reviews) : ?>
|
||||
<div class="reviews-front__scroll-hint" aria-hidden="true">👆 Листайте отзывы →</div>
|
||||
<div class="reviews-front__wrap">
|
||||
<div class="reviews-front__grid">
|
||||
@ -91,8 +177,6 @@ if ($has_reviews) {
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php wp_reset_postdata(); ?>
|
||||
|
||||
<div class="reviews-front__all-link">
|
||||
<a href="/otzyvy/" class="reviews-link__btn">Читать все отзывы →</a>
|
||||
|
||||
@ -130,7 +130,7 @@ if (! empty($extra_msgs) && is_array($extra_msgs)) {
|
||||
?>
|
||||
<p class="ds-hero__subtitle"><?php echo wp_kses_post($hero_subtitle); ?><br><br>Открыт набор на <span class="ds-hero__year"><?php echo esc_html($school_year); ?></span> учебный год в 1–4 классы.</p>
|
||||
|
||||
<?php if (! empty($floats)) : ?>
|
||||
<?php if (count($floats) > 0) : ?>
|
||||
<div class="ds-hero__mobile-usps">
|
||||
<?php foreach ($floats as $float) : ?>
|
||||
<span class="ds-hero__mobile-usp">
|
||||
|
||||
@ -90,10 +90,10 @@ $teachers_query = new WP_Query($query_args);
|
||||
$name_first = $name_parts[0] ?? $t_name;
|
||||
$name_rest = $name_parts[1] ?? '';
|
||||
|
||||
// Description truncation.
|
||||
// Description truncation (~3 строки карточки; поле: до 140 знаков).
|
||||
$t_about_clean = $t_about ? wp_strip_all_tags($t_about) : '';
|
||||
if (mb_strlen($t_about_clean) > 200) {
|
||||
$t_about_clean = mb_substr($t_about_clean, 0, 200) . '…';
|
||||
if (mb_strlen($t_about_clean) > 150) {
|
||||
$t_about_clean = mb_substr($t_about_clean, 0, 150) . '…';
|
||||
}
|
||||
?>
|
||||
<div class="teachers-cards__item">
|
||||
|
||||
@ -77,6 +77,11 @@ $current_year = (int) current_time('Y');
|
||||
<div class="teachers-card__direction"><?php echo esc_html($direction); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php $t_desc = trim((string) carbon_get_post_meta($tid, 'teacher_description')); ?>
|
||||
<?php if ($t_desc !== '') : ?>
|
||||
<p class="teachers-card__about"><?php echo esc_html(wp_strip_all_tags($t_desc)); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($tenure) : ?>
|
||||
<div class="teachers-card__tenure">Стаж: <strong><?php echo esc_html($tenure); ?></strong> лет</div>
|
||||
<?php endif; ?>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user