Единый форум поддержки

Информация о пользователе

Привет, Гость! Войдите или зарегистрируйтесь.


Вы здесь » Единый форум поддержки » Вопросы по оформлению форума » Общие вопросы по оформлению (65) #2


Общие вопросы по оформлению (65) #2

Сообщений 1781 страница 1800 из 1813

1

В этой теме просим помощи в оформлении форума и его элементов

Столкнулись с трудностями в оформлении вашего форума? Или просто хотите придать ему новый, свежий вид? В этой теме вы можете задать вопросы по стилизации, дизайну и внешнему виду вашего форума. Получите помощь по работе с CSS, настройке цветовой гаммы, шрифтов, размещению изображений и другим элементам оформления.

Вопросы по скриптам и их работоспособности, просьба, задавать в теме Запросы по скриптам #11
Вы новичок и не разбираетесь во всём этом? Загляните в наш справочный раздел. Возможно, там уже содержится ответ на ваш вопрос. Или задайте свой вопрос в теме Общие вопросы от новичков (63) #3

Важная информация перед тем, как задать вопрос:

  • Ответы дают другие участники форума по собственному желанию.

  • Администрация не может гарантировать моментальное решение или абсолютную корректность каждого ответа.

  • Пожалуйста, уважайте чужое время и усилия других пользователей, старайтесь писать грамотно и доходчиво.

  • Будьте взаимовежливы: Грубое или требовательное отношение к участникам форума неприемлемо и может привести к отказу в помощи на всём форуме.

  • Все вопросы, не касающиеся оформления, будут удаляться!

Сформулируйте свой вопрос максимально подробно, чтобы повысить вероятность отклика помощи:

  • Укажите ссылку на форум: Ссылка на форум, с которым требуется помочь, позволит нам увидеть, понять контекст проблемы и предложить наиболее точное решение, не тратя время на догадки.

  • Опишите желаемый результат: Четко и конкретно объясните, чего вы хотите добиться. Например: "Мне нужно, чтобы картинка шапки была во всю ширину блока", или "Хочу увеличить размер шрифта в названиях тем". Чем детальнее вы опишете, тем проще будет помочь.

  • Сделайте скриншот и выделите элемент: Помогающему необходимо понимать с чем ему предстоит вам помочь. Укажите ссылки на схожее оформление (если вы где-то видели подобное). Это не обязательно, но значительно упростит задачу.

Если на ваш вопрос долго нет ответа, пожалуйста, воздержитесь от повторных сообщений или обвинений. Возможно, ваш вопрос оказался излишне сложным, вероятно требующий больше времени и индивидуального подхода, будьте готовы к тому, что участники могут предложить свои услуги на платной основе. Обсуждение условий и оплаты происходит между вами и участником (исполнителем) в личных сообщениях или иных средствах связи (но не в теме!). Администрация форума не несет никакой ответственности за подобные договоренности и их выполнение.

Инструменты для поиска и редактирования стиля (скриптов).

Как с помощью браузера можно определить элемент дизайна.

Каталог скриптов/CSS

Полезные скрипты, необходимые темы для новичков, а также ссылки на сайты рассказывающие что такое HTML и CSS.

Типовые Вопросы (ЧаВо)

Ответы на часто задаваемые вопросы.

Как задавать вопросы! Разница между стилем и скриптом. ( Советы)

Плюс к названию темы еще и Памятка.

Любой вопрос по оформлению/неполадкам сопровождайте ссылкой на форум, по которому возникли сложности.

+1

1781

Подскажите, не могу разобраться как настроить маску. Вроде все настроил, все значения выставил, как итог: на превью маска правильно сидит, но в сообщении не отображается.

скрин

Отредактировано ква (Чт, 27 Авг 2026 02:49:50)

0

1782

ква
По идее есть новый скрипт маски (то что у Вас старьё древнее) => https://mayak.f-rpg.me/viewtopic.php?id=160

+1

1783

Deff написал(а):

ква
По идее есть новый скрипт маски (то что у Вас старьё древнее) => https://mayak.f-rpg.me/viewtopic.php?id=160

благодарю!

0

1784

И снова здравствуйте! Появилась новая проблемка, при отправке нового сообщения ломается лз, и после перезагрузки страницы  встает на место!

скрин

0

1785

ква
Нужна ссылка на форум.
У вас стоят скрипты-костыли на самозаполнение хтмл-полей простыми юзерами?

+1

1786

ква
Ну я думаю Вы ЛЗ разрешили заполнять пользователю, а трансформируете текст в HTML в Лз скриптом, плюс разрешили отправку сообщений через аякс..

Вам либо запретить заполнение ЛЗ пользователем (чтобы HTML в ЛЗ выставлялся сам движком сервиса), либо запретить отправку постов аяксом

Отредактировано Deff (Пт, 28 Авг 2026 22:15:41)

+1

1787

Deff написал(а):

либо запретить отправку постов аяксом

Да можно скрипт под аякс подогнать, но я бы голосовал за отказ от него.
Как его ни фикси, всё равно огромная дыра в безопасности.

+1

1788

Добрый день! Увидел сегодня ваше сообщение в ТГ и вот на форуме тоже оно есть: Ко вниманию скриптёров: немного про блокировки от сервиса по IP
У нас такого на барсуке не наблюдается, но я на всякий случай решил прийти спросить хд

https://badger.rolevaya.ru/ - форум вот
скрипт 1, написанный с помощью дипсика: 
https://badger.rolevaya.ru/pages/kartoteka
скрипт 2:
https://badger.rolevaya.ru/pages/hronika_sobitii

в скриптах я ничего не понимаю хд

сам скрипты, которые были навоявлены нейросеткой:

скрипт 1
Код:
<div id="chronologyRoot">
    <div class="chr-top-bar">
        <div class="chr-search">
            <label>🔍 Поиск</label>
            <input type="text" id="chrSearchInput" placeholder="Поиск по участникам или названию...">
            <button id="chrResetBtn">Сбросить</button>
        </div>
        <div class="chr-filters">
            <label>📋 Фильтр:</label>
            <select id="chrStatusFilter">
                <option value="all">Все эпизоды</option>
                <option value="completed">Завершенные</option>
                <option value="deadline">Срок отписи</option>
                <option value="no-deadline">Без срока отписи</option>
            </select>
        </div>
        <div class="chr-admin-panel" style="display:none;">
            <button id="chrAddBtn" class="chr-admin-btn">➕ Добавить эпизод</button>
        </div>
    </div>

    <div id="chrEditForm" style="display:none;">
        <h3 id="chrFormTitle">Добавление эпизода</h3>
        <input type="hidden" id="chrKey">
        <div class="chr-form-row">
            <label>Дата (дд.мм.гггг или с "до н.э."):</label>
            <input type="text" id="chrDate" placeholder="01.01.2024 или 15.03.45 до н.э.">
        </div>
        <div class="chr-form-row">
            <label>Название эпизода:</label>
            <input type="text" id="chrTitle" placeholder="Название">
        </div>
        <div class="chr-form-row">
            <label>Ссылка на тему:</label>
            <input type="text" id="chrLink" placeholder="https://... или /viewtopic.php?id=123">
        </div>
        <div class="chr-form-row">
            <label>Участники:</label>
            <input type="text" id="chrParticipants" placeholder="Имя1, Имя2">
        </div>
        <div class="chr-form-row">
            <label>Вид (личный/сюжет/альтернатива):</label>
            <select id="chrType">
                <option value="личный">личный</option>
                <option value="сюжет">сюжет</option>
                <option value="альтернатива">альтернатива</option>
            </select>
        </div>
        <div class="chr-form-row">
            <label>Статус:</label>
            <select id="chrStatus">
                <option value="completed">Завершен</option>
                <option value="deadline">Срок отписи (180 дней)</option>
                <option value="no-deadline">Без срока</option>
            </select>
        </div>
        <div class="chr-form-row" id="chrStartDateRow" style="display:none;">
            <label>Дата начала (для дедлайна):</label>
            <input type="text" id="chrStartDate" placeholder="01.01.2024">
        </div>
        <div class="chr-form-row">
            <label>Кто пишет:</label>
            <input type="text" id="chrWriter" placeholder="Имя персонажа">
        </div>
        <div class="chr-form-row">
            <label>Текст статуса (для "без срока"):</label>
            <input type="text" id="chrStatusText" placeholder="без срока">
        </div>
        <div class="chr-form-buttons">
            <button id="chrSaveBtn" class="chr-admin-btn">💾 Сохранить</button>
            <button id="chrCancelBtn" class="chr-admin-btn">❌ Отмена</button>
        </div>
    </div>

    <div class="chr-table-wrapper">
        <table class="chr-table">
            <thead>
                <tr>
                    <th class="col-num">№</th>
                    <th class="col-date">Дата<br>(игровая)</th>
                    <th class="col-episode">Эпизод</th>
                    <th class="col-participants">Участники</th>
                    <th class="col-type">Вид</th>
                    <th class="col-status">Статус</th>
                    <th class="col-writer">Пишет</th>
                    <th class="col-actions" style="display:none;">⚙️</th>
                </tr>
            </thead>
            <tbody id="chrTableBody">
                <tr><td colspan="8" style="text-align:center">Загрузка эпизодов...</td></tr>
            </tbody>
        </table>
    </div>
