Source: 12-misc.js

/* ── Distraction tracking ── */
const STORE_DISTRACTIONS = 'wl_distractions_v1';
function loadDistractions() {
  try {
    const raw = JSON.parse(localStorage.getItem(STORE_DISTRACTIONS) || '[]');
    return Array.isArray(raw)
      ? raw.filter((distraction) => distraction && typeof distraction.ts === 'number')
      : [];
  } catch (err) {
    return [];
  }
}
function saveDistraction(note) {
  const entry = getActiveTimer()
    ? getEntries().find((logEntry) => logEntry.id === getActiveTimer().entryId)
    : null;
  const d = {
    ts: Date.now(),
    date: dk(new Date()),
    task: entry ? entry.text : null,
    note: note || null,
  };
  const all = loadDistractions();
  all.push(d);
  localStorage.setItem(STORE_DISTRACTIONS, JSON.stringify(all));
  renderDistractionCount();
}
function renderDistractionCount() {
  const el = document.getElementById('distractionSection');
  if (!el) return;
  const today = dk(new Date());
  const all = loadDistractions().filter((distraction) => distraction.date === today);
  if (!all.length) {
    el.innerHTML = '';
    return;
  }
  const rows = all
    .map((distraction) => {
      const t = new Date(distraction.ts);
      const hh = String(t.getHours()).padStart(2, '0');
      const mm = String(t.getMinutes()).padStart(2, '0');
      const task = distraction.task
        ? `<span style="color:var(--text3);font-size:11px"> — ${escHtml(distraction.task)}</span>`
        : '';
      const note = distraction.note
        ? `<span style="color:var(--text2)"> "${escHtml(distraction.note)}"</span>`
        : '';
      return `<div style="font-size:12px;padding:3px 0;border-bottom:0.5px solid var(--border)">${hh}:${mm}${task}${note}</div>`;
    })
    .join('');
  el.innerHTML = `
      <div style="font-size:11px;text-transform:uppercase;letter-spacing:0.06em;color:var(--text3);margin-bottom:6px">
        😵 ${all.length} distraction${all.length === 1 ? '' : 's'} today
      </div>
      ${rows}`;
}

/**
 * Handles the "distracted" button: pauses a running timer, asks for an
 * optional note, and logs the distraction. Cancelling the prompt logs nothing
 * and resumes the timer — but only if this click paused it, so a timer the
 * user had already paused stays paused (#607).
 */
function handleDistractClick() {
  const timer = getActiveTimer();
  const pausedByThisClick = Boolean(timer && !timer.paused);
  if (pausedByThisClick) pauseTimer();
  const note = prompt('What pulled you away? (optional — press Enter to skip)');
  if (note === null) {
    if (pausedByThisClick) resumeTimer();
    return;
  }
  saveDistraction(note.trim() || null);
  // Timer stays paused after a logged distraction — user resumes manually
  renderDistractionCount();
}

document.getElementById('timerDistract').addEventListener('click', handleDistractClick);

/* ── Parked thoughts ── */
const STORE_PARKED = 'wl_parked_v1';
let parkedThoughts = [];

