[hideprofile]

Код:
<!--HTML-->
<div class="chronicle-app">
  <div class="chronicle-header">
    <div class="chronicle-main-title">ХРОНОЛОГИЯ</div>
    <div class="chronicle-subtitle">В этой теме можно найти все эпизоды и посмотреть, что вообще играется на форуме. Чтобы свести отыгрываемое с важными событиями хронологии, используйте кнопку "с событиями".<br>Хронология ведётся администрацией и регулярно обновляется.</div>
  </div>
  <div class="chronicle-controls">
    <div class="chronicle-filter">
      <label for="yearFilter">Год:</label>
      <select id="yearFilter" class="chronicle-select">
        <option value="all">Не выбрано</option>
      </select>
    </div>
    <div class="chronicle-filter">
      <label for="participantFilter">Участник:</label>
      <select id="participantFilter" class="chronicle-select">
        <option value="all">Не выбрано</option>
      </select>
    </div>
    <div class="chronicle-filter">
      <label for="statusFilter">Статус:</label>
      <select id="statusFilter" class="chronicle-select">
        <option value="all">Не выбрано</option>
        <option value="активный">Активный</option>
        <option value="завершённый">Завершённый</option>
        <option value="незаконченный">Незаконченный</option>
      </select>
    </div>
    <button id="eventsBtn" class="chronicle-btn" style="border-color: var(--accent);">С событиями</button>
    <button id="resetFilters" class="chronicle-btn">Сбросить</button>
    <div class="chronicle-stats">
      <span class="stats-badge">эпизодов: <b id="episodeCount">0</b></span>
    </div>
  </div>
  <div id="chronicleContainer"></div>
</div>