</div>

<style>
#chronologyRoot {
    margin: 0px 10px 60px 10px;
    font-family: 'Raleway Medium', sans-serif;
    font-size: 14px;
    background: var(--forcol, #f5f0eb) !important;
    padding: 20px;
    border-radius: 8px;
}

#chronologyRoot .chr-top-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px;
    margin-bottom: 15px;
}

#chronologyRoot .chr-search {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    flex: 2;
}

#chronologyRoot .chr-search label {
    font: 14px 'Raleway Medium' !important;
    margin: 0 !important;
    color: #4a3c35 !important;
}

#chronologyRoot .chr-search input {
    padding: 6px 12px;
    width: 250px;
    background: var(--forcol, #f5f0eb) !important;
    border: 3px solid #8b847a !important;
    border-radius: 3px;
    font-family: 'Raleway Medium';
    font-size: 14px;
    color: #4a3c35;
}

#chronologyRoot .chr-search input::placeholder {
    color: #4a3c35;
    opacity: 0.7;
}

#chronologyRoot .chr-search button,
.chr-admin-btn {
    background: var(--forcol, #f5f0eb) !important;
    border-radius: 7px !important;
    border: 1px solid #46464638 !important;
    color: #322a26 !important;
    padding: 9px 10px 7px 10px !important;
    cursor: pointer !important;
    font-size: 11px !important;
    width: auto !important;
    font-weight: bolder !important;
    font-family: 'Raleway Medium';
}

#chronologyRoot .chr-filters {
    display: flex;
    align-items: center;
    gap: 10px;
    flex: 1;
    justify-content: center;
}

#chronologyRoot .chr-filters label {
    font: 14px 'Raleway Medium' !important;
    color: #4a3c35 !important;
}

#chronologyRoot .chr-filters select {
    padding: 6px 12px;
    background: var(--forcol, #f5f0eb) !important;
    border: 3px solid #8b847a !important;
    border-radius: 3px;
    font-family: 'Raleway Medium';
    font-size: 14px;
    color: #4a3c35;
}

.chr-admin-panel {
    display: flex;
    gap: 10px;
}

#chrEditForm {
    background: #e8e2da;
    padding: 15px;
    border-radius: 8px;
    margin-bottom: 15px;
    border: 2px solid #8b847a;
}

#chrEditForm h3 {
    margin-top: 0;
    color: #4a3c35;
    font-family: 'Raleway Medium';
}

.chr-form-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 10px;
    flex-wrap: wrap;
}

.chr-form-row label {
    width: 200px;
    color: #4a3c35;
    font-weight: bold;
}

.chr-form-row input,
.chr-form-row select {
    flex: 1;
    min-width: 200px;
    padding: 6px 12px;
    background: var(--forcol, #f5f0eb) !important;
    border: 2px solid #8b847a !important;
    border-radius: 3px;
    font-family: 'Raleway Medium';
    font-size: 14px;
    color: #4a3c35;
}

.chr-form-buttons {
    display: flex;
    gap: 10px;
    margin-top: 15px;
}

#chronologyRoot .chr-table-wrapper {
    max-height: 500px;
    overflow-y: auto;
    overflow-x: auto;
    border: none;
    margin-top: 5px;
    margin-bottom: 20px;
    position: relative;
}

#chronologyRoot .chr-table {
    width: 100%;
    min-width: 700px;
    border-collapse: collapse;
    font-family: 'Raleway Medium', sans-serif;
    font-size: 14px;
    background-color: transparent;
    border: 3px solid #8b847a;
    table-layout: fixed;
}

#chronologyRoot .chr-table th,
#chronologyRoot .chr-table td {
    border: none;
    padding: 10px 8px;
    text-align: left;
    vertical-align: top;
    word-wrap: break-word;
    overflow-wrap: break-word;
}

#chronologyRoot .chr-table tbody tr td {
    border-top: 3px solid #8b847a;
    border-bottom: 3px solid #8b847a;
}

#chronologyRoot .chr-table tbody tr:first-child td {
    border-top: none;
}

#chronologyRoot .chr-table tbody tr:last-child td {
    border-bottom: none;
}

#chronologyRoot .chr-table thead tr {
    position: sticky;
    top: 0;
    z-index: 20;
}

#chronologyRoot .chr-table thead th {
    background-color: #8c7f72 !important;
    border-bottom: 3px solid #8b847a;
    text-align: center !important;
    font-weight: bold !important;
    font-size: 14px !important;
    color: #4a3c35 !important;
    font-family: 'Raleway Medium', sans-serif;
    vertical-align: middle;
}

.col-num { width: 50px; }
.col-date { width: 80px; }
.col-episode { width: 140px; }
.col-participants { width: 120px; }
.col-type { width: 100px; }
.col-status { width: 100px; }
.col-writer { width: 120px; }
.col-actions { width: 60px; }

#chronologyRoot .chr-table td.center {
    text-align: center;
}

#chronologyRoot .chr-table .status-completed {
    font-weight: bold;
}

#chronologyRoot .chr-table .status-deadline {
    font-weight: bold;
}

#chronologyRoot .chr-table .status-no-deadline {
    font-weight: bold;
    color: #4a3c35;
}

#chronologyRoot .chr-table .deadline-date {
    font-weight: bold;
    font-style: italic;
}

.episode-link {
    color: #4a3c35;
    text-decoration: none;
    font-weight: bold;
    transition: all 0.2s ease;
    cursor: pointer;
    display: inline-block;
    word-break: break-word;
}

.episode-link:hover {
    color: #8c7f72;
    text-decoration: underline;
}

.writer-cell {
    white-space: normal;
    word-break: break-word;
}

.writer-name {
    font-weight: 500;
}

.first-letter {
    font-weight: bold;
    text-decoration: underline;
}

#chronologyRoot .chr-table td:nth-child(4) {
    word-break: break-word;
    white-space: normal;
}

.chr-table-wrapper::-webkit-scrollbar {
    width: 8px;
    height: 8px;
}

.chr-table-wrapper::-webkit-scrollbar-track {
    background: #e0dcd5;
    border-radius: 4px;
}

.chr-table-wrapper::-webkit-scrollbar-thumb {
    background: #8b847a;
    border-radius: 4px;
}

.chr-action-btn {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 16px;
    padding: 2px 5px;
}

.chr-action-btn:hover {
    opacity: 0.7;
}
</style>

<script>
// ==================== КОНФИГУРАЦИЯ ====================
var CHR_CONFIG = {
    apiUrl: location.origin + '/api.php',
    appId: 16777215,
    userId: 1,
    adminGroups: [1],
    allowedGroups: [1, 2, 5],
    keyPrefix: 'chr_ep_',
    debug: false
};

// ==================== ГЛОБАЛЬНЫЕ ДАННЫЕ ====================
var episodesData = [];
var isAdmin = false;
var apiToken = '';

// ==================== ТОКЕН ====================
function getToken() {
    if (typeof window !== 'undefined' && window.ForumAPITicket) return window.ForumAPITicket;
    if (typeof ForumAPITicket !== 'undefined') return ForumAPITicket;
    if (typeof guestTicket !== 'undefined') return guestTicket;
    if (typeof FORUM !== 'undefined' && FORUM.getSessionId) return FORUM.getSessionId();
    
    var cookies = document.cookie.split(';');
    for (var i = 0; i < cookies.length; i++) {
        var c = cookies[i].trim();
        if (c.indexOf('token=') === 0) return c.substring(6);
        if (c.indexOf('session=') === 0) return c.substring(8);
        if (c.indexOf('sid=') === 0) return c.substring(4);
    }
    
    if (typeof Storage !== 'undefined') {
        var ls = localStorage.getItem('token') || localStorage.getItem('session');
        if (ls) return ls;
    }
    
    return '';
}

function logDebug(msg) {
    if (!CHR_CONFIG.debug) return;
    console.log(msg);
}

// ==================== ПРОВЕРКА ПРАВ ====================
function checkPermissions() {
    var groupId = 3;
    if (typeof GroupID !== 'undefined') groupId = GroupID;
    else if (typeof UserGroup !== 'undefined') groupId = UserGroup;
    
    var hasAccess = CHR_CONFIG.allowedGroups.indexOf(groupId) !== -1;
    isAdmin = CHR_CONFIG.adminGroups.indexOf(groupId) !== -1;
    
    if (!hasAccess) {
        document.getElementById('chronologyRoot').innerHTML = 
            '<div style="text-align:center; padding:50px; color:#4a3c35;">' +
            '<h2>🔒 Доступ ограничен</h2>' +
            '<p>Хронология событий доступна только для принятых участников.</p>' +
            '</div>';
        return false;
    }
    return true;
}

