The Ripple Effect of Honesty

Student-led — create a scenario, map the ripple effects, publish, peer-review, and export. No teacher required.

Ripple Map Editor
Add effects and assign them to layers: immediate, short-term, long-term, community.
`; return page; } function votePost(id){ const posts = loadPosts(); const p=posts.find(x=>x.id===id); if(!p) return; p.upvotes = (p.upvotes||0)+1; savePosts(posts); renderPosts(); } function promptComment(id){ const name = prompt('Your name (or leave blank for Anon)'); const text = prompt('Your comment'); if(!text) return; const posts = loadPosts(); const p = posts.find(x=>x.id===id); p.comments = p.comments||[]; p.comments.push({id:randId(),name:name||'Anon',text,created:now()}); savePosts(posts); renderPosts(); } function promptRubric(id){ const clarity = Number(prompt('Clarity (0-3)',2)); const connections = Number(prompt('Connections (0-3)',2)); const depth = Number(prompt('Depth (0-4)',2)); const posts = loadPosts(); const p = posts.find(x=>x.id===id); p.rubric = {clarity,connections,depth}; savePosts(posts); renderPosts(); alert('Rubric saved locally.'); } function promptEdit(id){ const posts = loadPosts(); const p = posts.find(x=>x.id===id); if(!p) return; const action = prompt('Type EDIT to edit, DELETE to delete'); if(!action) return; if(action.toUpperCase()==='EDIT'){ const pass = prompt('Enter your passcode to edit'); if(hashPasscode(pass)!==p.passHash) return alert('Wrong passcode'); const newTitle = prompt('New title',p.title); const newScenario = prompt('New scenario',p.scenario); if(newTitle) p.title=newTitle; if(newScenario) p.scenario=newScenario; savePosts(posts); renderPosts(); alert('Edited.'); } else if(action.toUpperCase()==='DELETE'){ const pass = prompt('Enter your passcode to delete'); if(hashPasscode(pass)!==p.passHash) return alert('Wrong passcode'); const idx = posts.findIndex(x=>x.id===id); posts.splice(idx,1); savePosts(posts); renderPosts(); alert('Deleted.'); } } // Wire UI el('addEffect').onclick = ()=>{ const text = document.getElementById('effectText').value.trim(); const layer = document.getElementById('layerSelect').value; if(!text) return; mapEffects.push({id:randId(),text,layer}); document.getElementById('effectText').value=''; renderRippleCanvas(); } document.querySelectorAll('[data-layer-preview]').forEach(b=>b.onclick = ()=>renderRippleCanvas(b.getAttribute('data-layer-preview'))); el('showAll').onclick = ()=>renderRippleCanvas(); el('preview').onclick = ()=>renderRippleCanvas(); el('publish').onclick = publishPost; el('clear').onclick = ()=>{ if(confirm('Clear the form and map?')){ el('title').value='';el('scenario').value='';el('passcode').value=''; mapEffects=[]; renderRippleCanvas(); }} el('refreshPosts').onclick = renderPosts; el('filter').onchange = renderPosts; el('exportAll').onclick = ()=>{ const data = JSON.stringify(loadPosts(),null,2); const blob = new Blob([data],{type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href=url; a.download='ripple_posts.json'; a.click(); URL.revokeObjectURL(url); } el('exportMap').onclick = ()=>{ const data = JSON.stringify(mapEffects,null,2); const blob = new Blob([data],{type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href=url; a.download='ripple_map.json'; a.click(); URL.revokeObjectURL(url); } // initial render renderRippleCanvas(); renderPosts();