function saveParked() {
  localStorage.setItem(STORE_PARKED, JSON.stringify(parkedThoughts));
}
function loadParked() {
  try {
    parkedThoughts = JSON.parse(localStorage.getItem(STORE_PARKED) || '[]');
  } catch (err) {
    parkedThoughts = [];
  }
}
function renderParked() {
  const open = parkedThoughts.filter((thought) => !thought.done);
  const section = document.getElementById('parkSection');
  const list = document.getElementById('parkList');
  const badge = document.getElementById('parkBadge');
  if (!section || !list) return;
  if (open.length === 0) {
    section.style.display = 'none';
    return;
  }
  section.style.display = '';
  if (badge) badge.textContent = open.length;
  list.innerHTML = open
    .map((thought) => {
      const safeId = escHtml(thought.id);
      return `
      <div class="parked-item" data-id="${safeId}">
        <div class="parked-item-text">
          ${escHtml(thought.text)}
          ${thought.fromTask ? `<span class="parked-from">while working on: ${escHtml(thought.fromTask)}</span>` : ''}
        </div>
        <button class="parked-promote" data-id="${safeId}">→ task</button>
        <button class="parked-dismiss" data-id="${safeId}" title="dismiss">✓</button>
      </div>`;
    })
    .join('');
  list.querySelectorAll('.parked-promote').forEach((btn) => {
    btn.addEventListener('click', (event) => {
      event.stopPropagation();
      const p = parkedThoughts.find((thought) => thought.id === btn.dataset.id);
      if (!p) return;
      const todayKey = dk(new Date());
      getPlanTasks().push({
        id: Date.now() + '',
        text: p.text,
        status: 'todo',
        date: todayKey,
        tag: getSelectedTag(),
      });
      savePlan();
      p.done = true;
      saveParked();
      renderParked();
      renderPlan();
    });
  });
  list.querySelectorAll('.parked-dismiss').forEach((btn) => {
    btn.addEventListener('click', (event) => {
      event.stopPropagation();
      const p = parkedThoughts.find((thought) => thought.id === btn.dataset.id);
      if (p) {
        p.done = true;
        saveParked();
        renderParked();
      }
    });
  });
}

// Park button in timer bar
(() => {
  const btn = document.getElementById('timerParkBtn');
  const inp = document.getElementById('parkCapture');
  if (!btn || !inp) return;
  btn.addEventListener('click', (event) => {
    event.stopPropagation();
    const showing = inp.classList.contains('show');
    inp.classList.toggle('show', !showing);
    btn.classList.toggle('active', !showing);
    if (!showing) {
      inp.focus();
    } else {
      inp.value = '';
    }
  });
  inp.addEventListener('keydown', (event) => {
    if (event.key === 'Enter') {
      const text = inp.value.trim();
      if (!text) {
        inp.classList.remove('show');
        btn.classList.remove('active');
        return;
      }
      const liveEntry = getActiveTimer()
        ? getEntries().find((en) => en.id === getActiveTimer().entryId)
        : null;
      parkedThoughts.push({
        id: Date.now() + '',
        text,
        ts: Date.now(),
        fromTask: liveEntry ? liveEntry.text : null,
        done: false,
      });
      saveParked();
      renderParked();
      inp.value = '';
      inp.classList.remove('show');
      btn.classList.remove('active');
    } else if (event.key === 'Escape') {
      inp.value = '';
      inp.classList.remove('show');
      btn.classList.remove('active');
    }
  });
})();

// Hero running panel — park-a-distraction inline input
(() => {
  const inp = document.getElementById('heroParkInput');
  if (!inp) return;
  inp.addEventListener('keydown', (event) => {
    if (event.key === 'Enter') {
      const text = inp.value.trim();
      if (!text) return;
      const liveEntry = getActiveTimer()
        ? getEntries().find((en) => en.id === getActiveTimer().entryId)
        : null;
      parkedThoughts.push({
        id: Date.now() + '',
        text,
        ts: Date.now(),
        fromTask: liveEntry ? liveEntry.text : null,
        done: false,
      });
      saveParked();
      renderParked();
      inp.value = '';
    } else if (event.key === 'Escape') {
      inp.value = '';
    }
  });
})();

/* ── IDKW (I don't know what to do) ── */
(() => {
  const btn = document.getElementById('idkwBtn');
  if (!btn) return;
  let idkwTimer = null;
  btn.addEventListener('click', (event) => {
    event.stopPropagation();
    const todayKey = dk(new Date());
    const candidates = getPlanTasks().filter(
      (task) => task.date === todayKey && task.status === 'todo' && !task.parentId
    );
    if (!candidates.length) return;
    document
      .querySelectorAll('.idkw-highlight')
      .forEach((el) => el.classList.remove('idkw-highlight'));
    clearTimeout(idkwTimer);
    const pick = candidates[Math.floor(Math.random() * candidates.length)];
    const row = document.querySelector(`.plan-row[data-id="${pick.id}"]`);
    if (row) {
      row.classList.add('idkw-highlight');
      row.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
      idkwTimer = setTimeout(() => row.classList.remove('idkw-highlight'), 8000);
    }
  });
})();