// ==================== API ====================
function apiCall(method, params, callback, errorCallback) {
    var data = { 
        method: method,
        app_id: CHR_CONFIG.appId
    };
    
    apiToken = getToken();
    if (apiToken) {
        data.token = apiToken;
    }
    
    for (var key in params) {
        if (params.hasOwnProperty(key)) {
            data[key] = params[key];
        }
    }
    
    var queryString = serializeParams(data);
    var url = CHR_CONFIG.apiUrl + '?' + queryString;
    
    logDebug('API Request: ' + method + ' | token=' + (apiToken ? 'YES' : 'NO'));
    
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
    
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            logDebug('API Response ' + method + ': ' + xhr.status);
            
            if (xhr.status === 200) {
                try {
                    var response = JSON.parse(xhr.responseText);
                    logDebug('Response: ' + JSON.stringify(response).substring(0, 200));
                    callback(response);
                } catch (e) {
                    logDebug('Parse error: ' + e.message);
                    if (errorCallback) errorCallback('Parse error: ' + e.message);
                }
            } else {
                logDebug('HTTP error: ' + xhr.status);
                if (errorCallback) errorCallback('HTTP ' + xhr.status);
            }
        }
    };
    
    xhr.onerror = function() {
        logDebug('Network error');
        if (errorCallback) errorCallback('Network error');
    };
    
    xhr.send();
}

function serializeParams(obj) {
    var str = [];
    for (var p in obj) {
        if (obj.hasOwnProperty(p)) {
            str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
        }
    }
    return str.join("&");
}

// ==================== ПРОВЕРКА УСПЕХА API ====================
function isSuccess(response) {
    return response.response === "1" || 
           (response.response && response.response.storage && response.response.storage.success);
}

// ==================== ЗАГРУЗКА ДАННЫХ ====================
function loadData() {
    logDebug('=== ЗАГРУЗКА ДАННЫХ ===');
    
    apiCall('storage.keys', {}, function(response) {
        if (response.response && response.response.storage && response.response.storage.keys) {
            var allKeys = response.response.storage.keys;
            logDebug('Всего ключей в хранилище: ' + allKeys.length);
            
            var ourKeys = [];
            for (var i = 0; i < allKeys.length; i++) {
                if (allKeys[i].indexOf(CHR_CONFIG.keyPrefix) === 0) {
                    ourKeys.push(allKeys[i]);
                }
            }
            
            logDebug('Наши ключи (' + CHR_CONFIG.keyPrefix + '): ' + ourKeys.length);
            if (ourKeys.length > 0) {
                logDebug('Ключи: ' + ourKeys.join(', '));
            }
            
            if (ourKeys.length === 0) {
                episodesData = [];
                renderTable();
                return;
            }
            
            loadKeysData(ourKeys);
        } else if (response.error) {
            logDebug('Ошибка API: ' + JSON.stringify(response.error));
            episodesData = [];
            renderTable();
        } else {
            logDebug('Нет ключей в ответе');
            episodesData = [];
            renderTable();
        }
    }, function(err) {
        logDebug('Ошибка загрузки ключей: ' + err);
        document.getElementById('chrTableBody').innerHTML = 
            '<tr><td colspan="8" style="text-align:center">Ошибка: ' + err + '</td></tr>';
    });
}

function loadKeysData(keys) {
    var params = {};
    for (var i = 0; i < keys.length; i++) {
        params['key[' + i + ']'] = keys[i];
    }
    
    logDebug('Загрузка данных по ' + keys.length + ' ключам');
    
    apiCall('storage.get', params, function(response) {
        if (response.response && response.response.storage && response.response.storage.data) {
            var data = response.response.storage.data;
            logDebug('Получены данные, ключей: ' + Object.keys(data).length);
            
            episodesData = [];
            
            for (var key in data) {
                if (data.hasOwnProperty(key)) {
                    try {
                        var rawData = data[key];
                        var ep = JSON.parse(rawData);
                        ep._key = key;
                        episodesData.push(ep);
                    } catch(e) {
                        logDebug('Ошибка парсинга ключа ' + key + ': ' + e.message);
                    }
                }
            }
            
            logDebug('Загружено эпизодов: ' + episodesData.length);
            renderTable();
        } else if (response.error) {
            logDebug('Ошибка API: ' + JSON.stringify(response.error));
            episodesData = [];
            renderTable();
        } else {
            logDebug('Нет данных в ответе storage.get');
            episodesData = [];
            renderTable();
        }
    }, function(err) {
        logDebug('Ошибка загрузки данных: ' + err);
    });
}

// ==================== СОХРАНЕНИЕ ====================
function saveEpisode(episode, callback) {
    var key = episode._key;
    if (!key) {
        key = CHR_CONFIG.keyPrefix + Date.now() + '_' + Math.random().toString(36).substr(2, 6);
        episode._key = key;
    }
    
    var data = {
        date: episode.date,
        dateRaw: episode.dateRaw || parseDateForSort(episode.date),
        title: episode.title,
        link: episode.link || '',
        participants: episode.participants,
        type: episode.type || 'сюжет',
        status: episode.status || 'completed',
        startDate: episode.startDate || '',
        writer: episode.writer || '',
        statusText: episode.statusText || ''
    };
    
    var jsonData = JSON.stringify(data);
    logDebug('Сохранение: ' + key + ' | ' + jsonData.substring(0, 100));
    
    apiToken = getToken();
    if (!apiToken) {
        alert('Ошибка: не найден токен авторизации. Сохранение невозможно.');
        if (callback) callback(false);
        return;
    }
    
    apiCall('storage.set', {
        key: key, 
        value: jsonData
    }, function(response) {
        logDebug('Save response: ' + JSON.stringify(response).substring(0, 200));
        
        if (isSuccess(response)) {
            logDebug('Успешно сохранено!');
            if (callback) callback(true, key);
        } else if (response.error) {
            logDebug('Ошибка API: ' + JSON.stringify(response.error));
            if (callback) callback(false);
        } else {
            logDebug('Неизвестная ошибка в ответе');
            if (callback) callback(false);
        }
    }, function(err) {
        logDebug('Ошибка сохранения: ' + err);
        if (callback) callback(false);
    });
}

function deleteEpisode(key, callback) {
    logDebug('Удаление: ' + key);
    
    apiToken = getToken();
    if (!apiToken) {
        alert('Ошибка: не найден токен авторизации.');
        if (callback) callback(false);
        return;
    }
    
    apiCall('storage.delete', {key: key}, function(response) {
        logDebug('Delete response: ' + JSON.stringify(response));
        if (isSuccess(response)) {
            if (callback) callback(true);
        } else {
            if (callback) callback(false);
        }
    }, function(err) {
        logDebug('Ошибка удаления: ' + err);
        if (callback) callback(false);
    });
}

// ==================== ПАРСИНГ ДАТЫ ДЛЯ СОРТИРОВКИ ====================
function parseDateForSort(dateStr) {
    if (!dateStr || dateStr.trim() === "") return 0;
    
    var original = dateStr.trim();
    
    // Определяем, до н.э. или нет
    var isBC = false;
    var cleanStr = original;
    
    // Проверка на "до н.э."
    if (original.toLowerCase().indexOf('до н.э.') !== -1 || original.indexOf('bc') !== -1 || original.toLowerCase().indexOf('до нэ') !== -1) {
        isBC = true;
        cleanStr = original.toLowerCase().replace(/до н\.?э\.?/g, '').replace(/bc/gi, '').replace(/до нэ/g, '').trim();
    }
    
    // Проверка на минус (например, -45)
    if (cleanStr.indexOf('-') === 0) {
        isBC = true;
        cleanStr = cleanStr.substring(1);
    }
    
    // Извлекаем день, месяц, год из формата дд.мм.гггг или дд.мм.гг и т.п.
    var parts = cleanStr.match(/^(\d{1,2})\.(\d{1,2})\.(\d{1,4})/);
    if (!parts) return 0;
    
    var day = parseInt(parts[1]) || 1;
    var month = parseInt(parts[2]) || 1;
    var year = parseInt(parts[3]) || 0;
    
    if (isBC) {
        // Для до н.э. делаем год отрицательным
        return -(year * 10000 + month * 100 + day);
    } else {
        // Наша эра: обычное число
        return year * 10000 + month * 100 + day;
    }
}

// ==================== ВСПОМОГАТЕЛЬНЫЕ ====================
function getDateRaw(dateStr) {
    return parseDateForSort(dateStr);
}

function addDaysToDate(dateStr, days) {
    var parts = dateStr.split('.');
    if (parts.length !== 3) return dateStr;
    
    var date = new Date(parseInt(parts[2]), parseInt(parts[1]) - 1, parseInt(parts[0]));
    date.setDate(date.getDate() + days);
    
    var day = date.getDate();
    var month = date.getMonth() + 1;
    var year = date.getFullYear();
    return (day < 10 ? '0' + day : day) + '.' + (month < 10 ? '0' + month : month) + '.' + year;
}

function getEpisodeLink(ep) {
    if (ep.link && ep.link.trim() !== "") {
        var link = ep.link;
        if (!link.startsWith('http') && !link.startsWith('/')) link = '/' + link;
        var isExternal = link.startsWith('http://') || link.startsWith('https://');
        var targetAttr = isExternal ? ' target="_blank" rel="noopener noreferrer"' : '';
        return '<a href="' + link + '" class="episode-link"' + targetAttr + '>' + ep.title + '</a>';
    }
    return '<span class="episode-link" style="cursor: default; opacity: 0.7;">' + ep.title + '</span>';
}

function getSortValue(episode) {
    if (episode.dateRaw !== undefined && episode.dateRaw !== null && episode.dateRaw !== 0) {
        return episode.dateRaw;
    }
    return parseDateForSort(episode.date);
}

