2026, мистика, 18+
    Kindred Spirits

    Kindred Spirits

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

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


    Вы здесь » Kindred Spirits » Канцелярия » Полевые задания


    Полевые задания

    Сообщений 1 страница 2 из 2

    1

    Детали

    [html]
    <!doctype html>
    <html lang="ru">
    <head>
      <meta charset="windows-1251" />
      <meta name="viewport" content="width=device-width,initial-scale=1" />
      <title>Доска полевых заданий</title>
      <link rel="stylesheet" href="https://forumstatic.ru/files/001c/82/f2/14304.css?v=3">
      <script src="https://feather-tail.github.io/MyBB-scripts-bundle/js/theme-switcher/theme-iframe.js"></script>
      <script src="https://feather-tail.github.io/MyBB-scripts-bundle/js/font-resizer/font-resizer-iframe.js"></script>
    </head>

    <body>
    <section class="fo-board" aria-label="Доска полевых заданий">
      <header class="fo-header">
        <div>
          <h1 class="fo-title">Доска полевых заданий</h1>
        </div>

        <div class="fo-counter">
          <span>По текущим фильтрам показано заданий: <strong id="foCount">0</strong></span>
        </div>
      </header>

      <section class="fo-controls" aria-label="Поиск и фильтры">
        <div class="fo-controls-grid">
          <div class="fo-field">
            <div class="fo-label">Поиск</div>
            <div class="fo-search">
              <i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
              <input class="fo-input" id="q" type="search" placeholder="Название, локация, риск, ключевое слово…" autocomplete="off" />
            </div>
          </div>

          <div class="fo-field">
            <label class="fo-label" for="difficulty">Сложность</label>
            <select class="fo-select" id="difficulty">
              <option value="">Любая</option>
              <option value="1">I</option>
              <option value="2">II</option>
              <option value="3">III</option>
              <option value="4">IV</option>
            </select>
          </div>

          <div class="fo-field">
            <label class="fo-label" for="type">Тип</label>
            <select class="fo-select" id="type">
              <option value="">Любой</option>
              <option value="recon">Разведка / наблюдение</option>
              <option value="escort">Сопровождение</option>
              <option value="investigation">Расследование</option>
              <option value="supply">Доставка / подготовка</option>
              <option value="maintenance">Техподдержка / периметр</option>
              <option value="collection">Сбор ингредиентов</option>
              <option value="training">Тренировка (полевая)</option>
            </select>
          </div>

          <div class="fo-field">
            <label class="fo-label" for="location">Локация</label>
            <select class="fo-select" id="location">
              <option value="">Любая</option>
              <option value="campus">Кампус</option>
              <option value="salem">Салем</option>
              <option value="state">Штат</option>
              <option value="zone">Зона ответственности</option>
              <option value="restricted">Запретная зона</option>
            </select>
          </div>

          <div class="fo-field">
            <label class="fo-label" for="group">Размер группы</label>
            <select class="fo-select" id="group">
              <option value="">Любой</option>
              <option value="solo">Соло</option>
              <option value="2-3">2–3</option>
              <option value="3-4">3–4</option>
              <option value="4-6">4–6</option>
            </select>
          </div>

          <div class="fo-field">
            <label class="fo-label" for="risk">Риски</label>
            <select class="fo-select" id="risk">
              <option value="">Любые</option>
              <option value="magic">Магия</option>
              <option value="blight">Скверна</option>
              <option value="people">Люди / социальное</option>
              <option value="medical">Медицинские</option>
              <option value="weather">Погодные</option>
            </select>
          </div>

          <div class="fo-field">
            <label class="fo-label" for="sort">Сортировка</label>
            <select class="fo-select" id="sort">
              <option value="urgency">По срочности</option>
              <option value="newest">Сначала новые</option>
              <option value="difficulty">По сложности</option>
              <option value="reward">По награде</option>
            </select>
          </div>
        </div>
        <span class="fo-sr-only" aria-live="polite" id="foLive"></span>
      </section>

      <main class="fo-grid" id="grid" aria-label="Список заданий"></main>
      <div class="fo-empty" id="empty" hidden></div>
    </section>

    <script>
    (() => {
      'use strict';

      const root = document.querySelector('.fo-board');
      if (!root) return;

      const $ = (sel, r = root) => r.querySelector(sel);

      const grid = $('#grid');
      const empty = $('#empty');
      const countEl = $('#foCount');
      const live = $('#foLive');

      const q = $('#q');
      const difficulty = $('#difficulty');
      const type = $('#type');
      const locationSel = $('#location');
      const group = $('#group');
      const risk = $('#risk');
      const sort = $('#sort');

      const DATA_URL = 'https://feathertail.ru/ks/pages/fieldops-tasks.json';

      const LABELS = {
        difficulty: { 1: 'I', 2: 'II', 3: 'III', 4: 'IV' },
        type: {
          recon: 'Разведка / наблюдение',
          escort: 'Сопровождение',
          investigation: 'Расследование',
          supply: 'Доставка / подготовка',
          maintenance: 'Техподдержка / периметр',
          collection: 'Сбор ингредиентов',
          training: 'Тренировка (полевая)',
        },
        location: {
          campus: 'Кампус',
          salem: 'Салем',
          state: 'Штат',
          zone: 'Зона ответственности',
          restricted: 'Запретная зона',
        },
        group: {
          solo: 'Соло',
          '2-3': '2–3',
          '3-4': '3–4',
          '4-6': '4–6',
        },
        risk: {
          magic: 'Магия',
          blight: 'Скверна',
          people: 'Люди, социальное',
          medical: 'Медицинские',
          weather: 'Погодные',
        },
        status: { open: 'Открыто', work: 'В работе' },
      };

      const ICONS = {
        difficulty: 'fa-signal',
        location: 'fa-location-dot',
        group: 'fa-people-group',
        solo: 'fa-user',
        type: 'fa-tag',
        risks: 'fa-triangle-exclamation',
        reward: 'fa-award',
        date: 'fa-clock',
        urgency: 'fa-bolt',
      };

      const REWARD_META = {
        practice: { icon: 'fa-award', text: (r) => `+${Number(r.amount || 0)} $` },
        money: {
          icon: 'fa-dollar-sign',
          text: (r) => `${Number(r.amount || 0)}${r.currency || '$'}`,
        },
        perk: { icon: 'fa-id-card', text: (r) => r.label || 'Бонус' },
        item: { icon: 'fa-box', text: (r) => r.label || 'Предмет' },
      };

      const norm = (s) => (s || '').toString().toLowerCase().trim();

      const getTaskUrl = (t) => {
        const u = t?.url || t?.episodeUrl || t?.topicUrl || t?.link;
        return typeof u === 'string' && u.trim() ? u.trim() : '';
      };

      const normalizeParticipants = (p) => {
        if (!p) return [];
        const arr = Array.isArray(p) ? p : [p];

        return arr
          .map((x) => {
            if (typeof x === 'string') return { name: x.trim(), url: '' };

            if (x && typeof x === 'object') {
              const name = (x.name || x.title || x.nick || '').toString().trim();
              const url = (x.url || x.href || x.link || '').toString().trim();
              if (!name && url) return { name: url, url };
              return name ? { name, url } : null;
            }

            return null;
          })
          .filter(Boolean)
          .filter((x) => x.name);
      };

      const formatDateRu = (iso) => {
        const d = new Date(iso + 'T00:00:00');
        if (Number.isNaN(d.getTime())) return iso;
        return d
          .toLocaleDateString('ru-RU', {
            day: '2-digit',
            month: 'short',
            year: 'numeric',
          })
          .replace(/\./g, '');
      };

      const urgencyLabel = (n) => {
        const v = Number(n) || 1;
        if (v >= 3) return 'высокая';
        if (v === 2) return 'средняя';
        return 'низкая';
      };

      const normalizeRewards = (reward) => {
        if (reward == null) return [];
        if (typeof reward === 'number') return [{ type: 'practice', amount: reward }];
        if (Array.isArray(reward)) return reward;
        if (typeof reward === 'object') return [reward];
        return [{ type: 'perk', label: String(reward) }];
      };

      const rewardText = (reward) => {
        const rewards = normalizeRewards(reward);
        if (!rewards.length) return '—';
        return rewards
          .map((r) => {
            const meta = REWARD_META[r.type] || {
              icon: 'fa-award',
              text: (x) => x.label || String(x.amount || 'Награда'),
            };
            return meta.text(r);
          })
          .join(' • ');
      };

      const rewardSortValue = (reward) => {
        const rewards = normalizeRewards(reward);
        let v = 0;
        for (const r of rewards) {
          if (r.type === 'practice') v += Number(r.amount) || 0;
          if (r.type === 'money') v += (Number(r.amount) || 0) * 0.5;
        }
        return v;
      };

      const createEl = (tag, props = {}, children = []) => {
        const el = document.createElement(tag);
        for (const [k, v] of Object.entries(props)) {
          if (k === 'class') el.className = v;
          else if (k === 'text') el.textContent = v;
          else if (k === 'html') el.innerHTML = v;
          else if (k.startsWith('data-')) el.setAttribute(k, v);
          else el.setAttribute(k, v);
        }
        for (const ch of children) el.appendChild(ch);
        return el;
      };

      const getClampMetrics = (el) => {
        const cs = getComputedStyle(el);
        const lines = Math.max(1, parseInt(cs.getPropertyValue('--fo-desc-lines'), 10) || 3);
        let lh = parseFloat(cs.lineHeight);
        if (!Number.isFinite(lh)) {
          const fs = parseFloat(cs.fontSize) || 13;
          lh = fs * 1.45;
        }
        const maxH = lh * lines + 0.5;
        return { maxH };
      };

      const clampDesc = (el) => {
        const full = (el.dataset.full ?? el.textContent ?? '').toString();
        el.dataset.full = full;

        el.textContent = full;

        const { maxH } = getClampMetrics(el);
        if (el.scrollHeight <= maxH) {
          el.dataset.clamped = '0';
          return false;
        }

        let lo = 0;
        let hi = full.length;
        let best = 0;

        while (lo <= hi) {
          const mid = (lo + hi) >> 1;
          el.textContent = full.slice(0, mid) + '…';
          if (el.scrollHeight <= maxH) {
            best = mid;
            lo = mid + 1;
          } else {
            hi = mid - 1;
          }
        }

        let cut = full.slice(0, best);
        const cutByWord = cut.replace(/\s+\S*$/, '').trimEnd();
        cut = cutByWord.length >= 12 ? cutByWord : cut.trimEnd();

        el.textContent = cut + '…';
        el.dataset.clamped = '1';
        return true;
      };

      const modal = (() => {
        const wrap = createEl('div', {
          class: 'fo-modal',
          hidden: '',
          role: 'dialog',
          'aria-modal': 'true',
          'aria-labelledby': 'foModalTitle',
        });
        const panel = createEl('div', { class: 'fo-modal__panel' });
        const head = createEl('div', { class: 'fo-modal__head' });

        const title = createEl('h3', {
          class: 'fo-modal__title',
          id: 'foModalTitle',
          text: '',
        });
        const close = createEl('button', {
          class: 'fo-modal__close',
          type: 'button',
          'aria-label': 'Закрыть',
          text: 'x',
        });

        const content = createEl('div', { class: 'fo-modal__content' });

        const media = createEl('div', { class: 'fo-modal__media' }, [
          createEl('img', {
            id: 'foModalImg',
            alt: 'Карта задания',
            loading: 'lazy',
          }),
        ]);

        const meta = createEl('div', {
          class: 'fo-modal__meta',
          id: 'foModalMeta',
        });

        const peopleWrap = createEl(
          'div',
          { class: 'fo-modal__people', id: 'foModalPeopleWrap', hidden: '' },
          [
            createEl('div', { class: 'fo-modal__people-h', text: 'Участники:' }),
            createEl('ul', { class: 'fo-modal__people-list', id: 'foModalPeople' }),
          ],
        );

        const body = createEl('div', { class: 'fo-modal__body' }, [
          createEl('p', { class: 'fo-modal__desc', id: 'foModalDesc', text: '' }),
          peopleWrap,
        ]);

        head.append(title, close);
        content.append(media, meta);
        panel.append(head, content, body);
        wrap.append(panel);
        root.append(wrap);

        let opened = false;
        let prevOverflow = '';

        const closeModal = () => {
          if (!opened) return;
          opened = false;
          wrap.hidden = true;
          wrap.setAttribute('aria-hidden', 'true');
          document.documentElement.style.overflow = prevOverflow;
          close.blur();
        };

        const row = (label, icon, value, titleText = '') => {
          const v = (value ?? '—').toString();
          return createEl('div', { class: 'fo-modal__row' }, [
            createEl('div', { class: 'fo-modal__k' }, [
              createEl('i', { class: `fa-solid ${icon}`, 'aria-hidden': 'true' }),
              createEl('span', { text: label }),
            ]),
            createEl('div', {
              class: 'fo-modal__v',
              text: v,
              title: titleText || v,
            }),
          ]);
        };

        const openModal = (t) => {
          prevOverflow = document.documentElement.style.overflow || '';
          document.documentElement.style.overflow = 'hidden';

          const taskUrl = getTaskUrl(t);
          title.innerHTML = '';

          if (t.status === 'work' && taskUrl) {
            title.append(
              createEl('a', {
                class: 'fo-titlelink',
                href: taskUrl,
                target: '_blank',
                rel: 'noopener noreferrer',
                text: t.title || 'Задание',
              }),
            );
          } else {
            title.textContent = t.title || 'Задание';
          }

          $('#foModalDesc', wrap).textContent = (t.summary || '').toString();

          const imgEl = $('#foModalImg', wrap);
          imgEl.src = t.image || '';
          imgEl.alt = t.imageAlt || 'Карта задания';

          const metaEl = $('#foModalMeta', wrap);
          metaEl.innerHTML = '';

          const diffShort = LABELS.difficulty[t.difficulty] || t.difficulty;
          metaEl.append(
            row('Статус', 'fa-circle', LABELS.status[t.status] || t.status),
            row('Сложность', ICONS.difficulty, diffShort, `Сложность ${diffShort}`),
            row('Локация', ICONS.location, LABELS.location[t.location] || t.location),
            row(
              'Размер группы',
              t.group === 'solo' ? ICONS.solo : ICONS.group,
              LABELS.group[t.group] || t.group,
            ),
            row('Тип', ICONS.type, LABELS.type[t.type] || t.type),
            row(
              'Риски',
              ICONS.risks,
              Array.isArray(t.risks) ? t.risks.map((r) => LABELS.risk[r] || r).join(', ') : '—',
            ),
            row('Награда', ICONS.reward, rewardText(t.reward)),
          );

          metaEl.append(
            row('Дата', ICONS.date, formatDateRu(t.created || '')),
            row('Срочность', ICONS.urgency, urgencyLabel(t.urgency), `срочность: ${urgencyLabel(t.urgency)}`),
          );

          const people = normalizeParticipants(t.participants || t.people || t.members || t.players);
          const peopleWrapEl = $('#foModalPeopleWrap', wrap);
          const peopleListEl = $('#foModalPeople', wrap);

          if (peopleListEl) peopleListEl.innerHTML = '';

          if (people.length && peopleWrapEl && peopleListEl) {
            peopleWrapEl.hidden = false;

            for (const p of people) {
              const li = document.createElement('li');

              if (p.url) {
                li.append(
                  createEl('a', {
                    href: p.url,
                    target: '_blank',
                    rel: 'noopener noreferrer',
                    text: p.name,
                  }),
                );
              } else {
                li.textContent = p.name;
              }

              peopleListEl.append(li);
            }
          } else if (peopleWrapEl) {
            peopleWrapEl.hidden = true;
          }

          wrap.hidden = false;
          wrap.removeAttribute('aria-hidden');
          opened = true;
          close.focus();
        };

        close.addEventListener('click', closeModal);
        wrap.addEventListener('click', (e) => {
          if (e.target === wrap) closeModal();
        });
        document.addEventListener('keydown', (e) => {
          if (opened && e.key === 'Escape') closeModal();
        });

        return { openModal };
      })();

      const buildCard = (t) => {
        const statusText = LABELS.status[t.status] || t.status;
        const badgeClass =
          t.status === 'work' ? 'fo-badge fo-badge--work' : 'fo-badge fo-badge--open';

        const diffShort = LABELS.difficulty[t.difficulty] || String(t.difficulty || '');
        const diffFull = `Сложность ${diffShort}`;

        const locLabel = LABELS.location[t.location] || t.location;
        const groupLabel = LABELS.group[t.group] || t.group;

        const groupIcon = t.group === 'solo' ? 'fa-user' : 'fa-people-group';

        const risksText = (Array.isArray(t.risks) ? t.risks : [])
          .map((r) => LABELS.risk[r] || r)
          .join(', ');

        const article = createEl('article', {
          class: 'fo-card',
          'data-task-id': t.id || '',
        });

        const img = createEl('img', {
          src: t.image || '',
          alt: t.imageAlt || 'Карта локации',
          loading: 'lazy',
        });

        const badge = createEl('span', { class: badgeClass }, [
          createEl('i', { class: 'fa-solid fa-circle', 'aria-hidden': 'true' }),
          createEl('span', { text: statusText }),
        ]);

        const media = createEl('div', { class: 'fo-media' }, [img, badge]);

        const taskUrl = getTaskUrl(t);

        const title =
          t.status === 'work' && taskUrl
            ? createEl('h2', { class: 'fo-h' }, [
                createEl('a', {
                  class: 'fo-hlink',
                  href: taskUrl,
                  target: '_blank',
                  rel: 'noopener noreferrer',
                  text: t.title || 'Без названия',
                }),
              ])
            : createEl('h2', {
                class: 'fo-h',
                text: t.title || 'Без названия',
              });

        const top = createEl('div', { class: 'fo-top' }, [title]);

        const meta = createEl('ul', { class: 'fo-meta' }, [
          createEl('li', {}, [
            createEl('i', { class: 'fa-solid fa-signal', 'aria-hidden': 'true' }),
            createEl('span', {
              class: 'fo-meta-text',
              text: diffShort,
              title: diffFull,
            }),
          ]),
          createEl('li', {}, [
            createEl('i', { class: 'fa-solid fa-location-dot', 'aria-hidden': 'true' }),
            createEl('span', {
              class: 'fo-meta-text',
              text: locLabel,
              title: locLabel,
            }),
          ]),
          createEl('li', {}, [
            createEl('i', { class: `fa-solid ${groupIcon}`, 'aria-hidden': 'true' }),
            createEl('span', { class: 'fo-meta-text', text: groupLabel }),
          ]),
        ]);

        const descText = (t.summary || '').toString();
        const desc = createEl('p', { class: 'fo-desc', text: descText });
        desc.dataset.full = descText;

        const moreBtn = createEl('button', {
          type: 'button',
          class: 'fo-more fo-more--ghost',
          text: 'Читать полностью',
        });

        moreBtn.addEventListener('click', () => modal.openModal(t));

        const actions = createEl('div', { class: 'fo-actions' }, [moreBtn]);

        const kv = createEl('div', { class: 'fo-kv' }, [
          createEl('div', { class: 'fo-kvrow' }, [
            createEl('div', { class: 'fo-k' }, [
              createEl('i', { class: 'fa-solid fa-tag', 'aria-hidden': 'true' }),
              createEl('span', { text: 'Тип' }),
            ]),
            createEl('div', { class: 'fo-v', text: LABELS.type[t.type] || t.type }),
          ]),
          createEl('div', { class: 'fo-kvrow' }, [
            createEl('div', { class: 'fo-k' }, [
              createEl('i', { class: 'fa-solid fa-triangle-exclamation', 'aria-hidden': 'true' }),
              createEl('span', { text: 'Риски' }),
            ]),
            createEl('div', { class: 'fo-v', text: risksText || '—' }),
          ]),
          createEl('div', { class: 'fo-kvrow' }, [
            createEl('div', { class: 'fo-k' }, [
              createEl('i', { class: 'fa-solid fa-award', 'aria-hidden': 'true' }),
              createEl('span', { text: 'Награда' }),
            ]),
            createEl('div', { class: 'fo-v', text: rewardText(t.reward) }),
          ]),
        ]);

        const foot = createEl('div', { class: 'fo-foot' }, [
          createEl('span', {}, [
            createEl('i', { class: 'fa-regular fa-clock', 'aria-hidden': 'true' }),
            document.createTextNode(formatDateRu(t.created || '')),
          ]),
          createEl('span', {}, [
            createEl('i', { class: 'fa-solid fa-bolt', 'aria-hidden': 'true' }),
            document.createTextNode(`срочность: ${urgencyLabel(t.urgency)}`),
          ]),
        ]);

        const body = createEl('div', { class: 'fo-body' }, [top, meta, desc, actions, kv, foot]);

        article.append(media, body);
        return article;
      };

      const buildSearchHaystack = (t) => {
        const rewardStr = rewardText(t.reward);
        const peopleStr = normalizeParticipants(t.participants || t.people || t.members || t.players)
          .map((p) => p.name)
          .join(' ');
        const parts = [
          t.title,
          t.summary,
          LABELS.type[t.type] || t.type,
          LABELS.location[t.location] || t.location,
          Array.isArray(t.risks) ? t.risks.map((r) => LABELS.risk[r] || r).join(' ') : '',
          LABELS.group[t.group] || t.group,
          LABELS.status[t.status] || t.status,
          String(t.difficulty || ''),
          peopleStr,
          rewardStr,
        ];
        return norm(parts.filter(Boolean).join(' '));
      };

      const sortKey = (t, mode) => {
        const urgency = Number(t.urgency) || 0;
        const created = new Date((t.created || '1970-01-01') + 'T00:00:00').getTime() || 0;
        const diff = Number(t.difficulty) || 0;
        const reward = rewardSortValue(t.reward);

        if (mode === 'newest') return created;
        if (mode === 'difficulty') return diff;
        if (mode === 'reward') return reward;
        return urgency;
      };

      let TASKS = [];
      let TASKS_BY_ID = new Map();

      const shouldEnableModalButton = (t, clamped) => {
        if (clamped) return true;
        if (!t) return false;
        if (t.status === 'work') return true;
        const people = normalizeParticipants(t.participants || t.people || t.members || t.players);
        return people.length > 0;
      };

      const applyDescClampAndButtons = () => {
        const cards = grid.querySelectorAll('.fo-card');
        cards.forEach((card) => {
          const desc = card.querySelector('.fo-desc');
          const btn = card.querySelector('.fo-more');
          if (!desc || !btn) return;

          const clamped = clampDesc(desc);

          const id = card.getAttribute('data-task-id') || '';
          const t = id ? TASKS_BY_ID.get(id) : null;

          const enable = shouldEnableModalButton(t, clamped);

          if (enable) {
            btn.classList.remove('fo-more--ghost');
            btn.disabled = false;
            btn.removeAttribute('aria-hidden');
            btn.tabIndex = 0;
          } else {
            btn.classList.add('fo-more--ghost');
            btn.disabled = true;
            btn.setAttribute('aria-hidden', 'true');
            btn.tabIndex = -1;
          }
        });
      };

      const render = () => {
        const needle = norm(q.value);
        const diffVal = difficulty.value;
        const typeVal = type.value;
        const locVal = locationSel.value;
        const groupVal = group.value;
        const riskVal = risk.value;
        const sortMode = sort.value;

        const filtered = TASKS.filter((t) => {
          if (diffVal && String(t.difficulty) !== diffVal) return false;
          if (typeVal && t.type !== typeVal) return false;
          if (locVal && t.location !== locVal) return false;
          if (groupVal && t.group !== groupVal) return false;

          const risks = Array.isArray(t.risks) ? t.risks : [];
          if (riskVal && !risks.includes(riskVal)) return false;

          if (needle) {
            const hay = t._hay || '';
            if (!hay.includes(needle)) return false;
          }
          return true;
        });

        filtered.sort((a, b) => sortKey(b, sortMode) - sortKey(a, sortMode));

        grid.innerHTML = '';
        const frag = document.createDocumentFragment();
        for (const t of filtered) frag.appendChild(buildCard(t));
        grid.appendChild(frag);

        applyDescClampAndButtons();

        countEl.textContent = String(filtered.length);
        live.textContent = `Показано заданий: ${filtered.length}`;

        if (filtered.length === 0) {
          empty.hidden = false;
          empty.textContent = 'Ничего не найдено. Попробуйте изменить фильтры или запрос.';
        } else {
          empty.hidden = true;
          empty.textContent = '';
        }
      };

      const bind = () => {
        [q, difficulty, type, locationSel, group, risk, sort].forEach((el) => {
          el.addEventListener('input', render);
          el.addEventListener('change', render);
        });

        let raf = 0;
        window.addEventListener(
          'resize',
          () => {
            if (raf) cancelAnimationFrame(raf);
            raf = requestAnimationFrame(() => {
              applyDescClampAndButtons();
            });
          },
          { passive: true },
        );
      };

      const load = async () => {
        empty.hidden = false;
        empty.textContent = 'Загрузка заданий…';

        try {
          const res = await fetch(DATA_URL, { cache: 'no-store' });
          if (!res.ok) throw new Error(`HTTP ${res.status}`);
          const data = await res.json();
          TASKS = Array.isArray(data.tasks) ? data.tasks : [];

          TASKS = TASKS.map((t) => {
            const tt = { ...t };
            tt._hay = buildSearchHaystack(tt);
            return tt;
          });

          TASKS_BY_ID = new Map();
          for (const t of TASKS) {
            if (t && t.id) TASKS_BY_ID.set(String(t.id), t);
          }

          bind();
          render();
        } catch (e) {
          grid.innerHTML = '';
          countEl.textContent = '0';
          empty.hidden = false;
          empty.textContent = 'Не удалось загрузить JSON с заданиями. Проверьте URL или доступность файла.';
        }
      };

      load();
    })();
    </script>
    </body>
    </html>
    [/html]
    [hideprofile]

    +7

    2

    Можно нам с @Alice Covarein сходит в поход и послушать «Ненормальную тишину»?

    +1


    Вы здесь » Kindred Spirits » Канцелярия » Полевые задания