This forum can run locally on your device or sync via GitHub cloud-data. Use the buttons to load from cloud or propose updates via GitHub Issue.

Threads

Forum function currentAuthor(){ try{ Redirecting to /forum… return (u?.display_name || u?.email || 'user'); }catch{ return 'user'; } } function applyAuthGate(){ const authed = isAuthed(); const newBtn = document.getElementById('createThread'); const titleIn = document.getElementById('newTitle'); const composer = document.getElementById('composer'); const composerGate = document.getElementById('composerGate'); const authCta = document.getElementById('authCta'); const saveCloudBtn = document.getElementById('saveCloud'); if(!authed){ newBtn.classList.add('composer-disabled'); titleIn.classList.add('composer-disabled'); composer.classList.add('composer-disabled'); composerGate.style.display='block'; authCta.style.display='block'; authCta.innerHTML = `
Sign in to start a thread or reply. Login or Create Account
`; if(saveCloudBtn) { saveCloudBtn.classList.add('composer-disabled'); saveCloudBtn.title='Login required to propose changes'; } }else{ newBtn.classList.remove('composer-disabled'); titleIn.classList.remove('composer-disabled'); composer.classList.remove('composer-disabled'); composerGate.style.display='none'; authCta.style.display='none'; if(saveCloudBtn) { saveCloudBtn.classList.remove('composer-disabled'); saveCloudBtn.removeAttribute('title'); } } } function load(){ try{ state.threads = JSON.parse(localStorage.getItem(LS_KEY)||'[]'); }catch{ state.threads=[] } } function save(){ localStorage.setItem(LS_KEY, JSON.stringify(state.threads)); } function id(){ return Math.random().toString(36).slice(2) + Date.now().toString(36); } function renderThreads(){ const list = document.getElementById('threadList'); list.innerHTML = ''; if(state.threads.length===0){ list.innerHTML = '
No threads yet. Start one!
'; return; } state.threads.forEach(t=>{ const div = document.createElement('div'); div.className='thread'; const initials = (t.author||'U').trim()[0]?.toUpperCase() || 'U'; div.innerHTML = `
${initials}
${escapeHtml(t.title)}
Started by ${escapeHtml(t.author||'user')}${t.posts.length} postsLast activity ${new Date(t.updated_at).toLocaleString()}
`; div.onclick = (e)=>{ if(e.target.dataset.del){ deleteThread(e.target.dataset.del); e.stopPropagation(); return;} selectThread(t.id); }; list.appendChild(div); }); } function selectThread(tid){ state.selected = state.threads.find(x=>x.id===tid) || null; const title = document.getElementById('threadTitle'); const meta = document.getElementById('threadMeta'); const list = document.getElementById('postList'); const comp = document.getElementById('composer'); if(!state.selected){ title.textContent='No thread selected'; meta.textContent='Select a thread to view posts'; list.innerHTML=''; comp.style.display='none'; return; } title.textContent = state.selected.title; meta.textContent = `${state.selected.posts.length} posts • Started by ${state.selected.author||'user'} • Created ${new Date(state.selected.created_at).toLocaleString()}`; comp.style.display='block'; list.innerHTML=''; state.selected.posts.forEach(p=>{ const d = document.createElement('div'); d.className='post'; d.innerHTML = `
${escapeHtml(p.author||'user')}
${escapeHtml(p.body)}
${new Date(p.created_at).toLocaleString()}
`; d.onclick=(e)=>{ if(e.target.dataset.delp){ deletePost(p.id); e.stopPropagation(); } }; list.appendChild(d); }); } function createThread(){ if(!isAuthed()) { return; } const title = document.getElementById('newTitle').value.trim(); if(!title) return; const t = { id:id(), title, author: currentAuthor(), created_at: Date.now(), updated_at: Date.now(), posts: [] }; state.threads.unshift(t); save(); renderThreads(); selectThread(t.id); document.getElementById('newTitle').value=''; } function deleteThread(tid){ state.threads = state.threads.filter(t=>t.id!==tid); save(); renderThreads(); if(state.selected && state.selected.id===tid){ selectThread(null);} } function addPost(){ if(!isAuthed()) { return; } const body = document.getElementById('postBody').value.trim(); if(!body || !state.selected) return; state.selected.posts.push({ id:id(), body, author: currentAuthor(), created_at: Date.now() }); state.selected.updated_at = Date.now(); save(); document.getElementById('postBody').value=''; renderThreads(); selectThread(state.selected.id); } function deletePost(pid){ if(!state.selected) return; state.selected.posts = state.selected.posts.filter(p=>p.id!==pid); state.selected.updated_at = Date.now(); save(); renderThreads(); selectThread(state.selected.id); } function escapeHtml(s){ return s.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[c])); } // Export/Import function exportData(){ const blob = new Blob([JSON.stringify(state.threads,null,2)], {type:'application/json'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'pisces-forum.json'; a.click(); } async function importData(file){ const text = await file.text(); const arr = JSON.parse(text); if(Array.isArray(arr)){ state.threads = arr; save(); renderThreads(); selectThread(null); } } // Init load(); renderThreads(); document.getElementById('createThread').onclick = createThread; document.getElementById('addPost').onclick = addPost; document.getElementById('exportBtn').onclick = exportData; document.getElementById('importFile').onchange = (e)=>{ if(e.target.files[0]) importData(e.target.files[0]); }; // Cloud helpers async function loadFromCloud(){ try{ const { fetchCloudJson } = window.PiscesAICloud || {}; if(!fetchCloudJson) throw new Error('Cloud helpers not loaded'); const threads = await fetchCloudJson('data/forum/threads.json', []); const posts = await fetchCloudJson('data/forum/posts.json', []); const grouped = {}; posts.forEach(p=>{ (grouped[p.thread_id] = grouped[p.thread_id]||[]).push({ id:p.id, body:p.content||'', author:p.author||'user', created_at: Date.parse(p.created_at)||Date.now() }); }); state.threads = threads.map(t=>({ id:t.id, title:t.title, author: t.author||'user', created_at: Date.parse(t.created_at)||Date.now(), updated_at: Date.now(), posts: (grouped[t.id]||[]) })); save(); renderThreads(); selectThread(null); }catch(e){ alert('Failed to load cloud data'); console.warn(e); } } function saveToCloud(){ if(!isAuthed()) { return; } const { openCloudSyncIssue } = window.PiscesAICloud || {}; if(!openCloudSyncIssue){ alert('Cloud helpers not loaded'); return; } const threads = state.threads.map(t=>({ id:t.id, title:t.title, author:(t.author||'web'), created_at:new Date(t.created_at).toISOString() })); const posts = state.threads.flatMap(t=> t.posts.map(p=>({ id:p.id, thread_id:t.id, author:(p.author||'web'), content:p.body, created_at:new Date(p.created_at).toISOString() })) ); const updates = [ { path:'data/forum/threads.json', op:'merge', data: threads }, { path:'data/forum/posts.json', op:'merge', data: posts } ]; openCloudSyncIssue('Forum update', updates, 'Proposed by website user'); } document.getElementById('loadCloud').onclick = loadFromCloud; document.getElementById('saveCloud').onclick = saveToCloud; // Apply auth gating after DOM ready (app.js also runs DOMContentLoaded) document.addEventListener('DOMContentLoaded', applyAuthGate); // Also re-apply after a brief delay to ensure app.js updated state setTimeout(applyAuthGate, 400);