/* ── Make-it-interesting hook (Feature 1) ── */
const STORE_HOOKS = 'wl_hooks';
function getHook(taskText) {
  try {
    const map = JSON.parse(localStorage.getItem(STORE_HOOKS) || '{}');
    return map[taskText.toLowerCase()] || null;
  } catch (err) {
    return null;
  }
}
function saveHook(taskText, hook) {
  try {
    const map = JSON.parse(localStorage.getItem(STORE_HOOKS) || '{}');
    if (hook === null) {
      delete map[taskText.toLowerCase()];
    } else {
      map[taskText.toLowerCase()] = hook;
    }
    localStorage.setItem(STORE_HOOKS, JSON.stringify(map));
  } catch (err) {
    wlLog.warn('saveHook: failed to persist task hook to localStorage', err);
  }
}

(() => {
  const btn = document.getElementById('timerHookBtn');
  const panel = document.getElementById('timerHookPanel');
  const content = document.getElementById('timerHookContent');
  const closeBtn = document.getElementById('timerHookClose');
  if (!btn || !panel || !content || !closeBtn) return;

  function getPromptForTask(taskText) {
    return `Task: "${taskText}"

Provide two short, actionable suggestions to make this task more engaging.

Requirements:
- No preamble, no headers, no numbering, no markdown formatting
- Plain text only
- First suggestion: a genuine curiosity angle or interesting adjacent perspective
- Second suggestion: a way to add time pressure or stakes without real consequences
- Each suggestion: 1-2 sentences max
- Separate them with a blank line`;
  }

  function closePanel() {
    panel.style.display = 'none';
  }
  function showHook(hookText) {
    content.textContent = hookText;
    panel.style.display = 'block';
  }
  function copyToClipboard(text) {
    navigator.clipboard.writeText(text).catch((error) => console.warn('Clipboard failed:', error));
  }

  btn.addEventListener('click', async (event) => {
    event.stopPropagation();
    if (!getActiveTimer()) return;
    const entry = getEntries().find((en) => en.id === getActiveTimer().entryId);
    if (!entry) return;
    const taskText = entry.text.trim();
    const cached = getHook(taskText);
    if (cached) {
      showHook(cached);
      return;
    }

    content.textContent = 'thinking...';
    panel.style.display = 'block';
    try {
      const response = await fetch('/api/ai', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          model: 'claude-haiku-4-5-20251001',
          max_tokens: 600,
          system:
            'You help ADHD users make boring tasks engaging. Provide brief, practical suggestions with no numbering, labels, or preamble. Format: suggestion 1 on first line, blank line, suggestion 2 on third line. Plain text only, no markdown.',
          messages: [{ role: 'user', content: getPromptForTask(taskText) }],
        }),
      });
      if (!response.ok) throw new Error(`API error: ${response.status}`);
      const data = await response.json();
      const hookText = data.content?.[0]?.text || '';
      if (!hookText) throw new Error('No content in response');
      saveHook(taskText, hookText);
      showHook(hookText);
    } catch (err) {
      copyToClipboard(getPromptForTask(taskText));
      content.textContent =
        'AI unavailable — prompt copied to clipboard. (Set AnthropicApiKey in config.local.ps1)';
      panel.style.display = 'block';
    }
  });

  closeBtn.addEventListener('click', closePanel);

  const regenBtn = document.getElementById('timerHookRegen');
  if (regenBtn) {
    regenBtn.addEventListener('click', async (event) => {
      event.stopPropagation();
      if (!getActiveTimer()) return;
      const entry = getEntries().find((en) => en.id === getActiveTimer().entryId);
      if (!entry) return;
      saveHook(entry.text.trim(), null);
      btn.click();
    });
  }
})();

/* ── Accessibility utilities ── */

// Make a div[role="button"] respond to Enter/Space like a real button.
function a11yHeaderKeydown(el) {
  if (!el) return;
  el.addEventListener('keydown', (event) => {
    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      el.click();
    }
  });
}

