diff --git a/backend/Dockerfile b/backend/Dockerfile index 57d2439..d84af42 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,14 +1,14 @@ FROM node:20-alpine +# better-sqlite3 needs build tools +RUN apk add --no-cache python3 make g++ + WORKDIR /app - COPY package.json package-lock.json* ./ -RUN npm ci --production - +RUN npm install --production COPY . . RUN mkdir -p /app/data /app/uploads EXPOSE 4000 - CMD ["node", "server.js"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 14193c8..f1d7253 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,57 +1,44 @@ version: "3.8" +# ───────────────────────────────────────────── +# InkNote — Deploy on Docker VM +# Pull images from Gitea container registry +# +# Usage: +# docker login gitea.honeyy.duckdns.org +# docker-compose pull +# docker-compose up -d +# ───────────────────────────────────────────── + services: - # ─── Backend API + WebSocket Server ─── inknote-server: - build: - context: ./backend - dockerfile: Dockerfile + image: gitea.honeyy.duckdns.org/marc/inknote-server:latest # ← Change 'marc' to your Gitea username container_name: inknote-server restart: unless-stopped - ports: - - "4000:4000" environment: - PORT=4000 - - JWT_SECRET=${JWT_SECRET:-please-change-this-secret-key} + - JWT_SECRET=${JWT_SECRET} - ENCRYPTION_KEY=${ENCRYPTION_KEY} - NODE_ENV=production volumes: - inknote-data:/app/data - inknote-uploads:/app/uploads healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:4000/api/health"] + test: ["CMD", "wget", "-qO-", "http://localhost:4000/api/health"] interval: 30s timeout: 5s retries: 3 - # ─── Frontend (Production Build) ─── inknote-web: - build: - context: ./frontend - dockerfile: Dockerfile + image: gitea.honeyy.duckdns.org/marc/inknote-web:latest # ← Change 'marc' to your Gitea username container_name: inknote-web restart: unless-stopped ports: - "3000:80" depends_on: - - inknote-server - - # ─── Optional: PostgreSQL for production ─── - # Uncomment to use PostgreSQL instead of SQLite - # inknote-db: - # image: postgres:16-alpine - # container_name: inknote-db - # restart: unless-stopped - # environment: - # POSTGRES_DB: inknote - # POSTGRES_USER: inknote - # POSTGRES_PASSWORD: ${DB_PASSWORD:-change-me} - # volumes: - # - postgres-data:/var/lib/postgresql/data - # ports: - # - "5432:5432" + inknote-server: + condition: service_healthy volumes: inknote-data: - inknote-uploads: - # postgres-data: \ No newline at end of file + inknote-uploads: \ No newline at end of file diff --git a/frontend/dockerfile b/frontend/dockerfile new file mode 100644 index 0000000..79cab84 --- /dev/null +++ b/frontend/dockerfile @@ -0,0 +1,14 @@ +# Stage 1: Build +FROM node:20-alpine AS build +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm install +COPY . . +RUN npm run build + +# Stage 2: Serve with nginx +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/note-app.jsx b/frontend/note-app.jsx new file mode 100644 index 0000000..dd91f84 --- /dev/null +++ b/frontend/note-app.jsx @@ -0,0 +1,659 @@ +import { useState, useRef, useEffect, useCallback, useMemo } from "react"; + +const uid = () => Math.random().toString(36).slice(2, 10); +const now = () => new Date().toISOString(); +const fmtDate = (d) => new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); + +const COLORS = ["#0d1117","#e94560","#0f3460","#533483","#e07c24","#21bf73","#fd5e53","#3282b8","#ff6b6b","#4ecdc4","#f7dc6f","#bb8fce","#85c1e9","#f8c471","#82e0aa","#f1948a","#aeb6bf","#ffffff","#f39c12","#000000"]; +const PEN_SIZES = [1, 2, 3, 5, 8, 12, 20]; + +const COVERS = [ + { bg: "linear-gradient(135deg,#0f0c29,#302b63,#24243e)", spine: "#1a1540", accent: "#6c63ff" }, + { bg: "linear-gradient(135deg,#1a1a2e,#16213e,#0f3460)", spine: "#0a1628", accent: "#4fc3f7" }, + { bg: "linear-gradient(135deg,#134e5e,#71b280)", spine: "#0e3d4a", accent: "#a5d6a7" }, + { bg: "linear-gradient(135deg,#8e2de2,#4a00e0)", spine: "#3a0099", accent: "#ce93d8" }, + { bg: "linear-gradient(135deg,#fc4a1a,#f7b733)", spine: "#c43d15", accent: "#ffe082" }, + { bg: "linear-gradient(135deg,#c94b4b,#4b134f)", spine: "#3d0f40", accent: "#f48fb1" }, + { bg: "linear-gradient(135deg,#11998e,#38ef7d)", spine: "#0d7a71", accent: "#b9f6ca" }, + { bg: "linear-gradient(135deg,#373b44,#4286f4)", spine: "#2a2e35", accent: "#90caf9" }, + { bg: "linear-gradient(135deg,#141e30,#243b55)", spine: "#0e1520", accent: "#78909c" }, + { bg: "linear-gradient(135deg,#f12711,#f5af19)", spine: "#c01f0d", accent: "#fff59d" }, + { bg: "linear-gradient(135deg,#2c3e50,#3498db)", spine: "#1e2b38", accent: "#81d4fa" }, + { bg: "linear-gradient(135deg,#0f2027,#203a43,#2c5364)", spine: "#0a171c", accent: "#80cbc4" }, + { bg: "linear-gradient(135deg,#aa4b6b,#6b6b83,#3b8d99)", spine: "#7a3550", accent: "#f48fb1" }, + { bg: "linear-gradient(135deg,#1f1c2c,#928dab)", spine: "#15131f", accent: "#b39ddb" }, + { bg: "linear-gradient(135deg,#654ea3,#eaafc8)", spine: "#4e3c7e", accent: "#f8bbd0" }, +]; + +const DEFAULT_FOLDERS = [ + { id: "all", name: "All Notes", icon: "📋" }, + { id: "starred", name: "Starred", icon: "⭐" }, + { id: "pdfs", name: "PDFs", icon: "📄" }, + { id: "trash", name: "Trash", icon: "🗑️" }, +]; + +const I = { + plus:, + search:, + bold:, + italic:, + underline:, + list:, + listOrd:123, + check:, + trash:, + star:, + starFill:, + pen:, + eraser:, + highlight:, + undo:, + redo:, + sun:, + moon:, + menu:, + text:, + draw:, + edit:, + palette:, + grip:, +}; + + +/* ═══════ CONFIRM DIALOG ═══════ */ +function ConfirmDialog({ title, message, confirmLabel="Delete", onConfirm, onCancel, dark }) { + const T = useMemo(()=>({ bg:dark?"#161b22":"#fff", border:dark?"#30363d":"#d0d7de", text:dark?"#e6edf3":"#1f2328", muted:dark?"#8b949e":"#656d76", danger:dark?"#f85149":"#cf222e" }),[dark]); + return ( +
+
e.stopPropagation()}> +
{title}
+
{message}
+
+ + +
+
+
+ ); +} + +/* ═══════ CONTEXT MENU ═══════ */ +function ContextMenu({ x, y, items, onClose, dark }) { + const T = useMemo(()=>({ bg:dark?"#1c2128":"#fff", border:dark?"#30363d":"#d0d7de", text:dark?"#e6edf3":"#1f2328", hover:dark?"#262c36":"#f3f4f6", danger:dark?"#f85149":"#cf222e" }),[dark]); + useEffect(()=>{ + const h=()=>onClose(); + window.addEventListener("click",h); window.addEventListener("contextmenu",h); + return ()=>{window.removeEventListener("click",h);window.removeEventListener("contextmenu",h);}; + },[onClose]); + return ( +
e.stopPropagation()}> + {items.map((item,i)=>item.divider?( +
+ ):( + + ))} +
+ ); +} + +/* ═══════ EDIT NOTEBOOK MODAL ═══════ */ +function EditNotebookModal({ notebook, onClose, onSave, dark }) { + const [name,setName]=useState(notebook.name); + const [sel,setSel]=useState(notebook.cover); + const T=useMemo(()=>({bg:dark?"#161b22":"#fff",border:dark?"#30363d":"#d0d7de",text:dark?"#e6edf3":"#1f2328",muted:dark?"#8b949e":"#656d76",active:dark?"#388bfd":"#0969da"}),[dark]); + return ( +
+
e.stopPropagation()}> +

Edit Notebook

+ setName(e.target.value)} autoFocus + style={{width:"100%",padding:"10px 14px",borderRadius:10,border:`1px solid ${T.border}`,background:dark?"#0d1117":"#f6f8fa",color:T.text,fontSize:14,outline:"none",marginBottom:16,boxSizing:"border-box",fontFamily:"'DM Sans',sans-serif"}} + onKeyDown={e=>{if(e.key==="Enter"&&name.trim())onSave({name:name.trim(),cover:sel});}}/> +
+
Cover style
+
+ {COVERS.map((c,i)=>)} +
+
+
+ + +
+
+
+ ); +} + + +/* ═══════════════════════════════════════════════════════ + UNIFIED NOTE EDITOR — single toolbar, canvas area is + always the same size so drawings never shift + ═══════════════════════════════════════════════════════ */ +function NoteEditor({ note, onUpdate, dark }) { + const [mode, setMode] = useState("text"); + const editorRef = useRef(null); + const canvasRef = useRef(null); + const ctxRef = useRef(null); + const [isDrawing, setIsDrawing] = useState(false); + + // Draw tool state (lifted out of canvas) + const [tool, setTool] = useState("pen"); + const [penColor, setPenColor] = useState(dark ? "#e6edf3" : "#1a1a2e"); + const [penSize, setPenSize] = useState(3); + const [history, setHistory] = useState([]); + const [hIdx, setHIdx] = useState(-1); + const [showColors, setShowColors] = useState(false); + const [showSizes, setShowSizes] = useState(false); + + const T = useMemo(() => ({ + bg: dark?"#0d1117":"#ffffff", toolbar: dark?"#161b22":"#f6f8fa", + border: dark?"#30363d":"#d0d7de", text: dark?"#e6edf3":"#1f2328", + muted: dark?"#8b949e":"#656d76", active: dark?"#388bfd":"#0969da", + faint: dark?"#484f58":"#afb8c1", + }), [dark]); + + // Init text editor + useEffect(() => { + if (editorRef.current && editorRef.current.innerHTML !== (note.content||"")) + editorRef.current.innerHTML = note.content || ""; + }, [note.id]); + + // Init canvas when switching to draw mode + useEffect(() => { + if (mode !== "draw") return; + const c = canvasRef.current; if (!c) return; + const p = c.parentElement; + c.width = p.clientWidth; c.height = p.clientHeight; + const ctx = c.getContext("2d"); + ctx.lineCap = "round"; ctx.lineJoin = "round"; + ctxRef.current = ctx; + if (note.drawingData) { + const img = new Image(); + img.onload = () => { ctx.drawImage(img, 0, 0); snap(); }; + img.src = note.drawingData; + } else snap(); + }, [mode]); + + // Canvas helpers + const snap = () => { + if (!canvasRef.current) return; + const d = canvasRef.current.toDataURL(); + setHistory(p => { const n = [...p.slice(0, (hIdx >= 0 ? hIdx + 1 : p.length))]; n.push(d); return n; }); + setHIdx(p => p + 1); + }; + const restoreIdx = (idx) => { + setHIdx(idx); + const img = new Image(); + img.onload = () => { ctxRef.current.clearRect(0,0,canvasRef.current.width,canvasRef.current.height); ctxRef.current.drawImage(img,0,0); }; + img.src = history[idx]; + }; + const undo = () => { if (hIdx > 0) restoreIdx(hIdx - 1); }; + const redo = () => { if (hIdx < history.length - 1) restoreIdx(hIdx + 1); }; + const clearCanvas = () => { ctxRef.current?.clearRect(0,0,canvasRef.current.width,canvasRef.current.height); snap(); onUpdate({drawingData: canvasRef.current.toDataURL()}); }; + + const getPos = (e) => { + const c = canvasRef.current, r = c.getBoundingClientRect(); + const sx = c.width/r.width, sy = c.height/r.height; + const t = e.touches ? e.touches[0] : e; + return { x: (t.clientX-r.left)*sx, y: (t.clientY-r.top)*sy }; + }; + const startDraw = (e) => { + e.preventDefault(); const p = getPos(e); setIsDrawing(true); + const ctx = ctxRef.current; ctx.beginPath(); ctx.moveTo(p.x,p.y); + if (tool==="eraser"){ctx.globalCompositeOperation="destination-out";ctx.lineWidth=penSize*5;} + else if(tool==="highlighter"){ctx.globalCompositeOperation="source-over";ctx.strokeStyle=penColor+"44";ctx.lineWidth=penSize*6;} + else{ctx.globalCompositeOperation="source-over";ctx.strokeStyle=penColor;ctx.lineWidth=penSize;} + }; + const doDraw = (e) => {if(!isDrawing)return;e.preventDefault();const p=getPos(e);ctxRef.current.lineTo(p.x,p.y);ctxRef.current.stroke();}; + const stopDraw = (e) => { + if(!isDrawing)return;e?.preventDefault();setIsDrawing(false); + ctxRef.current.closePath();ctxRef.current.globalCompositeOperation="source-over"; + snap(); onUpdate({drawingData:canvasRef.current.toDataURL()}); + }; + + const exec = (cmd,val=null)=>{document.execCommand(cmd,false,val);editorRef.current?.focus();onUpdate({content:editorRef.current.innerHTML});}; + + const tbtn = (active)=>({ + display:"flex",alignItems:"center",justifyContent:"center",width:30,height:30,borderRadius:6, + border:"none",cursor:"pointer",background:active?T.active+"22":"transparent",color:active?T.active:T.text,transition:"all .12s",flexShrink:0, + }); + + const modeBtn = (m,icon,label) => ( + + ); + + return ( +
+ {/* ── SINGLE UNIFIED TOOLBAR ── */} +
+ {modeBtn("text",I.text,"Write")} + {modeBtn("draw",I.draw,"Draw")} +
+ + {mode==="text" ? (<> + + + +
+ + + +
+ + ) : (<> + + + +
+ + +
+ {/* Color picker */} +
+ + {showColors&&
+ {COLORS.map(c=>
} +
+ {/* Size picker */} +
+ + {showSizes&&
+ {PEN_SIZES.map(s=>)} +
} +
+
+ + )} +
+ + {/* ── CONTENT AREA — always same size regardless of mode ── */} +
+ {/* Text layer — always rendered */} +
onUpdate({content:editorRef.current.innerHTML})} + style={{ + position:"absolute",inset:0,padding:"20px 24px",outline:"none",fontSize:15,lineHeight:1.7, + color:T.text,overflowY:"auto",fontFamily:"'Newsreader','Georgia',serif", + zIndex:1, pointerEvents:mode==="text"?"auto":"none", + }} + /> + + {/* Drawing image overlay — visible in WRITE mode when drawings exist */} + {mode==="text" && note.drawingData && ( + + )} + + {/* Live canvas — visible in DRAW mode */} + {mode==="draw" && ( + + )} +
+
+ ); +} + + +/* ═══════ NOTEBOOK COVER ═══════ */ +function NotebookCover({notebook,noteCount,onClick,onContextMenu,isActive,isDragOver,draggable,onDragStart,onDragEnd,onDragOver,onDrop}) { + const cover=COVERS[notebook.cover%COVERS.length]; + const [hovered,setHovered]=useState(false); + return ( + + ); +} + +/* ═══════ CREATE NOTEBOOK MODAL ═══════ */ +function CreateNotebookModal({onClose,onCreate,dark}) { + const [name,setName]=useState("");const [sel,setSel]=useState(0); + const T=useMemo(()=>({bg:dark?"#161b22":"#fff",border:dark?"#30363d":"#d0d7de",text:dark?"#e6edf3":"#1f2328",muted:dark?"#8b949e":"#656d76",active:dark?"#388bfd":"#0969da"}),[dark]); + return ( +
+
e.stopPropagation()}> +

New Notebook

+ setName(e.target.value)} placeholder="Notebook name..." autoFocus + style={{width:"100%",padding:"10px 14px",borderRadius:10,border:`1px solid ${T.border}`,background:dark?"#0d1117":"#f6f8fa",color:T.text,fontSize:14,outline:"none",marginBottom:16,boxSizing:"border-box",fontFamily:"'DM Sans',sans-serif"}} + onKeyDown={e=>{if(e.key==="Enter"&&name.trim())onCreate({name:name.trim(),cover:sel});}}/> +
+
Choose cover style
+
+ {COVERS.map((c,i)=>)} +
+
+
+ + +
+
+
+ ); +} + + +/* ═══════════════════════ MAIN APP ═══════════════════════ */ +export default function NoteApp() { + const [dark,setDark]=useState(true); + const [sidebarOpen,setSidebarOpen]=useState(true); + 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 [dragNbId,setDragNbId]=useState(null); + const [dragOverNbId,setDragOverNbId]=useState(null); + const fileRef=useRef(null); + + const [notebooks,setNotebooks]=useState([ + {id:"nb1",name:"Personal",cover:0},{id:"nb2",name:"Work Projects",cover:2}, + {id:"nb3",name:"Ideas & Sketches",cover:4},{id:"nb4",name:"Study Notes",cover:7}, + ]); + const [notes,setNotes]=useState([ + {id:"n1",title:"Welcome to InkNote",folderId:"nb1",content:"

Welcome to InkNote ✨

Each note supports both writing and drawing — switch modes with the tabs. Drawings stay visible while you write!

  • Rich text with formatting, lists, checkboxes
  • Freehand drawing layered on top
",drawingData:null,starred:true,createdAt:"2025-06-10T10:00:00Z",updatedAt:now()}, + {id:"n2",title:"Sprint Planning",folderId:"nb2",content:"

Q3 Roadmap

  1. Sync engine
  2. Mobile gestures
  3. PDF export

Review: Friday 3pm

",drawingData:null,starred:false,createdAt:"2025-06-12T14:00:00Z",updatedAt:now()}, + {id:"n3",title:"UI Wireframes",folderId:"nb3",content:"

Dashboard layout concepts...

",drawingData:null,starred:false,createdAt:"2025-06-11T08:30:00Z",updatedAt:now()}, + {id:"n4",title:"Data Structures",folderId:"nb4",content:"

Binary Trees

Each node has at most two children. Operations: insert, delete, search, traversal.

",drawingData:null,starred:true,createdAt:"2025-06-09T11:00:00Z",updatedAt:now()}, + ]); + const [pdfs,setPdfs]=useState([]); + + 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]); + + const filteredNotes=useMemo(()=>{ + let list=notes; + if(activeFolder==="starred")list=list.filter(n=>n.starred); + else if(activeFolder==="pdfs")return pdfs; + else if(activeFolder==="trash")return[]; + else if(activeFolder!=="all"&&activeFolder!=="dashboard")list=list.filter(n=>n.folderId===activeFolder); + if(searchQuery){const q=searchQuery.toLowerCase();list=list.filter(n=>n.title.toLowerCase().includes(q)||(n.content||"").toLowerCase().includes(q));} + return list.sort((a,b)=>new Date(b.updatedAt)-new Date(a.updatedAt)); + },[notes,pdfs,activeFolder,searchQuery]); + + const currentNote=activeNote?notes.find(n=>n.id===activeNote)||pdfs.find(p=>p.id===activeNote):null; + + const createNote=()=>{ + const fid=["all","starred","pdfs","trash","dashboard"].includes(activeFolder)?(notebooks[0]?.id||"nb1"):activeFolder; + const n={id:uid(),title:"Untitled Note",folderId:fid,content:"",drawingData:null,starred:false,createdAt:now(),updatedAt:now()}; + setNotes(p=>[n,...p]);setActiveNote(n.id);if(activeFolder==="dashboard")setActiveFolder(fid);sim(); + }; + const updateNote=(id,u)=>{setNotes(p=>p.map(n=>n.id===id?{...n,...u,updatedAt:now()}:n));sim();}; + const requestDeleteNote=(id)=>{ + const note=notes.find(n=>n.id===id); + setConfirmDialog({title:"Delete Note",message:`Are you sure you want to delete "${note?.title||"this note"}"? This cannot be undone.`, + onConfirm:()=>{setNotes(p=>p.filter(n=>n.id!==id));if(activeNote===id)setActiveNote(null);sim();setConfirmDialog(null);}}); + }; + const toggleStar=(id)=>setNotes(p=>p.map(n=>n.id===id?{...n,starred:!n.starred}:n)); + const importPDF=(e)=>{const f=e.target.files[0];if(!f)return;const p={id:uid(),type:"pdf",title:f.name,starred:false,createdAt:now(),updatedAt:now()};setPdfs(prev=>[p,...prev]);setActiveNote(p.id);setActiveFolder("pdfs");sim();}; + const createNb=({name,cover})=>{const nb={id:uid(),name,cover};setNotebooks(p=>[...p,nb]);setShowNewNb(false);setActiveFolder(nb.id);}; + const updateNb=(id,updates)=>{setNotebooks(p=>p.map(nb=>nb.id===id?{...nb,...updates}:nb));setEditingNb(null);sim();}; + const requestDeleteNb=(id)=>{ + const nb=notebooks.find(n=>n.id===id);const nc=notes.filter(n=>n.folderId===id).length; + setConfirmDialog({title:"Delete Notebook",message:`Delete "${nb?.name}"${nc>0?` and its ${nc} note${nc>1?"s":""}`:""} permanently?`, + onConfirm:()=>{setNotes(p=>p.filter(n=>n.folderId!==id));setNotebooks(p=>p.filter(n=>n.id!==id));if(activeFolder===id)setActiveFolder("dashboard");setConfirmDialog(null);sim();}}); + }; + const sim=()=>{setSyncStatus("syncing");setTimeout(()=>setSyncStatus("synced"),1200);}; + + // Drag & Drop + const handleNbDragStart=(e,id)=>{setDragNbId(id);e.dataTransfer.effectAllowed="move";}; + const handleNbDragOver=(e,id)=>{e.preventDefault();if(dragNbId&&dragNbId!==id)setDragOverNbId(id);}; + const handleNbDrop=(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);return a;}); + setDragNbId(null);setDragOverNbId(null);}; + const handleNbDragEnd=()=>{setDragNbId(null);setDragOverNbId(null);}; + const handleNbContextMenu=(e,nb)=>{e.preventDefault();e.stopPropagation(); + setCtxMenu({x:Math.min(e.clientX,window.innerWidth-200),y:Math.min(e.clientY,window.innerHeight-180), + items:[{label:"Rename",icon:I.edit,action:()=>setEditingNb(nb)},{label:"Change Cover",icon:I.palette,action:()=>setEditingNb(nb)},{divider:true},{label:"Delete Notebook",icon:I.trash,danger:true,action:()=>requestDeleteNb(nb.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);},[]); + + const renderSidebar=()=>( +
+
+
+
✏️
+ InkNote +
+ +
+
+
+ {I.search} + setSearchQuery(e.target.value)} placeholder="Search notes..." + style={{border:"none",background:"transparent",outline:"none",flex:1,color:T.text,fontSize:13,fontFamily:"'DM Sans',sans-serif"}}/> +
+
+
+ +
+
+ {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"}}> + {I.grip} + +
+ ))} +
+
+
+ ● + {syncStatus==="synced"?"All changes synced":syncStatus==="syncing"?"Syncing...":"Offline"} +
+
+
+ ); + + 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 + onDragStart={e=>handleNbDragStart(e,nb.id)} onDragEnd={handleNbDragEnd} onDragOver={e=>handleNbDragOver(e,nb.id)} onDrop={e=>handleNbDrop(e,nb.id)}/> + ))} + +
+
+

