LEVEL UP!
Click anywhere to close
RPG QUEST BOARD
🧙‍♂️
Adventurer — The Novice LV.1 Code Conjurer
💰0g
All 🔴 Main 🟡 Side 🟢 Guild 0 quests 0 done
🧙‍♂️
Adventurer — The Novice
LV.1 Code Conjurer
XP Progress
0 / 1000
0
Quests
0
Done
0
Main
0g
Gold
⚔️ Loading your quest report…
⚡ Due Today
No quests due today 🎉
📅 This Week
No quests this week
🎯 Daily Bounties
💾
Haven't backed up in a while! Don't lose your quest progress — export a backup now.
📊 Quests Completed — Last 7 Days
🔥 30-Day Streak Map
🏃 Today's Habits
🏆 Top 3 Today
👥 Rival — Yesterday's You
⚡ Daily Challenge
⚡ Quick Capture
Brain-dump tasks — one per line. Keywords auto-assign priority: fix/bug → high, idea → low
📝 Notes & Journal

May 2026

📥 Import Calendar — Meetings as Quests
Import a .ics file from any calendar app. Events appear on the grid below and can be converted to quests with one click.
📱 Google Calendar: Settings → Import & Export → Export  ·  🍎 Apple Calendar: File → Export → Export  ·  📧 Outlook: File → Save As → .ics
🛡️ Hero Profile
🧙‍♂️
Adventurer — The Novice
0 / 1000 XP
🗡️STR — Bug Smashing10
🧙‍♂️INT — Feature Craft10
🏹DEX — Clean Deploy10
🦊CHA — Monetisation10
Equipped Gear
🚫Weapon
🚫Armor
🚫Trinket
🎒 Inventory Bag
🎯 Daily Bounties
⚙️ Settings & Actions
🛒 Visit the Market
🎨 Active Theme
🔔 Push Notifications
☀️ Morning Check-In
📤 What I Shipped (HTML portfolio)
☁️ GitHub Gist Sync — Cloud Backup
Sync your board to a private GitHub Gist. Your data never touches any server except GitHub. Create a Gist token →
🐉 Assemble a Guild Raid
Completing quests deals damage: low=25, med=50, high=150 HP per quest moved to Done.

⚔️ Coffee-Break Dungeon

=== DUNGEON PORTAL ===
Complete real quests to boost your stats, then return here to slay monsters on your break!

📝 Notes Book
📝 Select a note on the left, or create a new one.

Notes can be linked to quest cards. Perfect for planning, research, meeting notes, or any idea.
Quest 1 of 1
25:00
FOCUS

🛒 The Market

💰 0g
⚡ Active Buffs (click × to cancel early)
No active buffs — visit the shop below!

🌟 Daily Life

Self-care directly powers your RPG stats. Medicine ✓ → health stays up. Exercise ✓ → STR grows. Mental care ✓ → INT grows. Full completion = ×1.2 XP all day.
+ Add Life Quest
💡 Set a time for Medicine items to get browser reminder notifications when they're due.

🏃 Daily Habits

Complete habits every day to build streaks and earn XP. Streaks multiply your rewards.
Add New Habit
⌨️ Keyboard Shortcuts
New QuestCtrlK
New NoteCtrlN
Global SearchCtrl/
Quest TemplatesCtrlT
Close / CancelEsc
Show this panel?
Mobile Gestures
Complete questSwipe → right
Edit questSwipe → left
Open quest detailTap title

⚔️ What I Shipped

JV Design Studio · ${evoClass} · ${new Date().toLocaleDateString('en-GB',{month:'long',year:'numeric'})}