// Every collapsible section header that has role="button" must be listed here, or
// keyboard users can tab to it but not toggle it (WCAG 2.1.1). A test checks
// this list against work-log.html. planHeader is excluded: it contains a nested
// <button> (idkwBtn), so the outer div must not also carry role="button" or
// keyboard events would be ambiguous.
const KEYBOARD_ACTIVATED_HEADER_IDS = [
  'calHeader',
  'upcomingHeader',
  'pendingHeader',
  'completedHeader',
  'jiraHeader',
  'notionLinksHeader',
  'analyticsHeader',
  'parkHeader',
  'pomoHeader',
];
KEYBOARD_ACTIVATED_HEADER_IDS.forEach((id) => {
  a11yHeaderKeydown(document.getElementById(id));
});

// Sync aria-expanded on toggling section headers.
// Each section header's click listener was set up in other modules; we patch
// aria-expanded by observing classList changes on the section wrappers.
// planHeader is excluded: it has no widget role (it contains a nested <button>
// so role="button" would be invalid), and aria-expanded is not allowed on a
// generic div — see also the keyboard-nav exclusion note above.
(function syncAriaExpanded() {
  const pairs = [
    { sectionId: 'analyticsSection', headerId: 'analyticsHeader' },
    { sectionId: 'calSection', headerId: 'calHeader' },
    { sectionId: 'upcomingSection', headerId: 'upcomingHeader' },
    { sectionId: 'pendingSection', headerId: 'pendingHeader' },
    { sectionId: 'completedSection', headerId: 'completedHeader' },
    { sectionId: 'jiraSection', headerId: 'jiraHeader' },
    { sectionId: 'notionLinksSection', headerId: 'notionLinksHeader' },
    { sectionId: 'pomoSection', headerId: 'pomoHeader' },
  ];
  pairs.forEach(({ sectionId, headerId }) => {
    const section = document.getElementById(sectionId);
    const header = document.getElementById(headerId);
    if (!section || !header) return;
    // Set initial value from class list
    header.setAttribute('aria-expanded', String(!section.classList.contains('collapsed')));
    // Observe future class mutations
    new MutationObserver(() => {
      header.setAttribute('aria-expanded', String(!section.classList.contains('collapsed')));
    }).observe(section, { attributes: true, attributeFilter: ['class'] });
  });
})();

// Focus management for modal dialogs.
// Saves the element that had focus when a modal opens, restores it on close.
(function modalFocusManagement() {
  let _eodTrigger = null;
  let _expiryTrigger = null;

  // EOD modal
  const eodBtn = document.getElementById('eodBtn');
  const eodClose = document.getElementById('eodClose');
  const eodOverlay = document.getElementById('eodOverlay');

  if (eodBtn && eodOverlay) {
    eodBtn.addEventListener('click', () => {
      _eodTrigger = document.activeElement;
      setTimeout(() => {
        const first = eodOverlay.querySelector('button, [tabindex]:not([tabindex="-1"])');
        if (first) first.focus();
      }, 50);
    });

    function restoreEodFocus() {
      if (_eodTrigger) {
        _eodTrigger.focus();
        _eodTrigger = null;
      }
    }
    if (eodClose) eodClose.addEventListener('click', restoreEodFocus);
    eodOverlay.addEventListener('keydown', (event) => {
      if (event.key === 'Escape') {
        eodOverlay.classList.remove('show');
        restoreEodFocus();
      }
    });
  }

  // Expiry modal
  const expiryBtn = document.getElementById('expiryBtn');
  const expiryCancel = document.getElementById('expiryCancel');
  const expirySave = document.getElementById('expirySave');
  const expiryOverlay = document.getElementById('expiryOverlay');

  if (expiryBtn && expiryOverlay) {
    expiryBtn.addEventListener('click', () => {
      _expiryTrigger = document.activeElement;
    });

    function restoreExpiryFocus() {
      if (_expiryTrigger) {
        _expiryTrigger.focus();
        _expiryTrigger = null;
      }
    }
    if (expiryCancel) expiryCancel.addEventListener('click', restoreExpiryFocus);
    if (expirySave) expirySave.addEventListener('click', restoreExpiryFocus);
  }
})();