// ==================== РЕНДЕРИНГ ====================
function renderTable() {
    var tbody = document.getElementById('chrTableBody');
    if (!tbody) return;
    
    if (!episodesData || episodesData.length === 0) {
        tbody.innerHTML = '<tr><td colspan="8" style="text-align:center">Нет эпизодов</td></tr>';
        return;
    }
    
    var sorted = episodesData.slice().sort(function(a, b) {
        return getSortValue(a) - getSortValue(b);
    });
    
    var html = '';
    for (var i = 0; i < sorted.length; i++) {
        var ep = sorted[i];
        var num = i + 1;
        
        var statusHtml = '';
        if (ep.status === 'deadline' && ep.startDate) {
            var deadline = addDaysToDate(ep.startDate, 180);
            statusHtml = '<td class="status-deadline">⏰ срок до: <span class="deadline-date">' + deadline + '</span></td>';
        } else if (ep.status === 'no-deadline') {
            statusHtml = '<td class="status-no-deadline">🏖️ ' + (ep.statusText || 'без срока') + '</td>';
        } else {
            statusHtml = '<td class="status-completed">✨ завершен</td>';
        }
        
        var writerHtml = '—';
        if (ep.writer) {
            writerHtml = '📌 <span class="writer-name"><span class="first-letter">' + ep.writer.charAt(0) + '</span>' + ep.writer.slice(1) + '</span>';
        }
        
        var actions = '';
        if (isAdmin) {
            actions = '<td class="center">' +
                '<button class="chr-action-btn" onclick="editEpisode(\'' + ep._key + '\')" title="Редактировать">✏️</button>' +
                '<button class="chr-action-btn" onclick="deleteEpisodePrompt(\'' + ep._key + '\')" title="Удалить">🗑️</button>' +
                '</td>';
        }
        
        html += '<tr data-key="' + ep._key + '">';
        html += '<td class="center">' + num + '</td>';
        html += '<td>' + ep.date + '</td>';
        html += '<td>' + getEpisodeLink(ep) + '</td>';
        html += '<td>' + ep.participants + '</td>';
        html += '<td>' + (ep.type || '—') + '</td>';
        html += statusHtml;
        html += '<td class="writer-cell">' + writerHtml + '</td>';
        html += actions;
        html += '</tr>';
    }
    
    tbody.innerHTML = html;
    renumberRows();
}

function renumberRows() {
    var visible = document.querySelectorAll('#chrTableBody tr:not([style*="display: none"])');
    for (var j = 0; j < visible.length; j++) {
        visible[j].cells[0].textContent = j + 1;
    }
}

// ==================== ФИЛЬТРАЦИЯ (улучшенная) ====================
function filterTable() {
    var search = document.getElementById('chrSearchInput').value.toLowerCase().trim();
    var status = document.getElementById('chrStatusFilter').value;
    var rows = document.querySelectorAll('#chrTableBody tr');
    var hasSearch = search !== '';
    var matchedCount = 0;
    
    for (var i = 0; i < rows.length; i++) {
        var row = rows[i];
        var show = true;
        
        // Текст во всех ячейках (кроме действий)
        var rowText = '';
        for (var j = 0; j < row.cells.length; j++) {
            // Пропускаем последнюю ячейку (действия)
            if (j < row.cells.length - 1) {
                rowText += row.cells[j].textContent.toLowerCase() + ' ';
            }
        }
        
        // Поиск по участникам (4-я колонка) и названию эпизода (3-я колонка)
        var participants = row.cells[3] ? row.cells[3].textContent.toLowerCase() : '';
        var episodeTitle = row.cells[2] ? row.cells[2].textContent.toLowerCase() : '';
        
        if (hasSearch) {
            // Ищем в участниках или названии эпизода (можно добавить поиск по всем колонкам)
            var foundInParticipants = participants.indexOf(search) !== -1;
            var foundInTitle = episodeTitle.indexOf(search) !== -1;
            var foundInRow = rowText.indexOf(search) !== -1; // поиск по всем ячейкам
            
            show = foundInParticipants || foundInTitle || foundInRow;
        }
        
        // Фильтр по статусу
        if (show && status !== 'all') {
            var statusCell = row.cells[5] ? row.cells[5].textContent.toLowerCase() : '';
            if (status === 'completed') show = statusCell.indexOf('завершен') !== -1;
            else if (status === 'deadline') show = statusCell.indexOf('срок до:') !== -1;
            else if (status === 'no-deadline') show = statusCell.indexOf('без срока') !== -1;
        }
        
        row.style.display = show ? '' : 'none';
        if (show) matchedCount++;
    }
    
    // Обновляем нумерацию видимых строк
    renumberRows();
    
    // Показываем количество найденных
    var searchLabel = document.querySelector('.chr-search label');
    if (searchLabel && hasSearch) {
        searchLabel.textContent = '🔍 Найдено: ' + matchedCount;
        setTimeout(function() {
            searchLabel.textContent = '🔍 Поиск';
        }, 2000);
    }
}

// ==================== ФОРМА ====================
function showEditForm(episode) {
    var isNew = !episode;
    document.getElementById('chrFormTitle').textContent = isNew ? 'Добавление эпизода' : 'Редактирование эпизода';
    document.getElementById('chrKey').value = episode ? episode._key : '';
    document.getElementById('chrDate').value = episode ? episode.date : '';
    document.getElementById('chrTitle').value = episode ? episode.title : '';
    document.getElementById('chrLink').value = episode ? (episode.link || '') : '';
    document.getElementById('chrParticipants').value = episode ? episode.participants : '';
    document.getElementById('chrType').value = episode ? (episode.type || 'сюжет') : 'сюжет';
    document.getElementById('chrStatus').value = episode ? (episode.status || 'completed') : 'completed';
    document.getElementById('chrStartDate').value = episode ? (episode.startDate || '') : '';
    document.getElementById('chrWriter').value = episode ? (episode.writer || '') : '';
    document.getElementById('chrStatusText').value = episode ? (episode.statusText || '') : '';
    
    toggleStartDateRow();
    document.getElementById('chrEditForm').style.display = 'block';
    document.getElementById('chrEditForm').scrollIntoView({behavior: 'smooth'});
}

function hideEditForm() {
    document.getElementById('chrEditForm').style.display = 'none';
}

function toggleStartDateRow() {
    var status = document.getElementById('chrStatus').value;
    document.getElementById('chrStartDateRow').style.display = (status === 'deadline') ? 'flex' : 'none';
}

function collectFormData() {
    var dateStr = document.getElementById('chrDate').value;
    return {
        _key: document.getElementById('chrKey').value,
        date: dateStr,
        dateRaw: parseDateForSort(dateStr),
        title: document.getElementById('chrTitle').value,
        link: document.getElementById('chrLink').value,
        participants: document.getElementById('chrParticipants').value,
        type: document.getElementById('chrType').value,
        status: document.getElementById('chrStatus').value,
        startDate: document.getElementById('chrStartDate').value,
        writer: document.getElementById('chrWriter').value,
        statusText: document.getElementById('chrStatusText').value
    };
}

function validateForm() {
    var date = document.getElementById('chrDate').value;
    if (!date || date.trim() === "") {
        alert('Введите дату');
        return false;
    }
    // Проверяем, что хотя бы формат дд.мм.гггг (свободный) соблюдён
    var parts = date.match(/^(\d{1,2})\.(\d{1,2})\.(\d{1,4})/);
    if (!parts) {
        alert('Дата должна быть в формате дд.мм.гггг (например, 01.01.2024) или с указанием "до н.э." (например, 15.03.45 до н.э.)');
        return false;
    }
    if (!document.getElementById('chrTitle').value.trim()) {
        alert('Введите название эпизода');
        return false;
    }
    return true;
}

function saveForm() {
    if (!validateForm()) return;
    
    var ep = collectFormData();
    var isNew = !ep._key;
    
    logDebug('=== СОХРАНЕНИЕ === ' + (isNew ? 'новый' : 'обновление'));
    logDebug('Date: ' + ep.date + ' | dateRaw: ' + ep.dateRaw);
    
    saveEpisode(ep, function(success, key) {
        if (success) {
            if (isNew) {
                episodesData.push(ep);
            } else {
                for (var i = 0; i < episodesData.length; i++) {
                    if (episodesData[i]._key === ep._key) {
                        episodesData[i] = ep;
                        break;
                    }
                }
            }
            renderTable();
            hideEditForm();
            alert('Эпизод сохранен!');
        } else {
            alert('Ошибка сохранения! Проверьте debug-информацию.');
        }
    });
}

function deleteEpisodePrompt(key) {
    if (!confirm('Удалить этот эпизод?')) return;
    deleteEpisode(key, function(success) {
        if (success) {
            episodesData = episodesData.filter(function(ep) { return ep._key !== key; });
            renderTable();
            alert('Эпизод удален!');
        } else {
            alert('Ошибка удаления');
        }
    });
}

function editEpisode(key) {
    var ep = null;
    for (var i = 0; i < episodesData.length; i++) {
        if (episodesData[i]._key === key) {
            ep = episodesData[i];
            break;
        }
    }
    if (ep) showEditForm(ep);
}

