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: ,
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:
- Sync engine improvements
- Mobile gesture support
- 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 */}
{/* Search */}
{/* 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}
/>
)}
);
}