<style>
  .chronicle-app {
    background-image: url('https://forumstatic.ru/files/001c/5a/1f/57520.jpg');
    background-size: cover;
    background-position: center;
    width: 100%;
    max-height: 1024px;
    margin: -15px auto;
    padding: 100px 50px 35px 50px;
    box-sizing: border-box;
    border-radius: 8px;
    display: flex;
    flex-direction: column;
    color: var(--text1);
  }
  .chronicle-header {
    flex-shrink: 0;
    margin-bottom: 20px;
    text-align: center;
  }
  .chronicle-main-title {
    font-size: 22px;
    font-weight: bold;
    color: var(--text4);
    text-transform: uppercase;
    letter-spacing: 3px;
    text-shadow: 0px 0px 6px rgba(0, 0, 0, 1);
    margin-bottom: 8px;
  }
  .chronicle-subtitle {
    font-size: 12px;
    color: var(--text3, #bbb);
    max-width: 80%;
    margin: 0 auto;
    line-height: 1.5;
    font-style: italic;
    text-shadow: 0px 0px 6px rgba(0, 0, 0, 1);
  }
  .chronicle-controls {
    display: flex;
    flex-wrap: wrap;
    gap: 15px;
    margin-bottom: 20px;
    align-items: flex-end;
    background: transparent;
    padding: 0;
  }
  .chronicle-filter {
    display: flex;
    flex-direction: column;
    gap: 5px;
  }
  .chronicle-controls label {
    font-size: 11px;
    font-weight: bold;
    color: var(--text4);
    text-transform: uppercase;
  }
  .chronicle-select {
    padding: 6px 10px !important;
    color: #aba8a6 !important;
    background: #211c18 !important;
    border: none !important;
    border-radius: 10px;
    font: 400 11px var(--font), arial !important;
    box-sizing: border-box;
    max-width: 100%;
  }
  .chronicle-select option {
    background: #1a1612;
    color: #b9ae95;
  }
.chronicle-btn {
    background: rgba(18, 16, 14, .85);
    backdrop-filter: blur(30px);
    -webkit-backdrop-filter: blur(30px);
    border: 1px solid #756a5e;
    border-radius: 20px;
    color: #aba8a6;
    padding: 8px 20px;
    cursor: pointer;
    font-weight: bold;
    text-transform: uppercase;
    transition: transform 0.2s, box-shadow 0.2s;
    margin-left: auto;
    font-size: 10px;
    font-family: 'Montserrat';
  }
  .chronicle-btn:hover {
    transform: translateY(-3px) scale(1.1);
  }
  #eventsBtn.active {
    border-color: var(--accent);
    color: #aba8a6;
    background: rgba(18, 16, 14, .85);
  }
  .chronicle-stats {
    font-size: 12px;
    color: #b9ae95;
    margin-left: 10px;
    display: flex;
    align-items: center;
    height: 100%;
  }
  .stats-badge {
    background: rgba(18, 16, 14, 0.7);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border: 1px solid #756a5e;
    border-radius: 20px;
    padding: 7px 15px;
    font-size: 11px;
    color: #756a5e;
    font-weight: 500;
    letter-spacing: 0.5px;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
  }
  #chronicleContainer {
    flex: 1;
    overflow-y: auto;
    padding-right: 5px;
    min-height: 0;
  }
  .chronicle-timeline {
    max-width: 100%;
    font-family: inherit;
    font-size: 10px;
  }
  .chronicle-year-divider {
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 12px 0 8px;
    color: var(--text4, #d4c9a8);
    font-weight: bold;
    font-size: 14px;
    text-transform: uppercase;
    letter-spacing: 2px;
    text-shadow: 0 1px 3px rgba(0,0,0,0.3);
  }
  .chronicle-year-divider::before,
  .chronicle-year-divider::after {
    content: "";
    flex: 1;
    height: 1px;
    background: linear-gradient(to right, transparent, var(--text2), transparent);
    margin: 0 15px;
  }
  .chronicle-year-divider::before {
    background: linear-gradient(to left, transparent, var(--text2), transparent);
  }
  .chronicle-item {
    display: flex;
    flex-wrap: wrap;
    margin-top: 3px;
    padding: 8px 8px;
    border-bottom: 0px solid rgba(189, 185, 184, 0.1);
    transition: background 0.2s;
    background: rgba(18, 16, 14, 0.6);
    backdrop-filter: blur(30px);
    -webkit-backdrop-filter: blur(30px);
    border: 1px solid #393028;
    border-radius: 10px;
  }
  .chronicle-item.status-active {
    background: var(--beg100);
    border-left: 4px solid var(--accent);
    padding-left: 8px;
  }
  .chronicle-item.status-active .chronicle-title a {
    color: var(--links) !important;
  }
  .chronicle-item.status-active .chronicle-title a:hover {
    color: var(--links2) !important;
  }
  .chronicle-item.status-closed {
    opacity: 1;
    border-left: 4px solid var(--accent2);
    padding-left: 8px;
    background: var(--beg100);
  }
  .chronicle-item.status-unfinished {
    border: 1px solid #393028;
    padding-left: 8px;
    opacity: 0.8;
    background: var(--beg100);
  }
  .chronicle-left {
    flex: 0 0 35%;
    padding-right: 15px;
    box-sizing: border-box;
  }
  .chronicle-right {
    flex: 0 0 65%;
    box-sizing: border-box;
  }
  .chronicle-date {
    font-weight: bold;
    color: var(--text1);
    margin-bottom: 3px;
    font-size: 10px;
  }
  .chronicle-title {
    text-align: left;
    font-weight: bold;
    margin-bottom: 3px;
    font-size: 13px;
    text-transform: uppercase;
  }
  .chronicle-title a {
    color: var(--links);
    text-decoration: none;
  }
  .chronicle-title a:hover {
    color: var(--accent) !important;
  }
  .chronicle-participants {
    font-size: 9px;
    color: var(--text1);
    margin-top: 3px;
    display: inline;
  }
  .chronicle-location {
    font-weight: bold;
    color: var(--text1);
    margin-bottom: 3px;
    font-size: 10px;
  }
  .chronicle-desc {
    font-size: 11px;
    color: var(--text1);
    line-height: 1.4;
  }
  .participant-tag {
    display: inline-block;
    background: var(--beg300);
    padding: 2px 6px;
    border-radius: 12px;
    margin-right: 4px;
    margin-top: 4px;
    font-size: 10px;
    cursor: pointer;
    transition: background 0.2s;
    align-self: flex-start;
  }
  .participant-tag:hover {
    background: var(--beg100);
  }
  .chronicle-loading {
    text-align: center;
    padding: 40px;
    color: var(--text3);
  }

  /* Стили для событий */
  .chronicle-event {
    display: flex;
    flex-wrap: wrap;
    margin-top: 3px;
    padding: 8px 12px;
    border: 0px solid #393028;
    border-radius: 8px;
    background: rgba(18, 16, 14, 0.4);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
  }
  .chronicle-event .chronicle-date {
    flex: 0 0 120px;
    font-weight: bold;
    font-size: 12px;
    color: var(--accent);
  }
  .chronicle-event .chronicle-event-desc {
    flex: 1;
    font-size: 12px;
    color: #aba8a6;
    line-height: 1.5;
  }

  @media (max-width: 720px) {
    .chronicle-app {
      padding: 50px 20px;
    }
    .chronicle-left,
    .chronicle-right {
      flex: 0 0 100%;
      padding-right: 0;
      margin-bottom: 5px;
    }
    .chronicle-controls {
      flex-direction: column;
      align-items: stretch;
    }
    .chronicle-btn {
      margin-left: 0;
    }
    .chronicle-year-divider {
      font-size: 12px;
    }
    .chronicle-year-divider::before,
    .chronicle-year-divider::after {
      margin: 0 8px;
    }
    .chronicle-main-title {
      font-size: 18px;
    }
    .chronicle-subtitle {
      font-size: 11px;
      max-width: 100%;
    }
    .chronicle-select {
      min-width: 100%;
    }
    .chronicle-stats {
      margin-left: 0;
      justify-content: center;
    }
    .chronicle-event .chronicle-date {
      flex: 0 0 80px;
    }
  }
</style>

<script>
(function() {
  function init() {
    var container = document.getElementById('chronicleContainer');
    if (!container) return setTimeout(init, 50);

    // ========== БАЗА ЭПИЗОДОВ И СОБЫТИЙ ==========
    var rawData = [
      // ========== ЭПИЗОДЫ ==========
      {
        type: "episode",
        date: "27.06.10199",
        title: "Рыба ещё эта",
        participants: ["Ariste Atreides", "Feyd-Rautha Harkonnen"],
        status: "завершённый",
        link: "https://dune.rusff.me/viewtopic.php?id=43#p121",
        year: "10199", location: "Арракис, Арракин",
        description: "Фейд-Раута узнаёт, что после разорванной помолвки Аристе Атрейдес не улетала на Валлах, как ему сказали, а осталась на Арракисе и всё ещё находится там. На-барон решает объясниться с суженой лично."
      },
      {
        type: "episode",
        date: "01.03.10191",
        title: "Revenge Is a Dish Best Served Poisoned",
        participants: ["Feyd-Rautha Harkonnen", "Piter de Vries"],
        status: "активный",
        link: "https://dune.rusff.me/viewtopic.php?id=51#p339",
        year: "10191",
        location: "Гиеди Прайм",
        description: "Питер де Врис завершает незаконченные дела. Фейд-Раута получает урок семейных ценностей."
      },
      {
        type: "episode",
        date: "30.06.10198",
        title: "How to Charm a Princess",
        participants: ["Feyd-Rautha Harkonnen", "Ariste Atreides", "Piter de Vries"],
        status: "активный",
        link: "https://dune.rusff.me/viewtopic.php?id=52#p346",
        year: "10198",
        location: "Арракис",
        description: "Званый ужин в честь помолвки Аристе Атрейдес и Фейда-Рауты Харконнена"
      },
      {
        type: "episode",
        date: "27.06.10199",
        title: "You did what now?",
        participants: ["Feyd-Rautha Harkonnen", "Piter de Vries"],
        status: "активный",
        link: "https://dune.rusff.me/viewtopic.php?id=57#p625",
        year: "10199",
        location: "Арракис",
        description: "Питер и Фейд обсуждают вылазку Фейда в Арракин."
      },
      {
        type: "episode",
        date: "18.07.10199",
        title: "A Whole New World",
        participants: ["Feyd-Rautha Harkonnen", "Ariste Atreides"],
        status: "активный",
        link: "https://dune.rusff.me/viewtopic.php?id=56#p585",
        year: "10199",
        location: "Арракис",
        description: "Пытаясь выяснить настоящие причины смерти Бронсо Верниуса, Аристе связывается с Фейдом, чтобы запросить помощь"
      },
      {
        type: "episode",
        date: "13.08.10199",
        title: "Я не договорил",
        participants: ["Ariste Atreides", "Marcus Havok"],
        status: "активный",
        link: "https://dune.rusff.me/viewtopic.php?id=68#p1852",
        year: "10199",
        location: "Арракис",
        description: "Марк и Аристе наконец-то получают возможность объясниться после инцидента в Архивах и истинных причин смерти Зантары"
      },
      // ========== СОБЫТИЯ ==========
      {
        type: "event",
        date: "ХХ.04.10191",
        description: "<b>Битва за Арракин</b>: благодаря вмешательству леди Джессики, личность предателя удаётся раскрыть. Атрейдесы отражают атаку Харконненов и сардаукаров на Арракин.",
        year: "10191"
      },
      {
        type: "event",
        date: "ХХ.05.10191",
        description: "Решением Ландсраада на Арракисе объявляется <b>Война Ассасинов</b> между домами Харконненов и Атрейдесов.",
        year: "10191"
      },
      {
        type: "event",
        date: "ХХ.11.10194",
        description: "<b>Уничтожение Карфага</b> — столицы и основной базы операций Харконненов на Арракисе. Точные причины катастрофы неизвестны, объявляется расследование.",
        year: "10194"
      },
      {
        type: "event",
        date: "ХХ.11.10194",
        description: "Харконнены спешно строят космопорт близ деревни Харко и, решив не останавливаться на этом, объявляют строительство <b>Нео-Карфага</b>.",
        year: "10194"
      },
      {
        type: "event",
        date: "ХХ.02.10195",
        description: "Завершение расследования катастрофы, произошедшей в Карфаге. Официальной версией признаётся трагическая случайность: контакт щита Хольцмана с лазером.",
        year: "10195"
      },
      {
        type: "event",
        date: "ХХ.08.10195",
        description: "Сардаукарский погром, по официальной версии завершившийся полным истреблением фременов. На самом деле фремены мигрируют на юг планеты.",
        year: "10195"
      },
      {
        type: "event",
        date: "ХХ.06.10198",
        description: "Попытка завершить конфликт дипломатическим путём. При посредничестве Бене Гессерит, объявляется помолвка наследников домов Атрейдесов и Харконненов. Подписывается соглашение о временном перемирии.",
        year: "10198"
      }
    ];

    var allItems = rawData;
    var filteredItems = [];
    var showEvents = false; // по умолчанию события скрыты

    var yearFilter = document.getElementById('yearFilter');
    var participantFilter = document.getElementById('participantFilter');
    var statusFilter = document.getElementById('statusFilter');
    var resetBtn = document.getElementById('resetFilters');
    var eventsBtn = document.getElementById('eventsBtn');
    var countSpan = document.getElementById('episodeCount');

    // Уникальные годы и участники (из эпизодов)
    var episodesYearsSet = new Set();
    var allYearsSet = new Set();
    var participantsSet = new Set();
    allItems.forEach(function(item) {
      allYearsSet.add(item.year);
      if (item.type === 'episode') {
        episodesYearsSet.add(item.year);
      }
      if (item.participants) {
        item.participants.forEach(function(p) { participantsSet.add(p); });
      }
    });

    // Функция обновления опций годов в зависимости от режима
    function updateYearOptions() {
      var currentValue = yearFilter.value;
      while (yearFilter.options.length > 1) yearFilter.remove(1);
      var yearsArray = showEvents ? Array.from(allYearsSet) : Array.from(episodesYearsSet);
      yearsArray.sort(function(a, b) { return b - a; }).forEach(function(year) {
        var option = document.createElement('option');
        option.value = year;
        option.textContent = year;
        yearFilter.appendChild(option);
      });
      // Восстанавливаем выбранное значение, если оно есть
      if (currentValue) yearFilter.value = currentValue;
    }

    // Первичное заполнение годов (только эпизоды)
    updateYearOptions();

    while (participantFilter.options.length > 1) participantFilter.remove(1);
    Array.from(participantsSet).sort().forEach(function(participant) {
      var option = document.createElement('option');
      option.value = participant;
      option.textContent = participant;
      participantFilter.appendChild(option);
    });

    function parseDate(dateStr) {
      var parts = dateStr.split('.');
      if (parts.length !== 3) return 0;
      var day = parseInt(parts[0], 10);
      if (isNaN(day)) day = 0;
      var month = parseInt(parts[1], 10);
      if (isNaN(month)) month = 0;
      var year = parseInt(parts[2], 10);
      if (isNaN(year)) year = 0;
      return year * 10000 + month * 100 + day;
    }

    function updateEventsButton() {
      if (showEvents) {
        eventsBtn.classList.add('active');
        eventsBtn.textContent = 'Скрыть';
      } else {
        eventsBtn.classList.remove('active');
        eventsBtn.textContent = 'С событиями';
      }
    }

    function applyFilters() {
      var selectedYear = yearFilter.value;
      var selectedParticipant = participantFilter.value;
      var selectedStatus = statusFilter.value;

      filteredItems = allItems.filter(function(item) {
        if (selectedYear !== 'all' && item.year !== selectedYear) return false;
        if (selectedParticipant !== 'all') {
          if (!item.participants || item.participants.indexOf(selectedParticipant) === -1) return false;
        }

        if (showEvents) {
          if (selectedStatus !== 'all') {
            if (item.type === 'episode') {
              return item.status === selectedStatus;
            }
            return item.type === 'event';
          }
          return true;
        }

        if (selectedStatus !== 'all') {
          if (item.type !== 'episode' || item.status !== selectedStatus) return false;
        } else {
          if (item.type !== 'episode') return false;
        }
        return true;
      });

      filteredItems.sort(function(a, b) {
        var dateA = parseDate(a.date);
        var dateB = parseDate(b.date);
        if (dateA !== dateB) return dateA - dateB;
        if (a.type === 'event' && b.type !== 'event') return -1;
        if (a.type !== 'event' && b.type === 'event') return 1;
        return 0;
      });

      renderTimeline();
    }

    function renderTimeline() {
      if (filteredItems.length === 0) {
        container.innerHTML = '<div class="chronicle-loading">Нет записей, соответствующих фильтрам.</div>';
        countSpan.textContent = '0';
        return;
      }

      var grouped = {};
      filteredItems.forEach(function(item) {
        if (!grouped[item.year]) grouped[item.year] = [];
        grouped[item.year].push(item);
      });

      var years = Object.keys(grouped).sort(function(a, b) { return b - a; });
      var html = '<div class="chronicle-timeline">';
      var totalEpisodes = 0;

      years.forEach(function(year) {
        html += '<div class="chronicle-year-divider">' + year + ' AG</div>';
        grouped[year].forEach(function(item) {
          if (item.type === 'event') {
            html += '<div class="chronicle-event">';
            html += '<div class="chronicle-date">' + item.date + '</div>';
            html += '<div class="chronicle-event-desc">' + (item.description || '') + '</div>';
            html += '</div>';
          } else {
            totalEpisodes++;
            var statusClass = '';
            if (item.status === 'активный') statusClass = 'status-active';
            else if (item.status === 'завершённый') statusClass = 'status-closed';
            else if (item.status === 'незаконченный') statusClass = 'status-unfinished';

            var participantsHtml = '<div class="chronicle-participants">' +
              item.participants.map(function(p) {
                return '<span class="participant-tag" data-participant="' + p + '">' + p + '</span>';
              }).join('') +
              '</div>';

            html += '<div class="chronicle-item ' + statusClass + '">';
            html += '<div class="chronicle-left">';
            html += '<div class="chronicle-date">' + item.date + '</div>';
            html += '<div class="chronicle-title"><a href="' + item.link + '" target="_blank">' + item.title + '</a></div>';
            html += participantsHtml;
            html += '</div>';
            html += '<div class="chronicle-right">';
            html += '<div class="chronicle-location">' + item.location + '</div>';
            html += '<div class="chronicle-desc">' + (item.description || '') + '</div>';
            html += '</div>';
            html += '</div>';
          }
        });
      });

      html += '</div>';
      container.innerHTML = html;
      countSpan.textContent = totalEpisodes;

      var tags = container.querySelectorAll('.participant-tag');
      for (var j = 0; j < tags.length; j++) {
        tags[j].addEventListener('click', function() {
          var participant = this.getAttribute('data-participant');
          participantFilter.value = participant;
          applyFilters();
        });
      }
    }

    yearFilter.addEventListener('change', applyFilters);
    participantFilter.addEventListener('change', applyFilters);
    statusFilter.addEventListener('change', applyFilters);

    resetBtn.addEventListener('click', function() {
      yearFilter.value = 'all';
      participantFilter.value = 'all';
      statusFilter.value = 'all';
      showEvents = false;
      updateYearOptions();
      updateEventsButton();
      applyFilters();
    });

    eventsBtn.addEventListener('click', function() {
      showEvents = !showEvents;
      updateYearOptions();
      updateEventsButton();
      applyFilters();
    });

    updateEventsButton();
    applyFilters();
  }

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