// ==================== ИНИЦИАЛИЗАЦИЯ ====================
function init() {
    if (!checkPermissions()) return;
    
    apiToken = getToken();
    logDebug('Token: ' + (apiToken ? 'найден (' + apiToken.substring(0, 20) + '...)' : 'НЕ НАЙДЕН'));
    
    if (isAdmin) {
        document.querySelector('.chr-admin-panel').style.display = 'flex';
        var actionsHeader = document.querySelector('.col-actions');
        if (actionsHeader) actionsHeader.style.display = 'table-cell';
    }
    
    document.getElementById('chrAddBtn').addEventListener('click', function() { showEditForm(null); });
    document.getElementById('chrSaveBtn').addEventListener('click', saveForm);
    document.getElementById('chrCancelBtn').addEventListener('click', hideEditForm);
    document.getElementById('chrStatus').addEventListener('change', toggleStartDateRow);
    
    var searchInput = document.getElementById('chrSearchInput');
    var resetBtn = document.getElementById('chrResetBtn');
    var statusFilter = document.getElementById('chrStatusFilter');
    
    if (searchInput) searchInput.addEventListener('keyup', filterTable);
    if (resetBtn) resetBtn.addEventListener('click', function() { 
        if (searchInput) searchInput.value = ''; 
        if (statusFilter) statusFilter.value = 'all';
        filterTable(); 
    });
    if (statusFilter) statusFilter.addEventListener('change', filterTable);
    
    loadData();
}

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
} else {
    init();
}
</script>
скрипт 2
Код:
<div id="personnelRoot">
    <div class="prs-top-bar">
        <div class="prs-search">
            <label>🔍 Поиск по персонажам</label>
            <input type="text" id="prsSearchInput" placeholder="Введите имя или внешность...">
            <button id="prsResetBtn">Сбросить</button>
        </div>
        <div class="prs-filters">
            <label>📋 Фильтр:</label>
            <select id="prsStatusFilter">
                <option value="all">Все персонажи</option>
                <option value="alive">Живые</option>
                <option value="deceased">Мёртвые</option>
            </select>
        </div>
        <div class="prs-admin-panel" style="display:none;">
            <button id="prsAddBtn" class="prs-admin-btn">➕ Добавить персонажа</button>
        </div>
    </div>

    <div id="prsEditForm" style="display:none;">
        <h3 id="prsFormTitle">Добавление персонажа</h3>
        <input type="hidden" id="prsKey">
        <div class="prs-form-row">
            <label>Номер:</label>
            <input type="text" id="prsNumber" placeholder="1">
        </div>
        <div class="prs-form-row">
            <label>Фото (URL):</label>
            <input type="text" id="prsPhoto" placeholder="https://...">
        </div>
        <div class="prs-form-row">
            <label>Имя Фамилия / Прозвище:</label>
            <input type="text" id="prsName" placeholder="Иван Иванов">
        </div>
        <div class="prs-form-row">
            <label>Ссылка на анкету:</label>
            <input type="text" id="prsLink" placeholder="https://... или /viewtopic.php?id=123">
        </div>
        <div class="prs-form-row">
            <label>Возраст:</label>
            <input type="text" id="prsAge" placeholder="1990 г.р.">
        </div>
        <div class="prs-form-row">
            <label>Дата смерти (оставить пустым если жив):</label>
            <input type="text" id="prsDeathDate" placeholder="01.01.2024">
        </div>
        <div class="prs-form-row">
            <label>Вид (раса/тип):</label>
            <input type="text" id="prsType" placeholder="человек">
        </div>
        <div class="prs-form-row">
            <label>Местоположение:</label>
            <input type="text" id="prsLocation" placeholder="Нью-Йорк">
        </div>
        <div class="prs-form-row">
            <label>Род деятельности:</label>
            <input type="text" id="prsOccupation" placeholder="детектив">
        </div>
        <div class="prs-form-row">
            <label>Внешность:</label>
            <input type="text" id="prsAppearance" placeholder="актер/актриса">
        </div>
        <div class="prs-form-buttons">
            <button id="prsSaveBtn" class="prs-admin-btn">💾 Сохранить</button>
            <button id="prsCancelBtn" class="prs-admin-btn">❌ Отмена</button>
        </div>
    </div>

    <div class="prs-table-wrapper">
        <table class="prs-table">
            <thead>
                <tr>
                    <th class="col-num">№</th>
                    <th class="col-photo">Фото</th>
                    <th class="col-name">Имя Фамилия / Прозвище</th>
                    <th class="col-age">Возраст</th>
                    <th class="col-type">Вид</th>
                    <th class="col-info">Местоположение и род деятельности</th>
                    <th class="col-appearance">Внешность</th>
                    <th class="col-actions" style="display:none;">⚙️</th>
                </tr>
            </thead>
            <tbody id="prsTableBody">
                <tr><td colspan="8" style="text-align:center">Загрузка персонажей...</td></tr>
            </tbody>
        </table>
    </div>
</div>

<style>
#personnelRoot {
    margin: 0px 10px 60px 10px;
    font-family: 'Raleway Medium', sans-serif;
    font-size: 14px;
    background: var(--forcol, #f5f0eb) !important;
    padding: 20px;
    border-radius: 8px;
}

#personnelRoot .prs-top-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px;
    margin-bottom: 15px;
}

#personnelRoot .prs-search {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    flex: 2;
}

#personnelRoot .prs-search label {
    font: 14px 'Raleway Medium' !important;
    margin: 0 !important;
    color: #4a3c35 !important;
}

#personnelRoot .prs-search input {
    padding: 6px 12px;
    width: 250px;
    background: var(--forcol, #f5f0eb) !important;
    border: 3px solid #8b847a !important;
    border-radius: 3px;
    font-family: 'Raleway Medium';
    font-size: 14px;
    color: #4a3c35;
}

#personnelRoot .prs-search input::placeholder {
    color: #4a3c35;
    opacity: 0.7;
}

#personnelRoot .prs-search button,
.prs-admin-btn {
    background: var(--forcol, #f5f0eb) !important;
    border-radius: 7px !important;
    border: 1px solid #46464638 !important;
    color: #322a26 !important;
    padding: 9px 10px 7px 10px !important;
    cursor: pointer !important;
    font-size: 11px !important;
    width: auto !important;
    font-weight: bolder !important;
    font-family: 'Raleway Medium';
}

#personnelRoot .prs-filters {
    display: flex;
    align-items: center;
    gap: 10px;
    flex: 1;
    justify-content: center;
}

#personnelRoot .prs-filters label {
    font: 14px 'Raleway Medium' !important;
    color: #4a3c35 !important;
}

#personnelRoot .prs-filters select {
    padding: 6px 12px;
    background: var(--forcol, #f5f0eb) !important;
    border: 3px solid #8b847a !important;
    border-radius: 3px;
    font-family: 'Raleway Medium';
    font-size: 14px;
    color: #4a3c35;
}

.prs-admin-panel {
    display: flex;
    gap: 10px;
}

#prsEditForm {
    background: #e8e2da;
    padding: 15px;
    border-radius: 8px;
    margin-bottom: 15px;
    border: 2px solid #8b847a;
}

#prsEditForm h3 {
    margin-top: 0;
    color: #4a3c35;
    font-family: 'Raleway Medium';
}

.prs-form-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 10px;
    flex-wrap: wrap;
}

.prs-form-row label {
    width: 250px;
    color: #4a3c35;
    font-weight: bold;
}

.prs-form-row input,
.prs-form-row select {
    flex: 1;
    min-width: 200px;
    padding: 6px 12px;
    background: var(--forcol, #f5f0eb) !important;
    border: 2px solid #8b847a !important;
    border-radius: 3px;
    font-family: 'Raleway Medium';
    font-size: 14px;
    color: #4a3c35;
}

.prs-form-buttons {
    display: flex;
    gap: 10px;
    margin-top: 15px;
}

#personnelRoot .prs-table-wrapper {
    max-height: 500px;
    overflow-y: auto;
    overflow-x: auto;
    border: none;
    margin-top: 5px;
    margin-bottom: 20px;
    position: relative;
}

#personnelRoot .prs-table {
    width: 100%;
    min-width: 800px;
    border-collapse: collapse;
    font-family: 'Raleway Medium', sans-serif;
    font-size: 14px;
    background-color: transparent;
    border: 3px solid #8b847a;
    table-layout: fixed;
}

#personnelRoot .prs-table th,
#personnelRoot .prs-table td {
    border: none;
    padding: 10px 8px;
    vertical-align: middle;
    word-wrap: break-word;
    overflow-wrap: break-word;
}

#personnelRoot .prs-table th:not(:nth-child(3)):not(:nth-child(6)),
#personnelRoot .prs-table td:not(:nth-child(3)):not(:nth-child(6)) {
    text-align: center;
}

#personnelRoot .prs-table th:nth-child(3),
#personnelRoot .prs-table td:nth-child(3) {
    text-align: left;
}

#personnelRoot .prs-table th:nth-child(6),
#personnelRoot .prs-table td:nth-child(6) {
    text-align: left;
}

#personnelRoot .prs-table tbody tr td {
    border-top: 3px solid #8b847a;
    border-bottom: 3px solid #8b847a;
}

#personnelRoot .prs-table tbody tr:first-child td {
    border-top: none;
}

#personnelRoot .prs-table tbody tr:last-child td {
    border-bottom: none;
}

