12?12:14,fontWeight:600,fontFamily:"'Newsreader',Georgia,serif",textShadow:"0 1px 4px rgba(0,0,0,0.4)",display:"-webkit-box",WebkitLineClamp:2,WebkitBoxOrient:"vertical",overflow:"hidden",wordBreak:"break-word"}}>{notebook.name}
{notebook.name}
{noteCount} {noteCount===1?"note":"notes"}
);
}
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโ MAIN APP (offline-first) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
export default function NoteApp(){
const [dark,setDark]=useState(true);
const [user,setUser]=useState(null);const [loading,setLoading]=useState(true);
const [sidebarOpen,setSidebarOpen]=useState(window.innerWidth>768);
const [activeFolder,setActiveFolder]=useState("dashboard");
const [activeNote,setActiveNote]=useState(null);
const [searchQuery,setSearchQuery]=useState("");
const [showNewNb,setShowNewNb]=useState(false);
const [syncStatus,setSyncStatus]=useState("synced");
const [ctxMenu,setCtxMenu]=useState(null);
const [confirmDialog,setConfirmDialog]=useState(null);
const [editingNb,setEditingNb]=useState(null);
const [showAccount,setShowAccount]=useState(false);
const [dragNbId,setDragNbId]=useState(null);const [dragOverNbId,setDragOverNbId]=useState(null);
const [notebooks,setNotebooks]=useState([]);const [notes,setNotes]=useState([]);
const [noteContents,setNoteContents]=useState({});
const [isMobile,setIsMobile]=useState(window.innerWidth<768);
const [mobileView,setMobileView]=useState("list"); // list | editor
const [installPrompt,setInstallPrompt]=useState(null);
const T=useMemo(()=>dark?{
bg:"#0d1117",sidebar:"#010409",sidebarActive:"#161b22",card:"#161b22",toolbar:"#161b22",border:"#30363d",borderLight:"#21262d",text:"#e6edf3",textSec:"#c9d1d9",muted:"#8b949e",faint:"#484f58",accent:"#388bfd",accentSoft:"#388bfd22",accentText:"#58a6ff",danger:"#f85149",success:"#3fb950",warning:"#d29922",inputBg:"#0d1117",dashBg:"#0d1117",
}:{
bg:"#f6f8fa",sidebar:"#ffffff",sidebarActive:"#eef2ff",card:"#ffffff",toolbar:"#ffffff",border:"#d0d7de",borderLight:"#e5e7eb",text:"#1f2328",textSec:"#424a53",muted:"#656d76",faint:"#afb8c1",accent:"#0969da",accentSoft:"#0969da18",accentText:"#0550ae",danger:"#cf222e",success:"#1a7f37",warning:"#9a6700",inputBg:"#f6f8fa",dashBg:"#f0f2f5",
},[dark]);
// โโ Responsive โโ
useEffect(()=>{const h=()=>{const m=window.innerWidth<768;setIsMobile(m);if(!m)setSidebarOpen(true);};window.addEventListener("resize",h);return()=>window.removeEventListener("resize",h);},[]);
// โโ PWA install prompt โโ
useEffect(()=>{
const h=(e)=>{e.preventDefault();setInstallPrompt(e);};
window.addEventListener("beforeinstallprompt",h);
return()=>window.removeEventListener("beforeinstallprompt",h);
},[]);
const handleInstall=async()=>{if(!installPrompt)return;installPrompt.prompt();const r=await installPrompt.userChoice;if(r.outcome==="accepted")setInstallPrompt(null);};
// โโ Sync status listener โโ
useEffect(()=>sync.onSyncStatus(setSyncStatus),[]);
// โโ Data change listener โ reload from IndexedDB โโ
useEffect(()=>sync.onDataChange(async()=>{
setNotebooks(await sync.getNotebooks());
setNotes(await sync.getNotes());
// Keep loaded note content cached to avoid editor flicker/stalls on periodic sync
}),[]);
// โโ Startup: restore session โ load local data โ sync โโ
useEffect(()=>{(async()=>{
try{
const u=await sync.restoreSession();
if(u){setUser(u);await reloadLocal();
// Background sync if online
try{await sync.fullSync();await reloadLocal();}catch(e){if(e.message==="AUTH"){setUser(null);}}
}
}catch{}
setLoading(false);
})();},[]);
// Reload from IndexedDB
const reloadLocal=async()=>{
setNotebooks(await sync.getNotebooks());
setNotes(await sync.getNotes());
};
const onLogin=async(u)=>{setUser(u);
try{await sync.fullSync();}catch{}
await reloadLocal();
};
const onLogout=async()=>{await sync.logout();setUser(null);setNotes([]);setNotebooks([]);setNoteContents({});setActiveNote(null);setActiveFolder("dashboard");};
const activeNotes=useMemo(()=>notes.filter(n=>!n.trashed),[notes]);
const trashedNotes=useMemo(()=>notes.filter(n=>n.trashed),[notes]);
const filteredNotes=useMemo(()=>{
let list;
if(activeFolder==="trash")list=trashedNotes;
else if(activeFolder==="starred")list=activeNotes.filter(n=>n.starred);
else if(activeFolder==="all")list=activeNotes;
else if(activeFolder!=="dashboard")list=activeNotes.filter(n=>n.folderId===activeFolder);
else list=activeNotes;
if(searchQuery){const q=searchQuery.toLowerCase();list=list.filter(n=>n.title.toLowerCase().includes(q));}
return list.sort((a,b)=>new Date(b.updatedAt)-new Date(a.updatedAt));
},[notes,activeFolder,searchQuery,activeNotes,trashedNotes]);
const currentNoteMeta=activeNote?notes.find(n=>n.id===activeNote):null;
const currentNoteContent=activeNote?noteContents[activeNote]:null;
const activeNoteHasContent=!!currentNoteContent;
const currentNote=currentNoteMeta&¤tNoteContent?{...currentNoteMeta,...currentNoteContent}:null;
const currentNoteSafe=currentNote||(
currentNoteMeta
? {...currentNoteMeta,content:currentNoteContent?.content||"",drawingData:currentNoteContent?.drawingData||null}
: null
);
// Load note content on demand โ fetches server-first on cache miss (page load = all cache empty)
useEffect(()=>{
if(!activeNote || activeNoteHasContent) return;
let cancelled=false;
sync.loadNoteContent(activeNote)
.then(c=>{ if(!cancelled) setNoteContents(p=>({...p,[activeNote]:c})); })
.catch(()=>{ if(!cancelled) setNoteContents(p=>({...p,[activeNote]:{content:"",drawingData:null}})); });
return ()=>{cancelled=true;};
},[activeNote,activeNoteHasContent]);
// Hard fallback: never stay in loading state forever.
useEffect(()=>{
if(!activeNote || activeNoteHasContent) return;
const t=setTimeout(()=>{
setNoteContents(p=>p[activeNote]?p:{...p,[activeNote]:{content:"",drawingData:null}});
},5000);
return ()=>clearTimeout(t);
},[activeNote,activeNoteHasContent]);
// Keep currently open note content fresh across devices.
useEffect(()=>{
if(!user||!activeNote)return;
const i=setInterval(()=>{
if(!sync.isOnline())return;
sync.loadNoteContent(activeNote,{forceRemote:true}).then(c=>{
setNoteContents(p=>{
const prev=p[activeNote];
if(prev&&prev.content===c.content&&prev.drawingData===c.drawingData)return p;
return {...p,[activeNote]:c};
});
}).catch(()=>{});
},6000);
return()=>clearInterval(i);
},[user,activeNote]);
// โโ CRUD โ all go through sync engine โโ
const createNote=async()=>{
const fid=["all","starred","trash","dashboard"].includes(activeFolder)?(notebooks[0]?.id):activeFolder;if(!fid)return;
const note=await sync.createNote(fid);
setNotes(p=>[note,...p]);setNoteContents(p=>({...p,[note.id]:{content:"",drawingData:null}}));
setActiveNote(note.id);if(activeFolder==="dashboard")setActiveFolder(fid);
if(isMobile)setMobileView("editor");
};
const updateNote=async(id,u)=>{
if(u.title!==undefined){setNotes(p=>p.map(n=>n.id===id?{...n,title:u.title,updatedAt:now()}:n));sync.updateNoteTitle(id,u.title);}
if(u.content!==undefined){setNoteContents(p=>({...p,[id]:{...p[id],content:u.content}}));sync.updateNoteContent(id,u.content);}
if(u.drawingData!==undefined){setNoteContents(p=>({...p,[id]:{...p[id],drawingData:u.drawingData}}));sync.updateNoteDrawing(id,u.drawingData);}
};
const toggleStar=async(id)=>{const nv=await sync.toggleNoteStar(id);setNotes(p=>p.map(n=>n.id===id?{...n,starred:nv}:n));};
const trashNoteAction=(id)=>{const note=notes.find(n=>n.id===id);
setConfirmDialog({title:"Move to Trash",message:`Move "${note?.title}" to trash?`,confirmLabel:"Move to Trash",
onConfirm:async()=>{await sync.trashNote(id);setNotes(p=>p.map(n=>n.id===id?{...n,trashed:true}:n));
if(activeNote===id){setActiveNote(null);if(isMobile)setMobileView("list");}setConfirmDialog(null);}});
};
const restoreNote=async(id)=>{await sync.restoreNoteLocal(id);setNotes(p=>p.map(n=>n.id===id?{...n,trashed:false}:n));if(activeNote===id){setActiveNote(null);if(isMobile)setMobileView("list");}};
const permanentDeleteNote=(id)=>{const note=notes.find(n=>n.id===id);
setConfirmDialog({title:"Delete Permanently",message:`Permanently delete "${note?.title}"? This cannot be undone.`,
onConfirm:async()=>{await sync.permanentDeleteNoteLocal(id);setNotes(p=>p.filter(n=>n.id!==id));
if(activeNote===id){setActiveNote(null);if(isMobile)setMobileView("list");}setConfirmDialog(null);}});
};
const emptyTrash=()=>{
setConfirmDialog({title:"Empty Trash",message:`Permanently delete all ${trashedNotes.length} trashed notes?`,
onConfirm:async()=>{await sync.emptyTrashLocal();setNotes(p=>p.filter(n=>!n.trashed));setActiveNote(null);setConfirmDialog(null);}});
};
const moveNoteAction=async(id,newFolderId)=>{await sync.moveNote(id,newFolderId);setNotes(p=>p.map(n=>n.id===id?{...n,folderId:newFolderId}:n));};
const createNb=async({name,cover})=>{const nb=await sync.createNotebook({name,cover});setNotebooks(p=>[...p,nb]);setShowNewNb(false);setActiveFolder(nb.id);};
const updateNb=async(id,updates)=>{await sync.updateNotebook(id,updates);setNotebooks(p=>p.map(nb=>nb.id===id?{...nb,...updates}:nb));setEditingNb(null);};
const requestDeleteNb=(id)=>{const nb=notebooks.find(n=>n.id===id);const nc=activeNotes.filter(n=>n.folderId===id).length;
setConfirmDialog({title:"Delete Notebook",message:`Delete "${nb?.name}"${nc>0?` and trash its ${nc} notes`:""} permanently?`,
onConfirm:async()=>{await sync.deleteNotebookLocal(id);
setNotes(p=>p.map(n=>n.folderId===id?{...n,trashed:true}:n));setNotebooks(p=>p.filter(n=>n.id!==id));
if(activeFolder===id)setActiveFolder("dashboard");setConfirmDialog(null);}});
};
const handleNbDragStart=(e,id)=>{setDragNbId(id);e.dataTransfer.effectAllowed="move";};
const handleNbDragOver=(e,id)=>{e.preventDefault();if(dragNbId&&dragNbId!==id)setDragOverNbId(id);};
const handleNbDrop=async(e,targetId)=>{e.preventDefault();if(!dragNbId||dragNbId===targetId)return;
setNotebooks(prev=>{const a=[...prev];const fi=a.findIndex(n=>n.id===dragNbId);const ti=a.findIndex(n=>n.id===targetId);if(fi<0||ti<0)return prev;const[item]=a.splice(fi,1);a.splice(ti,0,item);
const ids=a.map(n=>n.id);sync.reorderNotebooks(ids);return a.map((n,i)=>({...n,sortOrder:i}));});
setDragNbId(null);setDragOverNbId(null);};
const handleNbDragEnd=()=>{setDragNbId(null);setDragOverNbId(null);};
const handleNbContextMenu=(e,nb)=>{e.preventDefault();e.stopPropagation();
setCtxMenu({x:e.clientX,y:e.clientY,items:[{label:"Edit",icon:I.edit,action:()=>setEditingNb(nb)},{divider:true},{label:"Delete Notebook",icon:I.trash,danger:true,action:()=>requestDeleteNb(nb.id)}]});};
const handleNoteContextMenu=(e,note)=>{e.preventDefault();e.stopPropagation();
if(note.trashed){setCtxMenu({x:e.clientX,y:e.clientY,items:[{label:"Restore",icon:I.restore,action:()=>restoreNote(note.id)},{divider:true},{label:"Delete Permanently",icon:I.trash,danger:true,action:()=>permanentDeleteNote(note.id)}]});}
else{const subs=notebooks.filter(nb=>nb.id!==note.folderId).map(nb=>({label:nb.name,colorDot:COVERS[nb.cover%COVERS.length].bg,action:()=>moveNoteAction(note.id,nb.id)}));
setCtxMenu({x:e.clientX,y:e.clientY,items:[{label:note.starred?"Unstar":"Star",icon:note.starred?I.starFill:I.star,action:()=>toggleStar(note.id)},...(subs.length>0?[{label:"Move to...",icon:I.move,submenu:subs}]:[]),{divider:true},{label:"Move to Trash",icon:I.trash,danger:true,action:()=>trashNoteAction(note.id)}]});}
};
useEffect(()=>{const l=document.createElement("link");l.href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400&family=DM+Sans:wght@400;500;600;700&display=swap";l.rel="stylesheet";document.head.appendChild(l);},[]);
// โโ Periodic sync (every 30s if online) โโ
useEffect(()=>{const i=setInterval(()=>{if(sync.isOnline())sync.fullSync().then(()=>reloadLocal()).catch(()=>{});},30000);return()=>clearInterval(i);},[]);
// โโ History API โ browser/phone back button โโ
const navStateRef=useRef({folder:"dashboard",note:null,view:"list"});
useEffect(()=>{
const cur=navStateRef.current;
if(cur.folder!==activeFolder||cur.note!==activeNote||cur.view!==mobileView){
const state={folder:activeFolder,note:activeNote,view:mobileView};
try{window.history.pushState(state,"");}catch{}
navStateRef.current=state;
}
},[activeFolder,activeNote,mobileView]);
useEffect(()=>{
const handler=(e)=>{
const s=e.state;
if(s){setActiveFolder(s.folder||"dashboard");setActiveNote(s.note||null);if(isMobile)setMobileView(s.view||"list");}
else{if(activeNote){setActiveNote(null);if(isMobile)setMobileView("list");}else if(activeFolder!=="dashboard"){setActiveFolder("dashboard");setActiveNote(null);}}
};
window.addEventListener("popstate",handler);return()=>window.removeEventListener("popstate",handler);
},[activeNote,activeFolder,isMobile]);
if(loading)return
Loading...
;
if(!user)return
;
const isTrashView=activeFolder==="trash";
/* โโ Back button for mobile โโ */
const backBtn=(label,onClick)=>;
/* โโ Sidebar โโ */
const renderSidebar=()=>(
{isMobile&&sidebarOpen&&
setSidebarOpen(false)} style={{position:"absolute",inset:0,background:"rgba(0,0,0,0.4)",zIndex:-1}}/>}
{DEFAULT_FOLDERS.map(f=>(
))}
Notebooks
{notebooks.map(nb=>(
handleNbDragStart(e,nb.id)} onDragOver={e=>handleNbDragOver(e,nb.id)} onDrop={e=>handleNbDrop(e,nb.id)} onDragEnd={handleNbDragEnd} onContextMenu={e=>handleNbContextMenu(e,nb)}
style={{display:"flex",alignItems:"center",borderRadius:10,transition:"all .15s",background:dragOverNbId===nb.id?T.accentSoft:activeFolder===nb.id?T.sidebarActive:"transparent",borderTop:dragOverNbId===nb.id?`2px solid ${T.accent}`:"2px solid transparent"}}>
{!isMobile&&
{I.grip}}
))}
{installPrompt&&
}
โ
{syncStatus==="synced"?"All synced":syncStatus==="syncing"?"Syncing...":syncStatus==="offline"?"Offline mode":syncStatus==="pending"?"Changes pending":"Sync error"}
{!sync.isOnline()&&offline}
);
/* โโ Dashboard โโ */
const renderDashboard=()=>(
My Notebooks
{notebooks.map(nb=>(
n.folderId===nb.id).length}
onClick={()=>{setActiveFolder(nb.id);setActiveNote(null);}} onContextMenu={e=>handleNbContextMenu(e,nb)}
isActive={activeFolder===nb.id} isDragOver={dragOverNbId===nb.id} draggable={!isMobile}
onDragStart={e=>handleNbDragStart(e,nb.id)} onDragEnd={handleNbDragEnd} onDragOver={e=>handleNbDragOver(e,nb.id)} onDrop={e=>handleNbDrop(e,nb.id)}/>
))}
Recent Notes
{activeNotes.slice(0,6).map(note=>{const nb=notebooks.find(b=>b.id===note.folderId);return(
);
/* โโ Note List โโ */
const renderNoteList=()=>{
const fname=(()=>{const sf=DEFAULT_FOLDERS.find(f=>f.id===activeFolder);if(sf)return sf.name;const nb=notebooks.find(n=>n.id===activeFolder);return nb?nb.name:"Notes";})();
// On mobile, hide list when editor is open
if(isMobile&&mobileView==="editor"&&activeNote)return null;
return(
{fname}
{!isTrashView&&}
{isTrashView&&trashedNotes.length>0&&}
{filteredNotes.length===0?(
{isTrashView?"๐๏ธ":"๐"}
{isTrashView?"Trash is empty":"No notes yet"}
{!isTrashView&&
}
):filteredNotes.map(note=>(
))}
);
};
/* โโ Editor โโ */
const renderEditor=()=>{
// On mobile, hide editor when list is shown
if(isMobile&&mobileView!=="editor")return null;
if(!currentNoteSafe)return isMobile?null:(
โ๏ธ
Select a note to edit
{!isTrashView&&
}
);
if(isTrashView&¤tNoteMeta)return(
{isMobile&&backBtn("Back",()=>{setActiveNote(null);setMobileView("list");})}
{currentNoteMeta.title}
Trashed notes are read-only. Restore to edit.
);
return(
{isMobile&&backBtn("Back",()=>{setActiveNote(null);setMobileView("list");})}
updateNote(currentNoteSafe.id,{title:e.target.value})}
style={{flex:1,border:"none",background:"transparent",outline:"none",fontSize:isMobile?15:17,fontWeight:600,color:T.text,fontFamily:"'Newsreader',Georgia,serif",minWidth:0}}/>
updateNote(currentNoteSafe.id,u)} dark={dark}/>
);
};
return(
{renderSidebar()}
{activeFolder==="dashboard"?renderDashboard():(<>{renderNoteList()}{renderEditor()}>)}
{showNewNb&&
setShowNewNb(false)} onCreate={createNb} dark={dark}/>}
{editingNb&&setEditingNb(null)} onSave={u=>updateNb(editingNb.id,u)} dark={dark}/>}
{showAccount&&setShowAccount(false)} onLogout={()=>{setShowAccount(false);onLogout();}} onUserUpdate={u=>setUser(u)} dark={dark}/>}
{confirmDialog&&setConfirmDialog(null)} dark={dark}/>}
{ctxMenu&&setCtxMenu(null)} dark={dark}/>}
);
}