${done.length}
Quests Complete
${rpg.level}
Level
${rpg.streak||0}
Day Streak
${Object.keys(rpg.achievements||{}).length}
Achievements
${['fix','build','deploy','workshop','videos','books','games','covers'].map(col=>{ const items=done.filter(c=>c.colId===col);if(!items.length)return''; const colTitle=state.columns.find(x=>x.id===col)?.title||col; return`
${colTitle}
${items.map(c=>`
${c.title}
${c.desc?`
${c.desc.replace(/- \[.\] /g,'').slice(0,120)}${c.desc.length>120?'…':''}
`:''}
${(c.tags||[]).map(t=>`${t.text}`).join('')}
`).join('')}`; }).join('')} `; const blob=new Blob([html],{type:'text/html'}); const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='what-i-shipped-'+_dateStr(new Date())+'.html';a.click(); showToast('📤 Shipped portfolio exported!');playSfx('quest'); } /* ── 11. MOTIVATIONAL HUD MESSAGE ─────────────────────────────────────────── */ const _MOTIVATIONS=[ r=>r.streak>=30?`💎 ${r.streak}-day streak. You are a machine.`:null, r=>r.streak>=14?`🔥 ${r.streak} days straight. Most people quit by now.`:null, r=>r.streak>=7?`⚡ Week-long streak. This is where habits are built.`:null, r=>r.streak===1?`Day 1 of a new streak. Don't stop now.`:null, (r,od)=>od>=5?`💀 ${od} quests overdue. Time to slay some dragons.`:null, (r,od)=>od>0&&od<5?`⚔️ ${od} quest${od>1?'s':''} overdue. Today's the day.`:null, r=>r.level>=15?`🌟 Level ${r.level}. You've shipped things that live on the internet.`:null, r=>r.level>=10?`🏆 Level ${r.level}. A decade of quests. The board knows your name.`:null, r=>r.gold>=1000?`💰 ${r.gold}g in the vault. The treasury grows.`:null, ()=>`⚔️ Every quest completed is one less thing between you and where you want to be.`, ]; function renderMotivation(){ const el=document.getElementById('motivationMsg');if(!el)return; const today=_dateStr(new Date()); const overdue=state.cards.filter(c=>c.colId!=='done'&&c.date&&c.datex.id===id);if(!item)return; const today=new Date().toDateString(); if(item.lastDone===today){showToast('Already done today — well done! 💪');return;} const yesterday=new Date(Date.now()-86400000).toDateString(); item.streak=item.lastDone===yesterday?(item.streak||0)+1:1; item.lastDone=today; // Stat buff for category const cat=LIFE_CATS[item.category]; if(cat?.stat)rpg.stats[cat.stat]=Math.min(rpg.stats[cat.stat]+1,99); flashScreen();playSfx('quest'); const streakBonus=Math.min(Math.floor(item.streak/7)*10,60); const xpEarned=item.xp+streakBonus; spawnXpFloat(`+${xpEarned} XP`,'gold'); if(item.streak>1)setTimeout(()=>spawnXpFloat(`💖 ${item.streak}-day streak!`,'purple'),300); showToast(`✅ ${item.name.slice(0,28)}! +${xpEarned} XP${streakBonus?' (streak bonus!)':''}`); addXp(item.xp);save();renderLifeTab();updateLifeBuffs();checkMedReminders(); } function deleteLifeItem(id){if(!confirm('Remove this life quest?'))return;rpg.selfCare.items=rpg.selfCare.items.filter(x=>x.id!==id);save();renderLifeTab();} function renderLifeTab(){ _initSelfCare(); const grid=document.getElementById('lifeGrid');if(!grid)return; const today=new Date().toDateString();grid.innerHTML=''; Object.entries(LIFE_CATS).forEach(([key,cat])=>{ const items=rpg.selfCare.items.filter(x=>x.category===key); const div=document.createElement('div');div.className='life-category'; div.innerHTML=`
${cat.label}
${items.length?'':'
Nothing yet
'}`; items.forEach(item=>{ const done=item.lastDone===today; const isDue=item.time&&_isTimeDue(item.time)&&!done; const row=document.createElement('div');row.className='life-item'; row.innerHTML=`
${done?'✓':''}
${escHtml(item.name)}
${item.xp}XP${item.time?`${isDue?'⚠️ DUE NOW':'⏰ '+item.time}`:''}🔥${item.streak||0}
`; div.appendChild(row); }); grid.appendChild(div); }); updateLifeBuffs(); } function _isTimeDue(t){ const now=new Date();const[h,m]=t.split(':').map(Number); const due=new Date();due.setHours(h,m,0,0); return now>=due&&(now-due)<3600000; } function updateLifeBuffs(){ _initSelfCare(); const today=new Date().toDateString(); const doneCount=(rpg.selfCare.items||[]).filter(x=>x.lastDone===today).length; const total=Math.max(rpg.selfCare.items.length,1); const pct=Math.round(doneCount/total*100); const buffs=[]; if(pct>=25)buffs.push({t:'✅ +5% XP',c:'pos'}); if(pct>=50)buffs.push({t:'💪 +STR',c:'pos'}); if(pct>=75)buffs.push({t:'🧘 +INT',c:'pos'}); if(pct>=100)buffs.push({t:'✨ Full Life: ×1.2 XP',c:'pos'}); if(pct<25&&total>=3)buffs.push({t:'⚠️ Self-care needed',c:'neg'}); ['lifeBuffDisplay','lifeBufsHero'].forEach(id=>{ const el=document.getElementById(id); if(el)el.innerHTML=buffs.map(b=>`${b.t}`).join(''); }); } function checkMedReminders(){ _initSelfCare(); if(!rpg.notificationsEnabled||Notification.permission!=='granted')return; const today=new Date().toDateString(); (rpg.selfCare.items||[]).filter(x=>x.category==='medicine'&&x.time&&x.lastDone!==today&&_isTimeDue(x.time)).forEach(item=>{ new Notification('💊 Medicine Reminder',{body:`Time for: ${item.name}`,icon:'/manifest.json',tag:'med-'+item.id}); }); } /* ── MORNING CHECK-IN ────────────────────────────────────────────────────────── */ let _ciMood=3,_ciSleep=3; function triggerCheckIn(){ _initSelfCare(); const today=new Date().toDateString(); if(rpg.selfCare.checkInDate===today)return; // already done today _ciMood=3;_ciSleep=3; // Pre-select defaults setTimeout(()=>{setCheckMood(3);setCheckSleep(3);},50); // Show medicine quick-tick const meds=(rpg.selfCare.items||[]).filter(x=>x.category==='medicine'); const ms=document.getElementById('checkInMeds'); if(ms&&meds.length){ ms.innerHTML=`
💊 Take your medicine?
`+ meds.map(m=>``).join(''); } else if(ms){ms.innerHTML='';} _updateCiPreview(); document.getElementById('checkInModal').classList.add('open'); } function setCheckMood(v){ _ciMood=v; document.querySelectorAll('#moodRow .mood-emoji').forEach(el=>el.classList.toggle('selected',+el.dataset.v===v)); _updateCiPreview(); } function setCheckSleep(v){ _ciSleep=v; document.querySelectorAll('#sleepRow .sleep-btn').forEach(el=>el.classList.toggle('selected',+el.dataset.v===v)); _updateCiPreview(); } function _updateCiPreview(){ const xp=(_ciMood-1)*10+(_ciSleep-1)*10; const m=_ciMood>=4?'✨ Good vibes — ×1.1 XP today':_ciMood<=2?'⚠️ Tough day — but keep going!':''; const el=document.getElementById('checkInBuffPreview'); if(el)el.innerHTML=`+${xp} XP${m?` · ${m}`:''}`; } function completeCheckIn(){ _initSelfCare(); const today=new Date().toDateString(); rpg.selfCare.checkInDate=today; if(!rpg.selfCare.mood)rpg.selfCare.mood={}; rpg.selfCare.mood[today]={mood:_ciMood,sleep:_ciSleep}; const xp=(_ciMood-1)*10+(_ciSleep-1)*10; if(xp>0){addXp(xp);spawnXpFloat(`+${xp} XP`,'gold');} // Tick checked medicines document.querySelectorAll('[id^="ci_med_"]').forEach(cb=>{if(cb.checked)tickLifeItem(cb.id.replace('ci_med_',''));}); flashScreen();playSfx('levelup'); document.getElementById('checkInModal').classList.remove('open'); showToast(`☀️ Day started! ${xp?'+'+xp+' XP — ':''} Let's go!`); save();renderDash(); } /* ── CLASS EVOLUTION ─────────────────────────────────────────────────────────── */ const CLASS_PATHS={ 'Code Conjurer': ['Code Conjurer','Code Wizard','Code Archmage','Code Legend','⚡ Transcendent Dev'], 'Pixel Paladin': ['Pixel Paladin','Pixel Knight','Pixel Champion','Pixel Legend','🗡️ Legendary Warrior'], 'Design Druid': ['Design Druid','Grove Keeper','Arch Druid','Elder Druid','🌿 Nature Sage'], 'QA Ranger': ['QA Ranger','Eagle Scout','Ranger Captain','Ranger Legend','🏹 Ghost Ranger'], }; const CLASS_LEVEL_REQ=[0,5,10,15,20]; function getEvolvedClass(){ const path=CLASS_PATHS[rpg.class]||CLASS_PATHS['Code Conjurer']; const tier=CLASS_LEVEL_REQ.filter(t=>rpg.level>=t).length-1; return path[Math.min(tier,path.length-1)]; } function renderClassPath(){ const el=document.getElementById('classPath');if(!el)return; const path=CLASS_PATHS[rpg.class]||CLASS_PATHS['Code Conjurer']; el.innerHTML=path.map((c,i)=>{ const achieved=rpg.level>=CLASS_LEVEL_REQ[i]; const isCurrent=i===Math.min(CLASS_LEVEL_REQ.filter(t=>rpg.level>=t).length-1,path.length-1); return `${c}${i→':''}`; }).join(''); } /* ── STORY CHAPTERS ──────────────────────────────────────────────────────────── */ const STORY_CHAPTERS=[ {level:1, title:'The Awakening', text:'"A lone creator opens a blank board. The cursor blinks. The adventure begins here."'}, {level:3, title:'First Victories', text:'"Three quests fall. The XP bar flickers. Something ancient stirs in the server room."'}, {level:5, title:'Class Ascension', text:'"Power flows through your fingertips. Your class has evolved. The board feels different now."'}, {level:7, title:'The Grind', text:'"Days blur into nights. Code, create, ship, repeat. The streak counter ticks upward."'}, {level:10,title:'Mastery', text:'"Level 10. You have shipped things that live on the internet. Other travellers visit them."'}, {level:15,title:'Legend Rising', text:'"The board knows your name. Boss quests fear you. The daily bounties feel… easy."'}, {level:20,title:'Transcendence', text:'"Level 20. You built things that matter. This is not the end — it is the prestige unlock."'}, ]; function renderStoryChapter(targetId){ const el=document.getElementById(targetId);if(!el)return; const ch=[...STORY_CHAPTERS].reverse().find(s=>rpg.level>=s.level)||STORY_CHAPTERS[0]; const unlocked=STORY_CHAPTERS.filter(s=>rpg.level>=s.level).length; el.innerHTML=`
📖 ${ch.title}
${ch.text}
${unlocked}/${STORY_CHAPTERS.length} chapters unlocked · LV.${rpg.level}
`; } /* ── WEEKLY BOSS EVENT ───────────────────────────────────────────────────────── */ const WEEKLY_BOSSES=[ {name:'🐉 The Procrastination Dragon', tasks:[{type:'any',target:5,label:'Complete 5 quests'}]}, {name:'👹 The Bug Troll King', tasks:[{type:'fix',target:3,label:'Crush 3 bug quests'}]}, {name:'🤖 The Content Golem', tasks:[{type:'videos',target:2,label:'Finish 2 video quests'}]}, {name:'💀 The Deadline Lich', tasks:[{type:'high',target:4,label:'Complete 4 main quests'}]}, {name:'🐙 Scope Creep Incarnate', tasks:[{type:'build',target:3,label:'Build 3 things'}]}, ]; function checkWeeklyBoss(){ _initSelfCare(); const now=new Date(); const monday=new Date(now);monday.setDate(now.getDate()-((now.getDay()+6)%7));monday.setHours(0,0,0,0); const boss=rpg.selfCare.weeklyBoss; if(!boss||!boss.week||new Date(boss.week)({...t,current:0})),hp:100,maxHp:100,slain:false}; save(); } } function updateWeeklyBossProgress(){ _initSelfCare(); const boss=rpg.selfCare.weeklyBoss;if(!boss||boss.slain)return; const doneCards=state.cards.filter(c=>c._wasDone&&c.colId==='done'); boss.tasks.forEach(t=>{ if(t.type==='any')t.current=Math.min(doneCards.length,t.target); else if(t.type==='fix')t.current=Math.min(doneCards.filter(c=>c.colId==='fix'||c.tags?.some(x=>x.text.toLowerCase()==='bug')).length,t.target); else if(t.type==='high')t.current=Math.min(doneCards.filter(c=>c.priority==='high').length,t.target); else t.current=Math.min(doneCards.filter(c=>c.colId===t.type).length,t.target); }); const allDone=boss.tasks.every(t=>t.current>=t.target); if(allDone&&!boss.slain){ boss.slain=true;const xp=1500,g=250; addXp(xp);addGold(g);save(); setTimeout(()=>{ document.getElementById('celebrateTitle').textContent='⚔️ WEEKLY BOSS SLAIN!'; document.getElementById('celebrateSub').innerHTML=`${boss.name} defeated!
+${xp} XP  +${g}g`; document.getElementById('celebrateOverlay').classList.add('show'); startConfetti();playSfx('levelup'); },600); } boss.hp=allDone?0:Math.max(1,Math.round((1-(boss.tasks.reduce((a,t)=>a+t.current,0)/boss.tasks.reduce((a,t)=>a+t.target,0)))*100)); save(); } function renderWeeklyBoss(){ const el=document.getElementById('weeklyBossPanel');if(!el)return; _initSelfCare();const boss=rpg.selfCare.weeklyBoss; if(!boss){el.innerHTML='';return;} el.innerHTML=`
${boss.name}
${boss.slain?'✅ SLAIN this week! New boss Monday.':'Defeat by completing tasks below — resets every Monday'}
${!boss.slain?`
BOSS HP${boss.hp}%
`:''} ${boss.tasks.map(t=>`
${t.label}
${t.current}/${t.target}
`).join('')}
`; } /* ── EXPANDED ACHIEVEMENTS ───────────────────────────────────────────────────── */ function spawnXpFloat(text,type='gold'){ const el=document.createElement('div'); el.className='xp-float'; const cols={gold:'#f5c842',green:'#22c55e',purple:'#a855f7'}; el.style.color=cols[type]||cols.gold; el.style.textShadow=`0 0 14px ${cols[type]||cols.gold}`; el.textContent=text; const x=window.innerWidth/2-80+Math.random()*160; const y=window.innerHeight/2-60+Math.random()*80; el.style.cssText+=`;left:${x}px;top:${y}px;`; document.body.appendChild(el); setTimeout(()=>el.remove(),1600); } function flashScreen(){ const el=document.createElement('div'); el.className='screen-flash'; document.body.appendChild(el); setTimeout(()=>el.remove(),750); } function showCombo(count){ document.querySelectorAll('.combo-pop').forEach(e=>e.remove()); const el=document.createElement('div'); el.className='combo-pop'; const msgs={3:'⚡ COMBO ×3!',4:'🔥 COMBO ×4!',5:'💥 COMBO ×5!',6:'🌟 COMBO ×6!',7:'✨ LEGENDARY COMBO!'}; el.textContent=msgs[Math.min(count,7)]||`💀 COMBO ×${count}!`; document.body.appendChild(el); setTimeout(()=>el.remove(),3500); } function updateStreakDisplay(){ const el=document.getElementById('hqStreak');if(!el)return; if((rpg.streak||0)>1){ const mult=getXpMultiplier(); el.className='streak-badge'; el.style.display='inline-flex'; el.innerHTML=`🔥 ${rpg.streak}-day streak! ×${mult} XP`; } else { el.style.display='none'; } } function checkBossThreats(){ if(!rpg.bossThreats)rpg.bossThreats={}; const now=Date.now(); const twoDays=172800000; const bosses=state.cards.filter(c=>c.isBoss&&c.colId!=='done'); bosses.forEach(c=>{ if(!rpg.bossThreats[c.id])rpg.bossThreats[c.id]=now; else if(now-rpg.bossThreats[c.id]>twoDays){ const drain=Math.max(5,Math.floor(rpg.gold*.05)); addGold(-drain); showToast(`😈 "${c.title}" attacked while ignored! −${drain}g`); spawnXpFloat(`−${drain}g`,'purple'); rpg.bossThreats[c.id]=now; } }); // Clear completed bosses from threat log Object.keys(rpg.bossThreats).forEach(id=>{ const c=state.cards.find(x=>x.id===id); if(!c||c.colId==='done')delete rpg.bossThreats[id]; }); save(); } function dailyLoginBonus(){ const today=new Date().toDateString(); if(rpg.loginDate===today)return; rpg.loginDate=today; const streakBonus=Math.min((rpg.streak||0)*5,100); const bonus=50+streakBonus; addGold(bonus); save(); setTimeout(()=>{ const el=document.createElement('div'); el.className='daily-bonus-pop'; el.innerHTML=`
☀️
Daily Login!
+${bonus}g${streakBonus?` (includes 🔥 streak bonus!)`:''}
Click to close
`; el.onclick=()=>el.remove(); document.body.appendChild(el); spawnXpFloat(`+${bonus}g`,'green'); setTimeout(()=>el.remove(),5000); },600); } /* ── NOTES ── */ let activeNoteId=null; function openNoteModal(id=null){ activeNoteId=id; const note=id?(rpg.notes||[]).find(n=>n.id===id):null; document.getElementById('noteModalTitle').textContent=id?'✏️ Edit Note':'📝 New Note'; document.getElementById('noteTitleInput').value=note?note.title:''; document.getElementById('noteBodyInput').value=note?note.body:''; const sel=document.getElementById('noteLinkCard'); sel.innerHTML=''; state.cards.filter(c=>c.colId!=='done').forEach(c=>{sel.innerHTML+=``;}); if(note&¬e.linkedCardId)sel.value=note.linkedCardId; document.getElementById('noteModal').classList.add('open'); setTimeout(()=>document.getElementById('noteTitleInput').focus(),100); } function closeNoteModal(){document.getElementById('noteModal').classList.remove('open');} function saveNote(){ const title=document.getElementById('noteTitleInput').value.trim(); const body=document.getElementById('noteBodyInput').value.trim(); if(!title&&!body){showToast('Add a title or some content!');return;} if(!rpg.notes)rpg.notes=[]; const linkedCardId=document.getElementById('noteLinkCard').value; const dateStr=new Date().toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric'}); if(activeNoteId){ const n=rpg.notes.find(x=>x.id===activeNoteId); if(n){n.title=title||body.slice(0,50);n.body=body;n.linkedCardId=linkedCardId;n.updated=dateStr;} }else{ rpg.notes.unshift({id:'n'+Date.now(),title:title||body.slice(0,50),body,linkedCardId,date:dateStr,updated:dateStr}); playSfx('coin'); } save();renderNotesList();closeNoteModal(); showToast(activeNoteId?'📝 Note updated!':'📝 Note saved!'); searchNotes(); } function deleteNote(id){ if(!confirm('Delete this note?'))return; rpg.notes=(rpg.notes||[]).filter(n=>n.id!==id); if(activeNoteId===id){ activeNoteId=null; const ed=document.getElementById('notesEditor'); if(ed)ed.innerHTML='
📝 Select a note or create a new one.
'; } save();renderNotesList();showToast('🗑 Note deleted'); } function openNote(id){ activeNoteId=id; const n=(rpg.notes||[]).find(x=>x.id===id);if(!n)return; const linked=n.linkedCardId?state.cards.find(c=>c.id===n.linkedCardId):null; const ed=document.getElementById('notesEditor');if(!ed)return; ed.innerHTML=`
${escHtml(n.title)}
${n.updated||n.date}${linked?`  ·  🔗 ${escHtml(linked.title)}`:''}
${linked?``:''}
${escHtml(n.body).replace(/\n/g,'
')}
`; document.querySelectorAll('.note-item').forEach(el=>el.classList.remove('active')); const item=document.querySelector(`.note-item[data-id="${id}"]`); if(item)item.classList.add('active'); } function renderNotesList(filter=''){ const list=document.getElementById('notesList');if(!list)return; list.innerHTML=''; const notes=(rpg.notes||[]).filter(n=>{ if(!filter)return true; const q=filter.toLowerCase(); return n.title.toLowerCase().includes(q)||n.body.toLowerCase().includes(q); }); if(!notes.length){list.innerHTML=`
${filter?'No notes match':'No notes yet — create one!'}
`;return;} notes.forEach(n=>{ const linked=n.linkedCardId?state.cards.find(c=>c.id===n.linkedCardId):null; const el=document.createElement('div'); el.className='note-item'+(activeNoteId===n.id?' active':''); el.dataset.id=n.id; el.innerHTML=`
${escHtml(n.title)}
${escHtml(n.body.slice(0,90))}${n.body.length>90?'…':''}
${n.updated||n.date}${linked?`  ·  🔗 ${escHtml(linked.title)}`:''}
`; el.onclick=()=>openNote(n.id); list.appendChild(el); }); } function filterNotesList(){ const q=document.getElementById('notesSideSearch')?.value||''; renderNotesList(q); } function searchNotes(){ const q=(document.getElementById('noteSearchInput')?.value||'').toLowerCase(); const preview=document.getElementById('notesPreview');if(!preview)return; preview.innerHTML=''; const notes=(rpg.notes||[]).filter(n=>!q||n.title.toLowerCase().includes(q)||n.body.toLowerCase().includes(q)); if(!notes.length){ preview.innerHTML=`
${q?'No notes match':(rpg.notes&&rpg.notes.length?'All notes shown below':'No notes yet — add your first one!')}
`; return; } notes.slice(0,5).forEach(n=>{ const el=document.createElement('div');el.className='note-item'; el.innerHTML=`
${escHtml(n.title)}
${escHtml(n.body.slice(0,70))}${n.body.length>70?'…':''}
`; el.onclick=()=>{switchView('notes');setTimeout(()=>openNote(n.id),80);}; preview.appendChild(el); }); } /* ── iCAL IMPORT ─────────────────────────────────────────────────────────────── */ let _parsedIcalEvents=[]; function handleIcalFile(e){ const file=e.target.files[0];if(!file)return; const reader=new FileReader(); reader.onload=ev=>{ try{ _parsedIcalEvents=parseIcal(ev.target.result); if(!_parsedIcalEvents.length){showToast('❌ No events found in this file');return;} openIcalModal(_parsedIcalEvents); // Also store them for calendar display if(!rpg.calEvents)rpg.calEvents=[]; document.getElementById('icalStatus').textContent=`${_parsedIcalEvents.length} events loaded from ${file.name}`; }catch(err){showToast('❌ Could not parse file: '+err.message);} e.target.value=''; }; reader.readAsText(file); } function parseIcal(text){ // RFC 5545 — unfold lines (CRLF + whitespace = continuation) text=text.replace(/\r\n[ \t]/g,'').replace(/\r\n/g,'\n').replace(/\r/g,'\n').replace(/\n[ \t]/g,''); const events=[]; // Parse a block of lines into a key→value object function parseBlock(block){ const obj={}; block.split('\n').filter(l=>l.trim()).forEach(line=>{ const ci=line.indexOf(':');if(ci===-1)return; const rawKey=line.slice(0,ci); const key=rawKey.split(';')[0].toUpperCase(); const val=line.slice(ci+1) .replace(/\\n/gi,'\n').replace(/\\N/g,'\n') .replace(/\\,/g,',').replace(/\\;/g,';').replace(/\\\\/g,'\\').trim(); if(!obj[key])obj[key]=val; // keep first occurrence // Preserve TZID param if present if(rawKey.toUpperCase().startsWith('DTSTART')&&rawKey.includes('TZID'))obj._DTSTART_RAW=val; }); return obj; } // VEVENT blocks const vevents=text.split('BEGIN:VEVENT'); for(let i=1;ia.start>b.start?1:a.start{ const isFuture=!ev.start||ev.start>=today; const typeLabel=ev.isTodo?'📋 Task':'📅 Event'; const timeLabel=ev.startTime?` · ${ev.startTime}`:''; return `
${ev.start||'No date'}
${escHtml(ev.title)}
${typeLabel}${timeLabel}${ev.location?' · 📍 '+escHtml(ev.location):''}${ev.desc?'
'+escHtml(ev.desc.slice(0,80))+(ev.desc.length>80?'…':''):''}
`; }).join(''); document.getElementById('icalModal').classList.add('open'); } function icalSelectAll(v){document.querySelectorAll('[id^="iev_"]').forEach(cb=>cb.checked=v);} function icalFilterFuture(){ const today=_dateStr(new Date()); _parsedIcalEvents.forEach((ev,i)=>{const cb=document.getElementById('iev_'+i);if(cb)cb.checked=!ev.start||ev.start>=today;}); } function importSelectedIcal(){ const selected=_parsedIcalEvents.filter((_,i)=>document.getElementById('iev_'+i)?.checked); if(!selected.length){showToast('Select at least one event!');return;} let added=0; selected.forEach(ev=>{ // Skip if already imported (match by id) if(state.cards.some(c=>c._icalId===ev.id))return; const lo=(ev.title+' '+ev.desc).toLowerCase(); // Smart column routing let colId='build'; if(lo.match(/\b(meet|sync|call|standup|review|planning|retro|1:1|interview|demo)\b/))colId='build'; if(lo.match(/\b(video|film|record|stream|youtube)\b/))colId='videos'; if(lo.match(/\b(doctor|dentist|physio|appointment|hospital|clinic)\b/))colId='build'; // Auto-priority: if it has a time it's a real event → high const priority=ev.startTime?'high':'med'; const desc=[ ev.isTodo?'📋 Imported task':'📅 Imported from calendar', ev.startTime?`⏰ ${ev.startTime}`:'', ev.location?`📍 ${ev.location}`:'', ev.desc||'', ].filter(Boolean).join('\n'); state.cards.push({ id:'cal'+(++state.nextId),colId,title:ev.title,desc, priority,date:ev.start||'',tags:[{text:ev.isTodo?'Task':'Meeting',col:'#0ea5e9'}], comments:[],isBoss:false,prereqId:'',repeat:'none',timeLogs:[],_icalId:ev.id }); added++; }); // Save to rpg.calEvents for calendar dot display if(!rpg.calEvents)rpg.calEvents=[]; selected.forEach(ev=>{if(!rpg.calEvents.find(x=>x.id===ev.id))rpg.calEvents.push(ev);}); save();render();renderCalendar();renderIcalUpcoming(); document.getElementById('icalModal').classList.remove('open'); showToast(`⚔️ ${added} event${added!==1?'s':''} imported as quests!`); if(added)playSfx('quest'); } function clearIcalEvents(){ if(!confirm('Clear all imported calendar events from the calendar view? (Quest cards are kept)'))return; rpg.calEvents=[];_parsedIcalEvents=[]; document.getElementById('icalStatus').textContent=''; document.getElementById('icalEventStrip').style.display='none'; save();renderCalendar();showToast('🗑 Calendar events cleared'); } function renderIcalUpcoming(){ const strip=document.getElementById('icalEventStrip'); const list=document.getElementById('icalUpcoming'); if(!strip||!list)return; const today=_dateStr(new Date()); const upcoming=(rpg.calEvents||[]).filter(e=>e.start&&e.start>=today).slice(0,5); if(!upcoming.length){strip.style.display='none';return;} strip.style.display='block'; list.innerHTML=upcoming.map(ev=>`
${ev.start} ${escHtml(ev.title)} ${ev.startTime?`⏰ ${ev.startTime}`:''}
`).join(''); } function quickConvertIcalEvent(id){ const ev=(rpg.calEvents||[]).find(x=>x.id===id);if(!ev)return; _parsedIcalEvents=[ev];openIcalModal([ev]); } /* ── GOOGLE CALENDAR ── */ function saveGCal(){ let url=(document.getElementById('gCalUrl').value||'').trim(); if(!url){showToast('Paste your Google Calendar embed URL first');return;} // Auto-fix common URL issues // If it's a plain calendar URL, convert to embed format if(url.includes('calendar.google.com/calendar/r')) url=url.replace('/calendar/r','/calendar/embed').replace(/\/[a-z]+$/,''); if(!url.includes('/embed'))url=url.replace('/calendar/','/calendar/embed?'); // Ensure ctz is set (avoids auth redirect in some cases) if(!url.includes('ctz=')){ try{const tz=Intl.DateTimeFormat().resolvedOptions().timeZone;if(tz)url+=(url.includes('?')?'&':'?')+'ctz='+encodeURIComponent(tz);}catch(e){} } // Ensure output=embed is present if(!url.includes('output='))url+=(url.includes('?')?'&':'?')+'output=embed'; localStorage.setItem('jvds_gcal_url',url); document.getElementById('gCalUrl').value=url; renderGCal(url);showToast('📅 Calendar synced!'); } function renderGCal(url){ const frame=document.getElementById('gCalFrame');if(!frame)return; if(!url){ frame.innerHTML='
🗓️ No calendar synced yet.

Google Calendar → Settings → [Your calendar] → "Integrate calendar" → copy the embed code src URL
'; return; } // Google Calendar embeds only work from their own domain due to their CSP — use a direct link instead frame.innerHTML=`
⚠️ Google blocks embedding their calendar on other sites via iframe. Open your calendar in a new tab instead:
📅 Open Google Calendar
Or use the built-in Calendar tab to track quest due dates.
`; } function loadGCal(){ const url=localStorage.getItem('jvds_gcal_url'); if(url){const inp=document.getElementById('gCalUrl');if(inp)inp.value=url;renderGCal(url);} } /* ── TABS / VIEWS ── */ function switchView(v){ document.querySelectorAll('.view').forEach(el=>el.classList.remove('active')); document.querySelectorAll('.tab-btn').forEach(el=>el.classList.remove('active')); document.querySelectorAll('.mob-nav-btn').forEach(el=>el.classList.remove('active')); document.getElementById('view-'+v)?.classList.add('active'); document.getElementById('tab-'+v)?.classList.add('active'); document.getElementById('mobtab-'+v)?.classList.add('active'); closeFab(); if(v==='calendar'){renderCalendar();loadGCal();} if(v==='profile'){rollBounties();updateRpgWidgets();} if(v==='raids')renderRaids(); if(v==='dash'){renderDash();renderProgressCharts();} if(v==='notes'){renderNotesList(document.getElementById('notesSideSearch')?.value||'');} if(v==='habits'){renderHabits();} if(v==='life'){renderLifeTab();} if(v==='shop'){renderShop();renderActiveBuffs();} if(v==='profile'){renderClassPath();initGistUI();_updateThemeSelect();} document.getElementById('mobtab-habits')?.classList[v==='habits'?'add':'remove']('active'); } /* ── EXPORT / IMPORT ── */ function exportData(){ const blob=new Blob([JSON.stringify({state:{columns:state.columns,cards:state.cards,nextId:state.nextId},rpg},null,2)],{type:'application/json'}); const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='jvds-tracker-'+new Date().toISOString().slice(0,10)+'.json';a.click();showToast('📤 Board exported!'); } function importData(){ const input=document.createElement('input');input.type='file';input.accept='.json'; input.onchange=e=>{const f=e.target.files[0];if(!f)return;const r=new FileReader();r.onload=ev=>{try{const d=JSON.parse(ev.target.result);if(d.state){const s=d.state;if(s.columns)state.columns=s.columns;if(s.cards)state.cards=s.cards;if(s.nextId)state.nextId=s.nextId;}if(d.rpg)Object.assign(rpg,d.rpg);save();render();updateRpgWidgets();showToast('📥 Board imported!');}catch(err){showToast('❌ Invalid file');}};r.readAsText(f);}; input.click(); } /* ── UTILS ── */ function escHtml(s){return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');} /* ── UPGRADED TOAST ── */ const _toastQ=[];let _toastBusy=false; function showToast(msg,type='info'){ _toastQ.push({msg,type}); if(!_toastBusy)_nextToast(); } function _nextToast(){ if(!_toastQ.length){_toastBusy=false;return;} _toastBusy=true; const{msg,type}=_toastQ.shift(); const t=document.getElementById('toast');if(!t)return; const cfg={ info: {bg:'var(--bg3)', bc:'var(--border2)', icon:'💬'}, success:{bg:'rgba(34,197,94,.12)', bc:'rgba(34,197,94,.4)', icon:'✅'}, error: {bg:'rgba(220,38,38,.12)', bc:'rgba(220,38,38,.4)', icon:'❌'}, xp: {bg:'rgba(245,200,66,.12)', bc:'rgba(245,200,66,.5)', icon:'⚡'}, gold: {bg:'rgba(245,200,66,.08)', bc:'rgba(245,200,66,.35)', icon:'💰'}, level: {bg:'rgba(168,85,247,.12)', bc:'rgba(168,85,247,.5)', icon:'🌟'}, warn: {bg:'rgba(245,158,11,.1)', bc:'rgba(245,158,11,.4)', icon:'⚠️'}, }[type]||{bg:'var(--bg3)',bc:'var(--border2)',icon:'💬'}; t.style.background=cfg.bg;t.style.borderColor=cfg.bc; t.innerHTML=`${cfg.icon}${msg}`; t.classList.add('show'); setTimeout(()=>{t.classList.remove('show');setTimeout(_nextToast,320);},2700); } /* ── ANIMATED COUNTER ── */ function animCount(el,target,dur=550){ if(!el)return; const start=parseFloat(el.textContent)||0; const diff=target-start; if(Math.abs(diff)<1){el.textContent=target;return;} const t0=performance.now(); const step=now=>{ const p=Math.min((now-t0)/dur,1); const e=1-Math.pow(1-p,3); el.textContent=Math.round(start+diff*e)+(target+'').includes('g')?'g':''; if(p<1)requestAnimationFrame(step); else el.textContent=target; }; requestAnimationFrame(step); } /* ── CARD COMPLETE ANIMATION ── */ function animCardComplete(cardId){ const el=document.querySelector(`[data-card-id="${cardId}"]`); if(!el)return; el.style.transition='all .35s cubic-bezier(.32,.72,0,1)'; el.style.transform='scale(1.06)'; el.style.boxShadow='0 0 30px rgba(34,197,94,.6)'; el.style.borderColor='var(--green)'; setTimeout(()=>{el.style.transform='scale(0) rotate(4deg)';el.style.opacity='0';},220); } /* ── MODAL CLOSE ON OVERLAY ── */ ['cardModal','colModal','achModal','lootModal','noteModal','icalModal','checkInModal'].forEach(id=>{ document.getElementById(id)?.addEventListener('click',e=>{if(e.target===e.currentTarget)document.getElementById(id).classList.remove('open');}); }); /* ── KEYBOARD ── */ document.addEventListener('keydown',e=>{ if(e.key==='Escape'){closeCardModal();closeColModal();closeNoteModal();closeSearch();document.getElementById('achModal').classList.remove('open');document.getElementById('lootModal').classList.remove('open');document.getElementById('shortcutsOverlay').classList.remove('open');document.getElementById('templatesModal').classList.remove('open');} if((e.metaKey||e.ctrlKey)&&e.key==='k'){e.preventDefault();openCardModal();} if((e.metaKey||e.ctrlKey)&&e.key==='n'){e.preventDefault();openNoteModal();} if((e.metaKey||e.ctrlKey)&&e.key==='/'){e.preventDefault();openSearch();} if((e.metaKey||e.ctrlKey)&&e.key==='t'){e.preventDefault();openTemplates();} if(e.key==='?'&&!e.ctrlKey&&!e.metaKey&&document.activeElement.tagName!=='INPUT'&&document.activeElement.tagName!=='TEXTAREA'){document.getElementById('shortcutsOverlay').classList.add('open');} }); /* ── INIT ── */ load(); if(!rpg.notes)rpg.notes=[]; if(!rpg.bossThreats)rpg.bossThreats={}; if(!rpg.streak)rpg.streak=0; if(!rpg.dailyStats)rpg.dailyStats={}; if(!rpg.habits)rpg.habits=[]; if(!rpg.calEvents)rpg.calEvents=[]; if(!rpg.activeBuffs)rpg.activeBuffs=[]; if(!rpg.shopUnlocked)rpg.shopUnlocked={}; if(!rpg.weeklyReviews)rpg.weeklyReviews=[]; // Ensure all cards have new fields state.cards.forEach((c,i)=>{ if(!c.timeLogs)c.timeLogs=[]; if(!c.repeat)c.repeat='none'; if(!c.flag)c.flag=null; if(!c.createdAt)c.createdAt=new Date(Date.now()-i*86400000*0.5).toISOString(); // estimate age for existing cards }); state.cards.filter(c=>c.colId==='done').forEach(c=>c._wasDone=true); applyTheme(); render(); rollBounties(); updateRpgWidgets(); updateStreakDisplay(); dailyLoginBonus(); checkBossThreats(); checkBackupNeeded(); scheduleDailyCheck(); checkWeeklyBoss(); renderDash(); renderProgressCharts(); renderClassPath(); renderActiveBuffs(); // Check medicine reminders every 15 mins while app is open setInterval(checkMedReminders, 900000); // Morning check-in (after a short delay so the app loads first) setTimeout(triggerCheckIn, 1500); if((rpg.calEvents||[]).length)renderIcalUpcoming(); if(!rpg.weeklyReviews)rpg.weeklyReviews=[]; generateDailyChallenge(); checkWeeklyReview(); // Add weekly review to modal close handlers document.getElementById('weeklyReviewModal')?.addEventListener('click',e=>{if(e.target===e.currentTarget)document.getElementById('weeklyReviewModal').classList.remove('open');}); // Hook export to record last export date const _origExportData=exportData; window.exportData=function(){_origExportData();rpg.lastExport=new Date().toISOString();save();document.getElementById('backupBanner')?.classList.remove('show');}; function applyTheme(){ const themes={cyberpunk:'theme-cyberpunk',ocean:'theme-ocean',blood:'theme-blood',fantasy:'theme-fantasy'}; document.body.className=themes[rpg.activeTheme]||'theme-fantasy'; } // ── URL shortcuts (PWA home screen shortcuts) ───────────────────────────────── (function handleUrlParams(){ const p=new URLSearchParams(location.search); if(p.get('action')==='new-quest'){setTimeout(()=>openCardModal(),400);} const hash=location.hash.replace('#',''); if(hash&&document.getElementById('view-'+hash))setTimeout(()=>switchView(hash),100); })(); // Set initial mobile nav active document.getElementById('mobtab-dash')?.classList.add('active');
⚔️ Install Quest Board as an app!Works offline · Looks native · No browser chrome
✏️ New Note
⚔️ New Quest
⚡ Quick Capture