#personnelRoot .prs-table thead tr {
    position: sticky;
    top: 0;
    z-index: 20;
}

#personnelRoot .prs-table thead th {
    background-color: #8c7f72 !important;
    border-bottom: 3px solid #8b847a;
    text-align: center !important;
    font-weight: bold !important;
    font-size: 14px !important;
    color: #4a3c35 !important;
    font-family: 'Raleway Medium', sans-serif;
    vertical-align: middle;
}

/* Ширина колонок */
.col-num { width: 40px; }
.col-photo { width: 90px; }
.col-name { width: 120px; }
.col-age { width: 100px; }
.col-type { width: 120px; }
.col-info { width: 140px; }
.col-appearance { width: 90px; }
.col-actions { width: 60px; }

/* Первая буква жирная и подчёркнутая */
.first-letter {
    font-weight: bold;
    text-decoration: underline;
}

.character-photo {
    width: 65px;
    height: 85px;
    object-fit: cover;
    border-radius: 4px;
    border: 2px solid #D6CFC4;
    box-shadow: 0 4px 12px rgba(0,0,0,0.2);
    background: #D6CFC4;
    padding: 4px 4px 6px 4px;
    transition: all 0.4s ease;
    display: block;
    margin: 0 auto;
    filter: sepia(0.5) hue-rotate(-5deg) brightness(0.96) contrast(1.08) saturate(1.2);
    clip-path: polygon(
        0% 3%, 2% 0%, 98% 0%, 100% 2%, 100% 5%, 99% 8%, 100% 12%, 98% 15%,
        99% 20%, 100% 25%, 98% 28%, 99% 32%, 100% 35%, 98% 40%, 99% 44%, 100% 48%,
        98% 52%, 99% 56%, 100% 60%, 98% 65%, 99% 70%, 100% 75%, 98% 80%, 99% 85%,
        100% 90%, 98% 94%, 99% 97%, 100% 100%, 97% 100%, 94% 98%, 90% 100%, 85% 99%,
        80% 100%, 75% 98%, 70% 99%, 65% 100%, 60% 98%, 55% 99%, 50% 100%, 45% 98%,
        40% 99%, 35% 100%, 30% 98%, 25% 99%, 20% 100%, 15% 98%, 10% 99%, 8% 100%,
        5% 98%, 3% 100%, 0% 97%
    );
}

.character-photo-left {
    transform: rotate(-3deg);
}

.character-photo-right {
    transform: rotate(3deg);
}

.character-photo-left:hover {
    filter: sepia(0) hue-rotate(0) brightness(1) contrast(1) saturate(1);
    transform: rotate(-5deg) scale(1.05);
}

.character-photo-right:hover {
    filter: sepia(0) hue-rotate(0) brightness(1) contrast(1) saturate(1);
    transform: rotate(5deg) scale(1.05);
}

.character-name-link {
    color: #4a3c35;
    text-decoration: none;
    font-weight: bold;
    transition: all 0.2s ease;
    cursor: pointer;
}

.character-name-link:hover {
    color: #8c7f72;
    text-decoration: underline;
}

.death-date {
    color: #4a3c35;
    font-style: italic;
    font-weight: bold;
}

.character-age {
    white-space: normal;
    word-break: keep-all;
}

.prs-action-btn {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 16px;
    padding: 2px 5px;
}

.prs-action-btn:hover {
    opacity: 0.7;
}

.prs-table-wrapper::-webkit-scrollbar {
    width: 8px;
    height: 8px;
}

.prs-table-wrapper::-webkit-scrollbar-track {
    background: #e0dcd5;
    border-radius: 4px;
}

.prs-table-wrapper::-webkit-scrollbar-thumb {
    background: #8b847a;
    border-radius: 4px;
}
</style>

<script>
// ==================== КОНФИГУРАЦИЯ ====================
var PRS_CONFIG = {
    apiUrl: location.origin + '/api.php',
    appId: 16777215,
    userId: 1,
    adminGroups: [1],
    allowedGroups: [1, 2, 3, 4, 5, 6, 7], // ВСЕ группы могут видеть
    keyPrefix: 'chr_prs_',
    debug: false
};

// ==================== ГЛОБАЛЬНЫЕ ДАННЫЕ ====================
var charactersData = [];
var isAdmin = false;
var apiToken = '';

// ==================== ТОКЕН ====================
function getToken() {
    if (typeof window !== 'undefined' && window.ForumAPITicket) return window.ForumAPITicket;
    if (typeof ForumAPITicket !== 'undefined') return ForumAPITicket;
    if (typeof guestTicket !== 'undefined') return guestTicket;
    if (typeof FORUM !== 'undefined' && FORUM.getSessionId) return FORUM.getSessionId();
    
    var cookies = document.cookie.split(';');
    for (var i = 0; i < cookies.length; i++) {
        var c = cookies[i].trim();
        if (c.indexOf('token=') === 0) return c.substring(6);
        if (c.indexOf('session=') === 0) return c.substring(8);
        if (c.indexOf('sid=') === 0) return c.substring(4);
    }
    
    if (typeof Storage !== 'undefined') {
        var ls = localStorage.getItem('token') || localStorage.getItem('session');
        if (ls) return ls;
    }
    
    return '';
}

function logDebug(msg) {
    if (!PRS_CONFIG.debug) return;
    console.log(msg);
}

// ==================== ПРОВЕРКА ПРАВ ====================
function checkPermissions() {
    var groupId = 3;
    if (typeof GroupID !== 'undefined') groupId = GroupID;
    else if (typeof UserGroup !== 'undefined') groupId = UserGroup;
    
    // ВСЕ пользователи (включая гостей) могут видеть картотеку
    // Гости обычно GroupID = 3 или undefined, но мы разрешаем всем
    var hasAccess = true; // Доступ открыт для всех
    
    // Админы могут редактировать
    isAdmin = PRS_CONFIG.adminGroups.indexOf(groupId) !== -1;
    
    return true; // Всегда разрешаем доступ
}

// ==================== API ====================
function apiCall(method, params, callback, errorCallback) {
    var data = { 
        method: method,
        app_id: PRS_CONFIG.appId
    };
    
    apiToken = getToken();
    if (apiToken) {
        data.token = apiToken;
    }
    
    for (var key in params) {
        if (params.hasOwnProperty(key)) {
            data[key] = params[key];
        }
    }
    
    var queryString = serializeParams(data);
    var url = PRS_CONFIG.apiUrl + '?' + queryString;
    
    logDebug('API Request: ' + method + ' | token=' + (apiToken ? 'YES' : 'NO'));
    
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
    
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            logDebug('API Response ' + method + ': ' + xhr.status);
            
            if (xhr.status === 200) {
                try {
                    var response = JSON.parse(xhr.responseText);
                    logDebug('Response: ' + JSON.stringify(response).substring(0, 200));
                    callback(response);
                } catch (e) {
                    logDebug('Parse error: ' + e.message);
                    if (errorCallback) errorCallback('Parse error: ' + e.message);
                }
            } else {
                logDebug('HTTP error: ' + xhr.status);
                if (errorCallback) errorCallback('HTTP ' + xhr.status);
            }
        }
    };
    
    xhr.onerror = function() {
        logDebug('Network error');
        if (errorCallback) errorCallback('Network error');
    };
    
    xhr.send();
}

function serializeParams(obj) {
    var str = [];
    for (var p in obj) {
        if (obj.hasOwnProperty(p)) {
            str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
        }
    }
    return str.join("&");
}

function isSuccess(response) {
    return response.response === "1" || 
           (response.response && response.response.storage && response.response.storage.success);
}

// ==================== ЗАГРУЗКА ДАННЫХ ====================
function loadData() {
    logDebug('=== ЗАГРУЗКА ДАННЫХ ===');
    
    apiCall('storage.keys', {}, function(response) {
        if (response.response && response.response.storage && response.response.storage.keys) {
            var allKeys = response.response.storage.keys;
            logDebug('Всего ключей: ' + allKeys.length);
            
            var ourKeys = [];
            for (var i = 0; i < allKeys.length; i++) {
                if (allKeys[i].indexOf(PRS_CONFIG.keyPrefix) === 0) {
                    ourKeys.push(allKeys[i]);
                }
            }
            
            logDebug('Наши ключи: ' + ourKeys.length);
            
            if (ourKeys.length === 0) {
                charactersData = [];
                renderTable();
                return;
            }
            
            loadKeysData(ourKeys);
        } else {
            charactersData = [];
            renderTable();
        }
    }, function(err) {
        document.getElementById('prsTableBody').innerHTML = 
            '<tr><td colspan="8" style="text-align:center">Ошибка: ' + err + '</td></tr>';
    });
}

function loadKeysData(keys) {
    var params = {};
    for (var i = 0; i < keys.length; i++) {
        params['key[' + i + ']'] = keys[i];
    }
    
    apiCall('storage.get', params, function(response) {
        if (response.response && response.response.storage && response.response.storage.data) {
            var data = response.response.storage.data;
            
            charactersData = [];
            
            for (var key in data) {
                if (data.hasOwnProperty(key)) {
                    try {
                        var rawData = data[key];
                        var char = JSON.parse(rawData);
                        char._key = key;
                        charactersData.push(char);
                    } catch(e) {
                        logDebug('Ошибка парсинга: ' + e.message);
                    }
                }
            }
            
            logDebug('Загружено персонажей: ' + charactersData.length);
            renderTable();
        } else {
            charactersData = [];
            renderTable();
        }
    }, function(err) {
        logDebug('Ошибка загрузки: ' + err);
    });
}

