My Study Hub
Private study workspace

Sign in to your Study Hub

Your study data is stored in your private Supabase account. Only your signed-in account can access it.

Create your account with your email, then sign in. If email confirmation is enabled, check your inbox.

function esc(x=''){return String(x).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));} function subj(id){return db.subjects.find(s=>s.id===id)?.name||'Unassigned'} function options(){return db.subjects.map(s=>``).join('')} function empty(t){return `
${t}
`} function head(t,s,a=''){return `

${t}

${s}
${a}
`} function go(p){page=p;render();window.scrollTo(0,0)} $('#nav').onclick=e=>{let b=e.target.closest('[data-page]');if(b)go(b.dataset.page)}; $('#themeToggle').onclick=()=>{theme=theme==='dark'?'light':'dark';document.documentElement.dataset.theme=theme;localStorage.setItem('msh-theme',theme)}; function render(){document.querySelectorAll('#nav button').forEach(b=>b.classList.toggle('active',b.dataset.page===page));let m=$('#main');m.innerHTML=({dashboard,subjects,library,runner,flashcards,quizzes,papers,notes,ai,settings}[page])();if(page==='runner'&&runnerId)runSelected();} function dashboard(){return head('Dashboard','Your learning space, organized your way')+`
${stat('Subjects',db.subjects.length)}${stat('Study files',db.files.length)}${stat('Flashcards',db.cards.length)}${stat('Quiz questions',db.questions.length)}${stat('Notes',db.notes.length)}${stat('Quiz attempts',db.attempts.length)}

Quick actions

Getting started

Create your own subjects, upload study files, then organize revision cards, quizzes and notes by topic.

Your subjects, notes, cards and quizzes sync to your private cloud account when you are signed in.

Your subjects

${db.subjects.length?db.subjects.map(s=>`
${esc(s.name)}
${esc(s.description||'')}
${s.chapters.length} chapters
`).join(''):empty('No sample subjects are included. Add your first subject in Subjects & chapters.')}
`} function stat(t,n){return `
${t}
${n}
`} function subjects(){return head('Subjects & chapters','Start with a blank workspace. Add, rename and remove your own subjects.')+`

Add a subject

Manage subjects

${db.subjects.length?db.subjects.map(s=>`
${esc(s.name)}
${esc(s.description||'')}
${s.chapters.map((c,i)=>`
${i+1}. ${esc(c)}
`).join('')}
`).join(''):empty('Your workspace is empty. Add any subjects you want above.')}
`} function addSubject(){let n=$('#sname').value.trim();if(!n)return alert('Enter a subject name.');db.subjects.push({id:uid(),name:n,description:$('#sdesc').value.trim(),chapters:[]});save();render()} function renameSubject(id){let s=db.subjects.find(x=>x.id===id),n=prompt('Subject name',s.name);if(n?.trim()){s.name=n.trim();save();render()}} function deleteSubject(id){if(confirm('Delete this subject? Linked content will remain but be unassigned.')){db.subjects=db.subjects.filter(x=>x.id!==id);save();render()}} function addChapter(id){let v=$('#ch-'+id).value.trim();if(v){db.subjects.find(x=>x.id===id).chapters.push(v);save();render()}} function removeChapter(id,i){db.subjects.find(x=>x.id===id).chapters.splice(i,1);save();render()} function library(){return head('Study library','Upload HTML, PDF, notes and documents. HTML files open as rendered pages, not source code.',``)+`
HTML/HTM files automatically open in the HTML Runner after upload and display as a rendered interactive page (not source code). PDF files can be opened in the browser. Other formats are catalogued; cloud file storage is a later setup step.

Materials (${db.files.length})

${db.files.length?db.files.map(f=>`
${esc(f.name)}
${esc(subj(f.subjectId))} · ${esc(f.kind)} · ${Math.ceil(f.size/1024)} KB
`).join(''):empty('No materials uploaded yet.')}
`} $('#uploadInput').onchange=async e=>{let fs=[...e.target.files];for(let f of fs){let ext=f.name.split('.').pop().toLowerCase(),kind=ext==='html'||ext==='htm'?'HTML':ext.toUpperCase();let content='';if(kind==='HTML'||f.type==='text/plain'){content=await f.text()}else if(kind==='PDF'){content='';} let subjectId=prompt(`Subject for ${f.name} (enter exact subject name, or leave blank):`)||'';let s=db.subjects.find(x=>x.name.toLowerCase()===subjectId.toLowerCase());db.files.push({id:uid(),name:f.name,kind,size:f.size,content,subjectId:s?.id||'',mime:f.type});} save(); const newestHtml=[...db.files].reverse().find(f=>f.kind==='HTML'&&typeof f.content==='string'&&f.content.trim()); e.target.value=''; if(newestHtml){runnerId=newestHtml.id;go('runner');} else{render();} }; function deleteFile(id){if(confirm('Delete this material?')){db.files=db.files.filter(x=>x.id!==id);save();render()}} function openFile(id){let f=db.files.find(x=>x.id===id);if(f.kind==='HTML'){runnerId=id;go('runner')}else if(f.kind==='PDF'){alert('PDF preview is available when the original PDF is stored as a cloud attachment. This starter currently catalogues the filename only.')}else if(f.content){let w=window.open();w.document.write('
'+esc(f.content)+'
')}else alert('This file is catalogued. Full document storage/preview is part of the cloud-storage upgrade.')} function runner(){let fs=db.files.filter(f=>f.kind==='HTML');let selected=fs.find(f=>f.id===runnerId)||fs[0];runnerId=selected?.id||null;return head('HTML Runner','Upload an HTML file and it will open here automatically. The source code stays hidden.',``)+`
${fs.length?`
Loading preview…

If this is a project with separate CSS, JavaScript or image files, upload a self-contained HTML file (or embed those assets in the HTML). This runner currently runs single HTML files, not multi-file folders.

`:empty('No HTML files yet. Upload an .html or .htm file and it will run here automatically.')}
`} async function uploadHtml(e){let f=e.target.files[0];if(!f)return;let content=await f.text();let obj={id:uid(),name:f.name,kind:'HTML',size:f.size,content,subjectId:'',mime:'text/html'};db.files.push(obj);runnerId=obj.id;save();e.target.value='';go('runner')} function runSelected(){let f=db.files.find(x=>x.id===runnerId);let frame=$('#runnerFrame');let status=$('#runnerStatus');if(!f||!frame)return;if(typeof f.content!=='string'||!f.content.trim()){if(status)status.textContent='This saved item has no HTML content (it may have been added in an older version). Tap Re-upload file and choose the original .html file again.';return;}if(status)status.textContent='Running '+f.name+'…';try{frame.onload=()=>{if(status)status.textContent='Preview loaded: '+f.name;};frame.onerror=()=>{if(status)status.textContent='The browser could not load this HTML preview. Try opening the file in Safari or check whether it depends on separate project files.';};frame.removeAttribute('src');frame.srcdoc=f.content;}catch(err){if(status)status.textContent='Could not start the preview: '+(err?.message||'unknown error');}} function openSeparate(){let f=db.files.find(x=>x.id===runnerId);if(!f)return;let w=window.open();if(w){w.document.open();w.document.write(f.content);w.document.close()}} function downloadHtml(){let f=db.files.find(x=>x.id===runnerId);if(!f)return;let a=document.createElement('a'),u=URL.createObjectURL(new Blob([f.content],{type:'text/html'}));a.href=u;a.download=f.name;a.click();URL.revokeObjectURL(u)} function flashcards(){let c=db.cards,card=c[cardIndex%c.length];return head('Flashcards','Create, flip, edit and revise your own cards',``)+`

Create a flashcard

Review cards

${c.length} cards
${card?`
${flipped?'ANSWER · TAP TO FLIP':'QUESTION · TAP TO REVEAL'}${esc(flipped?card.back:card.front)}
${cardIndex%c.length+1} / ${c.length}

${esc(subj(card.subjectId))} · ${esc(card.chapter||'')}

`:empty('No flashcards yet. Create your first card above.')}
`} function newCard(){$('#fcfront')?.focus()} function saveCard(){let front=$('#fcfront').value.trim(),back=$('#fcback').value.trim();if(!front||!back)return alert('Enter both sides of the card.');db.cards.push({id:uid(),front,back,subjectId:$('#fcsub').value,chapter:$('#fcchapter').value,status:'Need Revision'});save();render()} function rateCard(id,status){let c=db.cards.find(x=>x.id===id);c.status=status;save();cardIndex=(cardIndex+1)%db.cards.length;flipped=false;render()} function editCard(id){let c=db.cards.find(x=>x.id===id),f=prompt('Question',c.front);if(f===null)return;let b=prompt('Answer',c.back);if(b===null)return;c.front=f;c.back=b;save();render()} function deleteCard(id){if(confirm('Delete this card?')){db.cards=db.cards.filter(x=>x.id!==id);cardIndex=0;save();render()}} function quizzes(){return head('Quiz centre','Build MCQs and run a practice test')+`

Add a question

${[0,1,2,3].map(i=>`
`).join('')}

Practice quiz

${db.questions.length} questions
${quiz?quizUI():db.questions.length?``:empty('Add questions to create a mock test.')}

Past results

${db.attempts.map(a=>`
${new Date(a.date).toLocaleString()}${a.score}/${a.total} · ${Math.round(100*a.score/a.total)}%
`).join('')||empty('No attempts yet.')}
`} function addQuestion(){let text=$('#qtext').value.trim(),opts=[0,1,2,3].map(i=>$('#qo'+i).value.trim());if(!text||opts.some(x=>!x))return alert('Complete the question and all options.');db.questions.push({id:uid(),text,opts,correct:+$('#qcorrect').value,subjectId:$('#qsub').value,explain:$('#qexplain').value});save();render()} function startQuiz(){quiz={ids:db.questions.map(q=>q.id),i:0,answers:{}};render()} function quizUI(){let q=db.questions.find(x=>x.id===quiz.ids[quiz.i]);return `

Question ${quiz.i+1} of ${quiz.ids.length}

${esc(q.text)}

${q.opts.map((o,i)=>``).join('')}
`} function finishQuiz(){let score=quiz.ids.filter(id=>{let q=db.questions.find(x=>x.id===id);return quiz.answers[id]===q.correct}).length;db.attempts.unshift({date:new Date().toISOString(),score,total:quiz.ids.length});quiz=null;save();render();alert(`Result: ${score}/${db.attempts[0].total} (${Math.round(score/db.attempts[0].total*100)}%)`)} function papers(){return head('Previous-year papers','Catalogue papers and keep notes or extracted questions')+`

Add a paper record

Saved papers

${db.papers.map(p=>`
${esc(p.title)}
${esc(p.year)} · ${esc(subj(p.subjectId))}

${esc(p.content)}

`).join('')||empty('No papers added yet.')}
`} function addPaper(){let title=$('#ptitle').value.trim();if(!title)return alert('Enter paper title.');db.papers.unshift({id:uid(),title,year:$('#pyear').value,subjectId:$('#psub').value,content:$('#pcontent').value});save();render()} function deletePaper(id){db.papers=db.papers.filter(x=>x.id!==id);save();render()} function notes(){return head('My notes','Create and edit subject-wise notes')+`

Write a note

Saved notes

${db.notes.map(n=>`
${esc(n.title)}
${esc(subj(n.subjectId))}

${esc(n.body)}

`).join('')||empty('No notes yet.')}
`} function addNote(){let title=$('#ntitle').value.trim(),body=$('#nbody').value.trim();if(!title||!body)return alert('Enter a title and note.');db.notes.unshift({id:uid(),title,body,subjectId:$('#nsub').value});save();render()} function editNote(id){let n=db.notes.find(x=>x.id===id),t=prompt('Title',n.title);if(t===null)return;let b=prompt('Note content',n.body);if(b===null)return;n.title=t;n.body=b;save();render()} function deleteNote(id){if(confirm('Delete note?')){db.notes=db.notes.filter(x=>x.id!==id);save();render()}} function ai(){return head('AI assistant','Choose Gemini or ChatGPT. Live AI needs a secure backend connection.')+`
AI is not connected yet. This app does not pretend to generate live AI answers. Connect a secure backend for Gemini/OpenAI; never put secret API keys in this HTML file.
`} function askAI(){let provider=$('#provider').value,mode=$('#aimode').value,prompt=$('#aiprompt').value.trim();if(!prompt)return alert('Write a question.');$('#airesponse').textContent=`Request prepared for ${provider} · ${mode}. A secure backend endpoint is required to send this request and return an answer. Your question: ${prompt}`} function settings(){return head('Backup & cloud','Your account is protected by Supabase sign-in and row-level security')+`

Private cloud status

${cloudUser?'Signed in as '+esc(cloudUser.email||'your account')+'. Your study data syncs automatically when you make changes.':'Not signed in.'}

Cloud sync stores the app data record in your private account row. The private file bucket is set up separately; large file attachments are not yet uploaded to that bucket in this build.

Backup / restore

Keep a separate backup of your study data as an extra precaution.

Privacy

Only authenticated users can read or change their own cloud row under the row-level security policies. HTML files run in a sandboxed preview. Do not put passwords or AI secret keys in uploaded HTML.

`} function exportBackup(){let a=document.createElement('a'),u=URL.createObjectURL(new Blob([JSON.stringify(db,null,2)],{type:'application/json'}));a.href=u;a.download='my-study-hub-backup.json';a.click();URL.revokeObjectURL(u)} function importBackup(e){let f=e.target.files[0];if(!f)return;let r=new FileReader();r.onload=()=>{try{let x=JSON.parse(r.result);if(!x.subjects||!x.cards)throw 0;db={...blank(),...x};save();render();alert('Backup imported.')}catch{alert('Invalid backup file.')}};r.readAsText(f)} // Auth state listener above will show the login screen or load the private app.