/* ========================================================= WATCHLIST app.js ========================================================= */ /* State */ const state = { filter: 'all', type: '', forWhom: '', service: '', search: '', items: [], stats: { total: 0, watched: 0, unwatched: 0 }, services: [], }; /* DOM refs */ const grid = document.getElementById('grid'); const loadingState = document.getElementById('loadingState'); const statTotal = document.getElementById('statTotal'); const statUnwatched = document.getElementById('statUnwatched'); const statWatched = document.getElementById('statWatched'); const progressFill = document.getElementById('progressFill'); const statPercent = document.getElementById('statPercent'); const serviceRow = document.getElementById('serviceRow'); const searchInput = document.getElementById('searchInput'); // Modal Add/Edit const modalOverlay = document.getElementById('modalOverlay'); const modal = document.getElementById('modal'); const modalTitle = document.getElementById('modalTitle'); const openAddModal = document.getElementById('openAddModal'); const openAddModalEmpty = document.getElementById('openAddModalEmpty'); const closeModal = document.getElementById('closeModal'); const cancelModal = document.getElementById('cancelModal'); const addForm = document.getElementById('addForm'); const editId = document.getElementById('editId'); const inputTitle = document.getElementById('inputTitle'); const inputService = document.getElementById('inputService'); const inputGenre = document.getElementById('inputGenre'); const inputNotes = document.getElementById('inputNotes'); const submitBtn = document.getElementById('submitBtn'); const serviceList = document.getElementById('serviceList'); // Modal Delete const deleteOverlay = document.getElementById('deleteOverlay'); const closeDeleteModal = document.getElementById('closeDeleteModal'); const cancelDelete = document.getElementById('cancelDelete'); const confirmDelete = document.getElementById('confirmDelete'); const deleteTitle = document.getElementById('deleteTitle'); // Filter pills const filterGroup = document.getElementById('filterGroup'); const typeGroup = document.getElementById('typeGroup'); const forGroup = document.getElementById('forGroup'); // Toast const toastContainer = document.getElementById('toastContainer'); /* Type & For toggle state (modal) */ let selectedType = 'movie'; let selectedForWhom = 'all'; /* API helper */ async function api(params = {}, body = null) { const url = new URL('api.php', location.href); Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v)); const opts = body ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } : { method: 'GET' }; const res = await fetch(url, opts); return res.json(); } /* Fetch & render */ async function loadItems() { showLoading(true); try { const data = await api({ action: 'list', filter: state.filter, type: state.type, for_whom: state.forWhom, service: state.service, search: state.search, }); if (!data.success) throw new Error(data.error || 'Unknown error'); state.items = data.items || []; state.stats = data.stats || { total: 0, watched: 0, unwatched: 0 }; state.services = data.services || []; renderStats(); renderServiceRow(); renderGrid(); populateServiceDatalist(); } catch (err) { console.error(err); toast('Failed to load watchlist', 'error'); } finally { showLoading(false); } } function showLoading(on) { loadingState.style.display = on ? 'flex' : 'none'; } /* Render Stats */ function renderStats() { const { total, watched, unwatched } = state.stats; const t = parseInt(total) || 0; const w = parseInt(watched) || 0; const u = parseInt(unwatched) || 0; statTotal.textContent = t; statWatched.textContent = w; statUnwatched.textContent = u; const pct = t > 0 ? Math.round((w / t) * 100) : 0; progressFill.style.width = pct + '%'; statPercent.textContent = pct + '% complete'; } /* Render Service Row */ function renderServiceRow() { serviceRow.innerHTML = ''; if (!state.services.length) return; const all = chip('All Services', '', state.service === ''); all.addEventListener('click', () => { state.service = ''; loadItems(); }); serviceRow.appendChild(all); state.services.forEach(s => { const c = chip(s.streaming_service, s.streaming_service, state.service === s.streaming_service); c.addEventListener('click', () => { state.service = state.service === s.streaming_service ? '' : s.streaming_service; loadItems(); }); serviceRow.appendChild(c); }); } function chip(label, value, active) { const btn = document.createElement('button'); btn.className = 'service-chip' + (active ? ' active' : ''); btn.textContent = label; btn.dataset.value = value; return btn; } function populateServiceDatalist() { serviceList.innerHTML = ''; state.services.forEach(s => { const opt = document.createElement('option'); opt.value = s.streaming_service; serviceList.appendChild(opt); }); } /* Render Grid */ function renderGrid() { // Remove old cards (keep loading state) Array.from(grid.children).forEach(child => { if (child.id !== 'loadingState') child.remove(); }); if (!state.items.length) { renderEmpty(); return; } state.items.forEach(item => { grid.appendChild(buildCard(item)); }); } function renderEmpty() { const div = document.createElement('div'); div.className = 'empty-state'; div.innerHTML = `
Add your first title to get started.
`; grid.appendChild(div); div.querySelector('#openAddModalEmpty')?.addEventListener('click', openAdd); } /* Build Card */ function buildCard(item) { const watched = item.watched == 1; const isMovie = item.type === 'movie'; // For badge label const forLabels = { all: 'All', nik: 'Nik', tod: 'Tod' }; const forLabel = forLabels[item.for_whom] || 'All'; const showForBadge = item.for_whom && item.for_whom !== 'all'; // Format date const dateStr = item.watched_at ? 'Watched ' + formatDate(item.watched_at) : 'Added ' + formatDate(item.created_at); const card = document.createElement('div'); card.className = 'card' + (watched ? ' watched' : ''); card.dataset.id = item.id; card.innerHTML = `${escHtml(item.genre)}
` : ''} ${item.notes ? `${escHtml(item.notes)}
` : ''} ${dateStr}