// ==================== СОХРАНЕНИЕ ====================
function saveCharacter(char, callback) {
    var key = char._key;
    if (!key) {
        key = PRS_CONFIG.keyPrefix + Date.now() + '_' + Math.random().toString(36).substr(2, 6);
        char._key = key;
    }
    
    var data = {
        number: parseInt(char.number) || 0,
        photo: char.photo || '',
        name: char.name || '',
        link: char.link || '',
        age: char.age || '',
        deathDate: char.deathDate || '',
        type: char.type || '',
        location: char.location || '',
        occupation: char.occupation || '',
        appearance: char.appearance || ''
    };
    
    var jsonData = JSON.stringify(data);
    logDebug('Сохранение: ' + key);
    
    apiToken = getToken();
    if (!apiToken) {
        alert('Ошибка: не найден токен авторизации.');
        if (callback) callback(false);
        return;
    }
    
    apiCall('storage.set', {
        key: key, 
        value: jsonData
    }, function(response) {
        if (isSuccess(response)) {
            logDebug('Успешно сохранено!');
            if (callback) callback(true, key);
        } else {
            logDebug('Ошибка сохранения');
            if (callback) callback(false);
        }
    }, function(err) {
        if (callback) callback(false);
    });
}

function deleteCharacter(key, callback) {
    logDebug('Удаление: ' + key);
    
    apiToken = getToken();
    if (!apiToken) {
        alert('Ошибка: не найден токен авторизации.');
        if (callback) callback(false);
        return;
    }
    
    apiCall('storage.delete', {key: key}, function(response) {
        if (isSuccess(response)) {
            if (callback) callback(true);
        } else {
            if (callback) callback(false);
        }
    }, function(err) {
        if (callback) callback(false);
    });
}

// ==================== ВСПОМОГАТЕЛЬНЫЕ ====================
function getCharacterLink(char) {
    if (char.link && char.link.trim() !== "") {
        var link = char.link;
        if (!link.startsWith('http') && !link.startsWith('/')) link = '/' + link;
        var isExternal = link.startsWith('http://') || link.startsWith('https://');
        var targetAttr = isExternal ? ' target="_blank" rel="noopener noreferrer"' : '';
        return '<a href="' + link + '" class="character-name-link"' + targetAttr + '>' + char.name + '</a>';
    }
    return '<span class="character-name-link" style="cursor: default;">' + char.name + '</span>';
}

function getAgeDisplay(char) {
    if (char.deathDate && char.deathDate.trim() !== "") {
        return '<span class="death-date">⚰️ ' + char.deathDate + '</span>';
    }
    return char.age || '—';
}

// ==================== РЕНДЕРИНГ ====================
function renderTable() {
    var tbody = document.getElementById('prsTableBody');
    if (!tbody) return;
    
    if (!charactersData || charactersData.length === 0) {
        tbody.innerHTML = '<tr><td colspan="8" style="text-align:center">Нет персонажей</td></tr>';
        return;
    }
    
    // Сортируем по введённому номеру (number)
    var sorted = charactersData.slice().sort(function(a, b) {
        return (parseInt(a.number) || 0) - (parseInt(b.number) || 0);
    });
    
    var filterValue = document.getElementById('prsStatusFilter').value;
    var filtered = sorted;
    
    if (filterValue === 'deceased') {
        filtered = sorted.filter(function(char) {
            return char.deathDate && char.deathDate.trim() !== "";
        });
    } else if (filterValue === 'alive') {
        filtered = sorted.filter(function(char) {
            return !char.deathDate || char.deathDate.trim() === "";
        });
    }
    
    var html = '';
    for (var i = 0; i < filtered.length; i++) {
        var char = filtered[i];
        
        // ПЕРВАЯ БУКВА ЖИРНАЯ И ПОДЧЁРКНУТАЯ
        var infoText = [];
        if (char.location) {
            var locFirst = char.location.charAt(0);
            var locRest = char.location.slice(1);
            infoText.push('📍 <span class="first-letter">' + locFirst + '</span>' + locRest);
        }
        if (char.occupation) {
            var occFirst = char.occupation.charAt(0);
            var occRest = char.occupation.slice(1);
            infoText.push('💼 <span class="first-letter">' + occFirst + '</span>' + occRest);
        }
        var infoHtml = infoText.length > 0 ? infoText.join('<br>') : '—';
        
        var rotateClass = (i % 2 === 0) ? 'character-photo-left' : 'character-photo-right';
        var ageDisplay = getAgeDisplay(char);
        
        var actions = '';
        if (isAdmin) {
            actions = '<td class="center">' +
                '<button class="prs-action-btn" onclick="editCharacter(\'' + char._key + '\')" title="Редактировать">✏️</button>' +
                '<button class="prs-action-btn" onclick="deleteCharacterPrompt(\'' + char._key + '\')" title="Удалить">🗑️</button>' +
                '</td>';
        }
        
        html += '<tr>';
        // Показываем ВВЕДЁННЫЙ номер, а не порядковый
        html += '<td class="center">' + (char.number || '—') + '</td>';
        html += '<td class="center">';
        if (char.photo && char.photo.trim() !== "") {
            html += '<img src="' + char.photo + '" class="character-photo ' + rotateClass + '" alt="' + char.name + '">';
        } else {
            html += '<span style="color: #8b847a;">—</span>';
        }
        html += '</td>';
        html += '<td>' + getCharacterLink(char) + '</td>';
        html += '<td class="character-age">' + ageDisplay + '</td>';
        html += '<td>' + (char.type || '—') + '</td>';
        html += '<td>' + infoHtml + '</td>';
        html += '<td>' + (char.appearance || '—') + '</td>';
        html += actions;
        html += '</tr>';
    }
    
    tbody.innerHTML = html;
}

// ==================== ФИЛЬТРАЦИЯ И ПОИСК (ОБНОВЛЁННАЯ ВЕРСИЯ) ====================
function filterTable() {
    var search = document.getElementById('prsSearchInput').value.toLowerCase().trim();
    var status = document.getElementById('prsStatusFilter').value;
    var rows = document.querySelectorAll('#prsTableBody tr');
    
    for (var i = 0; i < rows.length; i++) {
        var row = rows[i];
        
        // Проверяем, есть ли в строке данные (не "Нет персонажей" и не "Загрузка...")
        if (row.cells.length < 2) {
            row.style.display = '';
            continue;
        }
        
        // Получаем данные из колонок (индексы с 0)
        var nameCell = row.cells[2] ? row.cells[2].textContent.toLowerCase() : '';      // Имя (колонка 3)
        var appearanceCell = row.cells[6] ? row.cells[6].textContent.toLowerCase() : ''; // Внешность (колонка 7)
        var statusCell = row.cells[3] ? row.cells[3].textContent.toLowerCase() : '';     // Возраст/Статус (колонка 4)
        
        var show = true;
        
        // ПОИСК: проверяем Имя ИЛИ Внешность
        if (search) {
            var nameMatch = nameCell.indexOf(search) !== -1;
            var appearanceMatch = appearanceCell.indexOf(search) !== -1;
            
            // Показываем, если совпадает хотя бы одно поле
            show = nameMatch || appearanceMatch;
        }
        
        // ФИЛЬТР ПО СТАТУСУ (жив/мёртв)
        if (show && status !== 'all') {
            var hasDeathDate = statusCell.indexOf('⚰️') !== -1 || statusCell.indexOf('дата смерти') !== -1;
            
            if (status === 'deceased') {
                show = hasDeathDate;
            } else if (status === 'alive') {
                show = !hasDeathDate;
            }
        }
        
        row.style.display = show ? '' : 'none';
    }
}

// ==================== ФОРМА ====================
function showEditForm(character) {
    var isNew = !character;
    document.getElementById('prsFormTitle').textContent = isNew ? 'Добавление персонажа' : 'Редактирование персонажа';
    document.getElementById('prsKey').value = character ? character._key : '';
    document.getElementById('prsNumber').value = character ? (character.number || '') : '';
    document.getElementById('prsPhoto').value = character ? (character.photo || '') : '';
    document.getElementById('prsName').value = character ? (character.name || '') : '';
    document.getElementById('prsLink').value = character ? (character.link || '') : '';
    document.getElementById('prsAge').value = character ? (character.age || '') : '';
    document.getElementById('prsDeathDate').value = character ? (character.deathDate || '') : '';
    document.getElementById('prsType').value = character ? (character.type || '') : '';
    document.getElementById('prsLocation').value = character ? (character.location || '') : '';
    document.getElementById('prsOccupation').value = character ? (character.occupation || '') : '';
    document.getElementById('prsAppearance').value = character ? (character.appearance || '') : '';
    
    document.getElementById('prsEditForm').style.display = 'block';
    document.getElementById('prsEditForm').scrollIntoView({behavior: 'smooth'});
}

function hideEditForm() {
    document.getElementById('prsEditForm').style.display = 'none';
}