Recent Notes

+
+ {notes.slice(0,6).map(note=>{const nb=notebooks.find(b=>b.id===note.folderId);return( + + );})} +
+
+
+ ); + + 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";})(); + return( +
+
+
+ +

{fname}

+
+
+ + + +
+
+
+ {filteredNotes.length===0?( +
📝
No notes yet
+ ):filteredNotes.map(note=>( + + ))} +
+
+ );}; + + const renderEditor=()=>{ + if(!currentNote)return( +
+
✏️
+
Select a note to edit
+ +
+ ); + return( +
+
+ updateNote(currentNote.id,{title:e.target.value})} + style={{flex:1,border:"none",background:"transparent",outline:"none",fontSize:17,fontWeight:600,color:T.text,fontFamily:"'Newsreader',Georgia,serif"}}/> + + +
+
+ updateNote(currentNote.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}/>} + {confirmDialog&&setConfirmDialog(null)} dark={dark}/>} + {ctxMenu&&setCtxMenu(null)} dark={dark}/>} +
+ ); +} \ No newline at end of file diff --git a/note-app.jsx b/note-app.jsx deleted file mode 100644 index 0401a99..0000000 --- a/note-app.jsx +++ /dev/null @@ -1,1116 +0,0 @@ -import { useState, useRef, useEffect, useCallback, useMemo } from "react"; - -/* ─────────────────────── helpers ─────────────────────── */ -const uid = () => Math.random().toString(36).slice(2, 10); -const now = () => new Date().toISOString(); -const fmtDate = (d) => { - const dt = new Date(d); - const opts = { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }; - return dt.toLocaleDateString("en-US", opts); -}; - -/* ─────────────────────── default data ─────────────────────── */ -const DEFAULT_FOLDERS = [ - { id: "all", name: "All Notes", icon: "📋", system: true }, - { id: "starred", name: "Starred", icon: "⭐", system: true }, - { id: "drawings", name: "Drawings", icon: "🎨", system: true }, - { id: "pdfs", name: "PDFs", icon: "📄", system: true }, - { id: "trash", name: "Trash", icon: "🗑️", system: true }, -]; - -const COLORS = [ - "#1a1a2e", "#e94560", "#0f3460", "#16213e", "#533483", - "#e07c24", "#21bf73", "#fd5e53", "#3282b8", "#ff6b6b", - "#4ecdc4", "#f7dc6f", "#bb8fce", "#85c1e9", "#f8c471", - "#82e0aa", "#f1948a", "#aeb6bf", "#ffffff", "#000000", -]; - -const PEN_SIZES = [1, 2, 3, 5, 8, 12, 20]; - -const COVER_GRADIENTS = [ - "linear-gradient(135deg, #0f0c29, #302b63, #24243e)", - "linear-gradient(135deg, #e44d26, #f16529, #ffd700)", - "linear-gradient(135deg, #134e5e, #71b280)", - "linear-gradient(135deg, #8e2de2, #4a00e0)", - "linear-gradient(135deg, #fc4a1a, #f7b733)", - "linear-gradient(135deg, #00b4db, #0083b0)", - "linear-gradient(135deg, #2c3e50, #3498db)", - "linear-gradient(135deg, #c94b4b, #4b134f)", - "linear-gradient(135deg, #11998e, #38ef7d)", - "linear-gradient(135deg, #373b44, #4286f4)", - "linear-gradient(135deg, #141e30, #243b55)", - "linear-gradient(135deg, #f12711, #f5af19)", -]; - -/* ────────────────────── Icons (inline SVG) ────────────────────── */ -const Icons = { - plus: ( - - ), - search: ( - - ), - bold: , - italic: , - underline: , - list: , - listOrdered: 123, - checkbox: , - trash: , - star: , - starFill: , - pen: , - eraser: , - undo: , - redo: , - sun: , - moon: , - folder: , - back: , - menu: , - download: , - highlight: , - notebook: , - settings: , - edit: , - sync: , - image: , - close: , -}; - - -/* ────────────────────── Drawing Canvas Component ────────────────────── */ -function DrawingCanvas({ drawingData, onSave, dark, standalone = false, overlayMode = false }) { - const canvasRef = useRef(null); - const ctxRef = useRef(null); - const [isDrawing, setIsDrawing] = useState(false); - const [tool, setTool] = useState("pen"); - const [color, setColor] = useState("#1a1a2e"); - const [size, setSize] = useState(3); - const [history, setHistory] = useState([]); - const [historyIdx, setHistoryIdx] = useState(-1); - const [showColorPicker, setShowColorPicker] = useState(false); - const [showSizePicker, setShowSizePicker] = useState(false); - const lastPoint = useRef(null); - - const T = useMemo(() => ({ - bg: dark ? "#0d1117" : "#ffffff", - toolbar: dark ? "#161b22" : "#f6f8fa", - border: dark ? "#30363d" : "#d0d7de", - text: dark ? "#e6edf3" : "#1f2328", - muted: dark ? "#8b949e" : "#656d76", - active: dark ? "#388bfd" : "#0969da", - hover: dark ? "#1c2128" : "#eef1f4", - }), [dark]); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - const parent = canvas.parentElement; - if (standalone || overlayMode) { - canvas.width = parent.clientWidth; - canvas.height = parent.clientHeight; - } else { - canvas.width = parent.clientWidth; - canvas.height = Math.max(600, parent.clientHeight); - } - const ctx = canvas.getContext("2d"); - ctx.lineCap = "round"; - ctx.lineJoin = "round"; - ctxRef.current = ctx; - - if (drawingData && drawingData.length > 0) { - const img = new Image(); - img.onload = () => { - ctx.drawImage(img, 0, 0); - saveToHistory(); - }; - img.src = drawingData; - } else { - saveToHistory(); - } - }, []); - - const saveToHistory = () => { - const canvas = canvasRef.current; - if (!canvas) return; - const data = canvas.toDataURL(); - setHistory((prev) => { - const next = prev.slice(0, historyIdx + 1 || prev.length); - next.push(data); - return next; - }); - setHistoryIdx((prev) => prev + 1); - }; - - const undo = () => { - if (historyIdx <= 0) return; - const newIdx = historyIdx - 1; - setHistoryIdx(newIdx); - const img = new Image(); - img.onload = () => { - const ctx = ctxRef.current; - ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); - ctx.drawImage(img, 0, 0); - }; - img.src = history[newIdx]; - }; - - const redo = () => { - if (historyIdx >= history.length - 1) return; - const newIdx = historyIdx + 1; - setHistoryIdx(newIdx); - const img = new Image(); - img.onload = () => { - const ctx = ctxRef.current; - ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); - ctx.drawImage(img, 0, 0); - }; - img.src = history[newIdx]; - }; - - const getPos = (e) => { - const canvas = canvasRef.current; - const rect = canvas.getBoundingClientRect(); - const scaleX = canvas.width / rect.width; - const scaleY = canvas.height / rect.height; - if (e.touches) { - return { - x: (e.touches[0].clientX - rect.left) * scaleX, - y: (e.touches[0].clientY - rect.top) * scaleY, - }; - } - return { - x: (e.clientX - rect.left) * scaleX, - y: (e.clientY - rect.top) * scaleY, - }; - }; - - const startDraw = (e) => { - e.preventDefault(); - const pos = getPos(e); - lastPoint.current = pos; - setIsDrawing(true); - const ctx = ctxRef.current; - ctx.beginPath(); - ctx.moveTo(pos.x, pos.y); - if (tool === "eraser") { - ctx.globalCompositeOperation = "destination-out"; - ctx.lineWidth = size * 4; - } else if (tool === "highlighter") { - ctx.globalCompositeOperation = "source-over"; - ctx.strokeStyle = color + "55"; - ctx.lineWidth = size * 6; - } else { - ctx.globalCompositeOperation = "source-over"; - ctx.strokeStyle = color; - ctx.lineWidth = size; - } - }; - - const draw = (e) => { - if (!isDrawing) return; - e.preventDefault(); - const pos = getPos(e); - const ctx = ctxRef.current; - ctx.lineTo(pos.x, pos.y); - ctx.stroke(); - lastPoint.current = pos; - }; - - const stopDraw = (e) => { - if (!isDrawing) return; - e?.preventDefault(); - setIsDrawing(false); - ctxRef.current.closePath(); - ctxRef.current.globalCompositeOperation = "source-over"; - saveToHistory(); - if (onSave) { - onSave(canvasRef.current.toDataURL()); - } - }; - - const clearCanvas = () => { - const ctx = ctxRef.current; - ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); - saveToHistory(); - if (onSave) onSave(canvasRef.current.toDataURL()); - }; - - const toolbarStyle = { - display: "flex", - alignItems: "center", - gap: "4px", - padding: "6px 10px", - background: T.toolbar, - borderBottom: `1px solid ${T.border}`, - flexWrap: "wrap", - position: overlayMode ? "absolute" : "relative", - top: overlayMode ? 0 : undefined, - left: overlayMode ? 0 : undefined, - right: overlayMode ? 0 : undefined, - zIndex: overlayMode ? 20 : undefined, - borderRadius: overlayMode ? "0 0 8px 8px" : 0, - }; - - const btnStyle = (active) => ({ - display: "flex", - alignItems: "center", - justifyContent: "center", - width: 34, - height: 34, - borderRadius: 8, - border: "none", - cursor: "pointer", - background: active ? T.active + "22" : "transparent", - color: active ? T.active : T.text, - transition: "all 0.15s", - }); - - return ( -
-
- - - -
- - -
- - {/* Color */} -
- - {showColorPicker && ( -
- {COLORS.map((c) => ( -
- )} -
- - {/* Size */} -
- - {showSizePicker && ( -
- {PEN_SIZES.map((s) => ( - - ))} -
- )} -
- -
- -
- -
- -
-
- ); -} - - -/* ────────────────────── Rich Text Editor ────────────────────── */ -function RichTextEditor({ content, onChange, dark }) { - const editorRef = useRef(null); - - const T = useMemo(() => ({ - bg: dark ? "#0d1117" : "#ffffff", - toolbar: dark ? "#161b22" : "#f6f8fa", - border: dark ? "#30363d" : "#d0d7de", - text: dark ? "#e6edf3" : "#1f2328", - muted: dark ? "#8b949e" : "#656d76", - active: dark ? "#388bfd" : "#0969da", - }), [dark]); - - useEffect(() => { - if (editorRef.current && editorRef.current.innerHTML !== content) { - editorRef.current.innerHTML = content || ""; - } - }, [content]); - - const exec = (cmd, val = null) => { - document.execCommand(cmd, false, val); - editorRef.current?.focus(); - if (onChange) onChange(editorRef.current.innerHTML); - }; - - const handleInput = () => { - if (onChange) onChange(editorRef.current.innerHTML); - }; - - const btnStyle = { - display: "flex", alignItems: "center", justifyContent: "center", - width: 32, height: 32, borderRadius: 6, border: "none", - cursor: "pointer", background: "transparent", color: T.text, - transition: "all 0.15s", - }; - - return ( -
-
- - - -
- - - -
- -
-
-
- ); -} - - -/* ────────────────────── PDF Viewer with Annotation ────────────────────── */ -function PDFViewer({ pdfData, annotations, onAnnotationSave, dark }) { - const [drawMode, setDrawMode] = useState(false); - const [pageNum, setPageNum] = useState(1); - const [totalPages, setTotalPages] = useState(1); - - const T = useMemo(() => ({ - bg: dark ? "#0d1117" : "#f6f8fa", - toolbar: dark ? "#161b22" : "#ffffff", - border: dark ? "#30363d" : "#d0d7de", - text: dark ? "#e6edf3" : "#1f2328", - muted: dark ? "#8b949e" : "#656d76", - active: dark ? "#388bfd" : "#0969da", - }), [dark]); - - return ( -
-
- -
- - Page {pageNum} / {totalPages} - -
- -
- {pdfData ? ( -
-
-
📄
-
PDF loaded — {pdfData.name}
-
- In a full deployment, PDF.js renders pages here. The annotation canvas overlays each page for freehand drawing, highlighting, and text annotations. -
-
- {drawMode && ( -
- -
- )} -
- ) : ( -
-
📄
-
No PDF loaded
-
Import a PDF to view and annotate it
-
- )} -
-
- ); -} - - -/* ────────────────────── Create Notebook Modal ────────────────────── */ -function CreateNotebookModal({ onClose, onCreate, dark }) { - const [name, setName] = useState(""); - const [selectedCover, setSelectedCover] = useState(0); - - const T = useMemo(() => ({ - bg: dark ? "#161b22" : "#ffffff", - border: dark ? "#30363d" : "#d0d7de", - text: dark ? "#e6edf3" : "#1f2328", - muted: dark ? "#8b949e" : "#656d76", - active: dark ? "#388bfd" : "#0969da", - }), [dark]); - - return ( -
-
e.stopPropagation()}> -

- New Notebook -

- -
- - {name || "Notebook"} - -
- - setName(e.target.value)} - placeholder="Notebook name..." - style={{ - width: "100%", padding: "10px 14px", borderRadius: 10, border: `1px solid ${T.border}`, - background: dark ? "#0d1117" : "#f6f8fa", color: T.text, fontSize: 14, - outline: "none", marginBottom: 16, boxSizing: "border-box", - }} - /> - -
-
Choose cover
-
- {COVER_GRADIENTS.map((g, i) => ( -
-
- -
- - -
-
-
- ); -} - - -/* ────────────────────────── MAIN APP ────────────────────────── */ -export default function NoteApp() { - const [dark, setDark] = useState(true); - const [sidebarOpen, setSidebarOpen] = useState(true); - const [activeFolder, setActiveFolder] = useState("all"); - const [activeNote, setActiveNote] = useState(null); - const [view, setView] = useState("notes"); // notes | drawing | pdf | settings - const [searchQuery, setSearchQuery] = useState(""); - const [showNewNotebook, setShowNewNotebook] = useState(false); - const [syncStatus, setSyncStatus] = useState("synced"); // synced | syncing | offline - const fileInputRef = useRef(null); - - const [notebooks, setNotebooks] = useState([ - { id: "nb1", name: "Personal", cover: 0, createdAt: now() }, - { id: "nb2", name: "Work", cover: 2, createdAt: now() }, - { id: "nb3", name: "Ideas", cover: 4, createdAt: now() }, - ]); - - const [notes, setNotes] = useState([ - { - id: "n1", type: "text", title: "Welcome to InkNote", folderId: "nb1", - content: "

Welcome to InkNote ✨

Your cross-platform note-taking companion.

Features include:

  • Rich text editing with formatting
  • Freehand drawing with pen & highlighter
  • PDF import & annotation
  • Notebook organization with custom covers
  • Dark & light themes

Start creating! Click + to add a new note.

", - starred: true, createdAt: "2025-06-10T10:00:00Z", updatedAt: now(), - }, - { - id: "n2", type: "drawing", title: "Quick Sketch", folderId: "nb3", - drawingData: null, starred: false, createdAt: "2025-06-11T08:30:00Z", updatedAt: now(), - }, - { - id: "n3", type: "text", title: "Meeting Notes", folderId: "nb2", - content: "

Sprint Planning

Discussed roadmap for Q3. Key points:

  1. Sync engine improvements
  2. Mobile gesture support
  3. PDF annotation export
", - starred: false, createdAt: "2025-06-12T14:00:00Z", updatedAt: now(), - }, - ]); - - const [pdfs, setPdfs] = useState([]); - - // Theme - const T = useMemo(() => dark ? { - bg: "#0d1117", sidebar: "#010409", sidebarHover: "#161b22", sidebarActive: "#161b22", - card: "#161b22", cardHover: "#1c2128", toolbar: "#161b22", - border: "#30363d", borderLight: "#21262d", - text: "#e6edf3", textSecondary: "#c9d1d9", muted: "#8b949e", faint: "#484f58", - accent: "#388bfd", accentSoft: "#388bfd22", accentText: "#58a6ff", - danger: "#f85149", dangerSoft: "#f8514922", - success: "#3fb950", successSoft: "#3fb95022", - warning: "#d29922", - shadow: "0 8px 24px rgba(0,0,0,0.4)", - inputBg: "#0d1117", - } : { - bg: "#f6f8fa", sidebar: "#ffffff", sidebarHover: "#f3f4f6", sidebarActive: "#eef2ff", - card: "#ffffff", cardHover: "#f9fafb", toolbar: "#ffffff", - border: "#d0d7de", borderLight: "#e5e7eb", - text: "#1f2328", textSecondary: "#424a53", muted: "#656d76", faint: "#afb8c1", - accent: "#0969da", accentSoft: "#0969da18", accentText: "#0550ae", - danger: "#cf222e", dangerSoft: "#cf222e15", - success: "#1a7f37", successSoft: "#1a7f3715", - warning: "#9a6700", - shadow: "0 8px 24px rgba(0,0,0,0.08)", - inputBg: "#f6f8fa", - }, [dark]); - - // Filtered notes - const filteredNotes = useMemo(() => { - let list = notes; - if (activeFolder === "starred") list = list.filter((n) => n.starred); - else if (activeFolder === "drawings") list = list.filter((n) => n.type === "drawing"); - else if (activeFolder === "pdfs") return pdfs; - else if (activeFolder === "trash") return []; - else if (activeFolder !== "all") list = list.filter((n) => n.folderId === activeFolder); - if (searchQuery) { - const q = searchQuery.toLowerCase(); - list = list.filter((n) => n.title.toLowerCase().includes(q) || (n.content || "").toLowerCase().includes(q)); - } - return list.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); - }, [notes, pdfs, activeFolder, searchQuery]); - - const currentNote = activeNote ? notes.find((n) => n.id === activeNote) || pdfs.find((p) => p.id === activeNote) : null; - - // Actions - const createNote = (type = "text") => { - const fid = ["all", "starred", "drawings", "pdfs", "trash"].includes(activeFolder) ? (notebooks[0]?.id || "nb1") : activeFolder; - const note = { - id: uid(), type, title: type === "drawing" ? "New Drawing" : "Untitled Note", - folderId: fid, content: "", drawingData: null, starred: false, - createdAt: now(), updatedAt: now(), - }; - setNotes((prev) => [note, ...prev]); - setActiveNote(note.id); - setView(type === "drawing" ? "drawing" : "notes"); - simulateSync(); - }; - - const updateNote = (id, updates) => { - setNotes((prev) => prev.map((n) => n.id === id ? { ...n, ...updates, updatedAt: now() } : n)); - simulateSync(); - }; - - const deleteNote = (id) => { - setNotes((prev) => prev.filter((n) => n.id !== id)); - if (activeNote === id) setActiveNote(null); - simulateSync(); - }; - - const toggleStar = (id) => { - setNotes((prev) => prev.map((n) => n.id === id ? { ...n, starred: !n.starred } : n)); - }; - - const importPDF = (e) => { - const file = e.target.files[0]; - if (!file) return; - const pdf = { - id: uid(), type: "pdf", title: file.name, name: file.name, - pdfData: { name: file.name, size: file.size }, - annotations: null, starred: false, - createdAt: now(), updatedAt: now(), - }; - setPdfs((prev) => [pdf, ...prev]); - setActiveNote(pdf.id); - setView("pdf"); - setActiveFolder("pdfs"); - simulateSync(); - }; - - const createNotebook = ({ name, cover }) => { - const nb = { id: uid(), name, cover, createdAt: now() }; - setNotebooks((prev) => [...prev, nb]); - setShowNewNotebook(false); - setActiveFolder(nb.id); - }; - - const simulateSync = () => { - setSyncStatus("syncing"); - setTimeout(() => setSyncStatus("synced"), 1200); - }; - - // Font import - useEffect(() => { - const link = document.createElement("link"); - link.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"; - link.rel = "stylesheet"; - document.head.appendChild(link); - }, []); - - /* ─── SIDEBAR ─── */ - const renderSidebar = () => ( -
- {/* Logo area */} -
-
-
✏️
- InkNote -
-
- -
-
- - {/* Search */} -
-
- {Icons.search} - setSearchQuery(e.target.value)} - placeholder="Search notes..." - style={{ - border: "none", background: "transparent", outline: "none", flex: 1, - color: T.text, fontSize: 13, fontFamily: "'DM Sans', sans-serif", - }} - /> -
-
- - {/* System folders */} -
- {DEFAULT_FOLDERS.map((f) => ( - - ))} -
- - {/* Notebooks */} -
-
- Notebooks - -
- {notebooks.map((nb) => ( - - ))} -
- - {/* Bottom actions */} -
-
- ● - {syncStatus === "synced" ? "All changes synced" : syncStatus === "syncing" ? "Syncing..." : "Offline"} -
-
-
- ); - - /* ─── NOTE LIST ─── */ - const renderNoteList = () => { - const currentFolderName = (() => { - 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"; - })(); - - return ( -
- {/* Header */} -
-
- -

- {currentFolderName} -

-
-
- - - - -
-
- - {/* Note cards */} -
- {filteredNotes.length === 0 ? ( -
-
{activeFolder === "pdfs" ? "📄" : "📝"}
-
- {activeFolder === "pdfs" ? "No PDFs yet. Import one to get started." : "No notes yet. Create one to get started."} -
-
- ) : ( - filteredNotes.map((note) => ( - - )) - )} -
-
- ); - }; - - /* ─── EDITOR AREA ─── */ - const renderEditor = () => { - if (!currentNote) { - return ( -
-
✏️
-
Select a note to edit
-
Or create a new one
-
- - -
-
- ); - } - - return ( -
- {/* Note header */} -
- { - if (currentNote.type === "pdf") { - setPdfs((prev) => prev.map((p) => p.id === currentNote.id ? { ...p, title: e.target.value } : p)); - } else { - updateNote(currentNote.id, { title: e.target.value }); - } - }} - style={{ - flex: 1, border: "none", background: "transparent", outline: "none", - fontSize: 17, fontWeight: 600, color: T.text, - fontFamily: "'Newsreader', Georgia, serif", - }} - /> - - -
- - {/* Editor content */} -
- {currentNote.type === "drawing" ? ( - updateNote(currentNote.id, { drawingData: data })} - dark={dark} - standalone - /> - ) : currentNote.type === "pdf" ? ( - { - setPdfs((prev) => prev.map((p) => p.id === currentNote.id ? { ...p, annotations: data, updatedAt: now() } : p)); - }} - dark={dark} - /> - ) : ( - updateNote(currentNote.id, { content: html })} - dark={dark} - /> - )} -
-
- ); - }; - - /* ─── MAIN RENDER ─── */ - return ( -
- - - {renderSidebar()} - {renderNoteList()} - {renderEditor()} - - {showNewNotebook && ( - setShowNewNotebook(false)} - onCreate={createNotebook} - dark={dark} - /> - )} -
- ); -} \ No newline at end of file