function collectFormData() {
    return {
        _key: document.getElementById('prsKey').value,
        number: document.getElementById('prsNumber').value,
        photo: document.getElementById('prsPhoto').value,
        name: document.getElementById('prsName').value,
        link: document.getElementById('prsLink').value,
        age: document.getElementById('prsAge').value,
        deathDate: document.getElementById('prsDeathDate').value,
        type: document.getElementById('prsType').value,
        location: document.getElementById('prsLocation').value,
        occupation: document.getElementById('prsOccupation').value,
        appearance: document.getElementById('prsAppearance').value
    };
}

function validateForm() {
    if (!document.getElementById('prsName').value.trim()) {
        alert('Введите имя персонажа');
        return false;
    }
    if (!document.getElementById('prsNumber').value.trim()) {
        alert('Введите номер');
        return false;
    }
    return true;
}

function saveForm() {
    if (!validateForm()) return;
    
    var char = collectFormData();
    var isNew = !char._key;
    
    logDebug('=== СОХРАНЕНИЕ === ' + (isNew ? 'новый' : 'обновление'));
    
    saveCharacter(char, function(success, key) {
        if (success) {
            if (isNew) {
                charactersData.push(char);
            } else {
                for (var i = 0; i < charactersData.length; i++) {
                    if (charactersData[i]._key === char._key) {
                        charactersData[i] = char;
                        break;
                    }
                }
            }
            renderTable();
            hideEditForm();
            alert('Персонаж сохранен!');
        } else {
            alert('Ошибка сохранения!');
        }
    });
}

function deleteCharacterPrompt(key) {
    if (!confirm('Удалить этого персонажа?')) return;
    deleteCharacter(key, function(success) {
        if (success) {
            charactersData = charactersData.filter(function(char) { return char._key !== key; });
            renderTable();
            alert('Персонаж удален!');
        } else {
            alert('Ошибка удаления');
        }
    });
}

function editCharacter(key) {
    var char = null;
    for (var i = 0; i < charactersData.length; i++) {
        if (charactersData[i]._key === key) {
            char = charactersData[i];
            break;
        }
    }
    if (char) showEditForm(char);
}

// ==================== ИНИЦИАЛИЗАЦИЯ ====================
function init() {
    if (!checkPermissions()) return;
    
    apiToken = getToken();
    logDebug('Token: ' + (apiToken ? 'найден' : 'НЕТ'));
    
    if (isAdmin) {
        document.querySelector('.prs-admin-panel').style.display = 'flex';
        var actionsHeader = document.querySelector('.col-actions');
        if (actionsHeader) actionsHeader.style.display = 'table-cell';
    }
    
    document.getElementById('prsAddBtn').addEventListener('click', function() { showEditForm(null); });
    document.getElementById('prsSaveBtn').addEventListener('click', saveForm);
    document.getElementById('prsCancelBtn').addEventListener('click', hideEditForm);
    
    var searchInput = document.getElementById('prsSearchInput');
    var resetBtn = document.getElementById('prsResetBtn');
    var statusFilter = document.getElementById('prsStatusFilter');
    
    if (searchInput) searchInput.addEventListener('keyup', filterTable);
    if (resetBtn) resetBtn.addEventListener('click', function() { 
        if (searchInput) searchInput.value = ''; 
        if (statusFilter) statusFilter.value = 'all';
        filterTable(); 
    });
    if (statusFilter) statusFilter.addEventListener('change', filterTable);
    
    loadData();
}

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
} else {
    init();
}
</script>

в данный момент они не заполнены. Хроника, если правильно помню, с гостя не видна, но скрипт на нее есть выше полный.
Вопрос вот в чем - мы своими говнокодами поделками ИИ ниче не нарушаем/не ломаем хд если да, то каков вопрос цены правок этого прекрасного ваяния и к кому за етим можно обратиться. Если не ломаем/не нарушаем, то и хорошо хд

Отредактировано Трамп (Вт, 8 Сен 2026 18:16:38)

0

1789

Трамп
Доброго дня!
Тут пояснил: На счет недавнего сообщения о блокировке форумов за скрипты
+ новость дополнил. Не подумал, что перепугаетесь - речь шла про очень конкретные скрипты с конкретными ошибками, а не про любые. Ну и если после установки подгрузочного скрипта у людей не возникает проблем с загрузкой форума - то всё в порядке, юзеров он не дудосит (если упрощенно и не погружаясь в сложное).

Тут речь не про нарушение какое-то, а что можно зацикленными запросами/пачечными запросами случайно забанить самого себя и подумать, что сломался сервис  :D

+1

1790

Arakhen
поняла-приняла хд спасибо в любом случае <3

+1

1791

Здравствуйте. Поставила шапку на форум https://artgalerei123.rolebb.ru/
Видна с компа и с альбомной версии телефона. С книжной версии телефона не видна. Помогите, пожалуйста, настроить. Спасибо!
Форум Арт-галерея "Шоколад"

0

1792

Доброго дня!

Санна написал(а):

Видна с компа и с альбомной версии телефона. С книжной версии телефона не видна.

Нужно уточнение, что имеется в виду под книжной версией. В смысле - с мобильной?

0

1793

Да. Вертикальной. Если телефон повернуть в горизонтальное положение, шапка видна и стоит правильно.

Отредактировано Санна (Вт, 15 Сен 2026 18:05:54)

0

1794

Санна
Попробуйте добавить красным:

<!-- ========== шапка форума ========== -->
<style type="text/css">
#pun-title .title-logo
{
background-image : url(https://forumstatic.ru/files/001c/ad/64/62411.png);
background-repeat : no-repeat;
background-size: contain;
width: 100%;
height: 10.4vw;
}
@media screen and (max-width: 540px){
#pun-title {
    background-image: url(https://forumstatic.ru/files/001c/ad/64/62411.png);
    background-repeat: no-repeat;
    background-position: 50% 20%;
    background-size: contain;
}}

</style>

+1

1795

Deff
Обнимаю и целую! Благодарю! :cool:

+1

1796

Deff написал(а):

Санна
Попробуйте добавить красным:

<!-- ========== шапка форума ========== -->
<style type="text/css">
#pun-title .title-logo
{
background-image : url(https://forumstatic.ru/files/001c/ad/64/62411.png);
background-repeat : no-repeat;
background-size: contain;
width: 100%;
height: 10.4vw;
}
@media screen and (max-width: 540px){
#pun-title {
    background-image: url(https://forumstatic.ru/files/001c/ad/64/62411.png);
    background-repeat: no-repeat;
    background-position: 50% 20%;
    background-size: contain;
}}

</style>

Активные темы стали неактивны... o.O  В админке они активны, а на форуме - нет.
Форум Арт-галерея "Шоколад"

Отредактировано Санна (Вт, 15 Сен 2026 22:50:09)

+1

1797

Санна
Добавьте после красного( перед тегом </style>)

Код:
 #pun-ulinks li{position:relative;z-index:100}

+1

1798

Надо отметить что оне неактивны были и без добавки красным

+1

1799

Deff
Ура! Благодарю! :love:

0

1800

Доброго времени суток!
Форум: https://badger.rolevaya.ru/

вопрос 1. На форуме стоит скрипт:

тык
Код:
<!-- Загрузчик изображений в форме ответа © 2018 Alex_63 -->
<script>FORUM.defaultHost = 'imgur'; // Хостинг по умолчанию: 'uploads' 'imgur' 'imageban' 'fastpic'</script>
<link rel="stylesheet" type="text/css" href="https://forumstatic.ru/files/0015/c4/3f/18002.css" />
<script type="text/javascript" src="https://forumstatic.ru/files/0015/c4/3f/18001.js"></script>
<script>if(FORUM.editor) {FORUM.editor.image.img_bbcodes.viewer.selected = false;FORUM.editor.image.img_bbcodes.source.selected = true;}</script>

и он был оплачен:

тык

https://upforme.ru/uploads/0000/14/1c/28188/t659073.png
https://upforme.ru/uploads/0000/14/1c/28188/t936704.png

и я либо где-то что-то пропустил, либо где-то что-то пропустил хд хотелось бы уточнить, почему ни один из загрузчиков вообще больше не работает и как можно это починить (можно ли вообще), ибо без них совсем грустно, так как стандартный загрузчик форума доводит до пердечного сриступа и инфаркта сидалищного органа хд

это второй вопрос и боль уже очень долгая.

Попробую объяснить. Я беру файл, он называется "без имени - 1". загружаю его в файлы форума.
https://upforme.ru/uploads/0000/14/1c/28188/t333749.png
в списках моих файлах он появляется с той превьюшкой которой надо и все хорошо.
потом я загружаю следующий файл, у него название точно такое же (стандартное из фш, на самом-то деле). И... я не знаю, что за магия происходит именно у меня (может быть не только у меня), но файл № 1 меняется на файл № 2 и я получаю два файла № 2.

То есть если я сейчас зайду на ссылку файла номер 1, вопрки тому, что превью там стоит нужная, там будет файл номер 2)

https://upforme.ru/uploads/001c/15/dc/2/777964.png

че это такое и как с эти бороться я не понимаю, ибо нередко меня не спасает даже переименовывание файлов)

0


Вы здесь » Единый форум поддержки » Вопросы по оформлению форума » Общие вопросы по оформлению (65) #2