Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions examples/advanced/grading-papers-comments-annotations/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}
13 changes: 13 additions & 0 deletions examples/advanced/grading-papers-comments-annotations/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Student Portal</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
20 changes: 20 additions & 0 deletions examples/advanced/grading-papers-comments-annotations/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "grading-papers-comments-annotations",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"pdfjs-dist": "4.3.136",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.0.4",
"vite": "^7.0.4"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
245 changes: 245 additions & 0 deletions examples/advanced/grading-papers-comments-annotations/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import '@harbour-enterprises/superdoc/style.css';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Header from './components/Header.jsx';
import AssignmentHeader from './components/AssignmentHeader.jsx';
import Drawer from './components/Drawer.jsx';
import { SuperDoc } from '@harbour-enterprises/superdoc';
import NickPDF from '/nick.pdf?url';
import * as pdfjsLib from 'pdfjs-dist/build/pdf.mjs';
import * as pdfjsViewer from 'pdfjs-dist/web/pdf_viewer.mjs';

const defaultWhiteboardOpacity = 1;
const disabledWhiteboardOpacity = 0.5;

const App = () => {
const superdocRef = useRef(null);
const docFileRef = useRef(null);

// UI state only (do not store SuperDoc instance in state).
const [whiteboardReady, setWhiteboardReady] = useState(false);
const [whiteboardEnabled, setWhiteboardEnabled] = useState(true);
const [activeTool, setActiveTool] = useState('select');

const toolButtons = useMemo(() => [
{ id: 'select', label: 'Select' },
{ id: 'text', label: 'Text' },
{ id: 'draw', label: 'Draw' },
{ id: 'erase', label: 'Erase' },
], []);

const registerStickers = useCallback(() => {
const superdoc = superdocRef.current;
if (!superdoc?.whiteboard) return;
superdoc.whiteboard.register('stickers', [
{ id: 'check-mark', label: 'Check Mark', src: '/stickers/check-mark.svg', width: 40, height: 40 },
{ id: 'nice', label: 'Nice!', src: '/stickers/nice.svg', width: 40, height: 40 },
{ id: 'needs-improvement', label: 'Needs improvement', src: '/stickers/needs-improvement.svg', width: 40, height: 40 },
]);
}, []);

const registerComments = useCallback(() => {
const superdoc = superdocRef.current;
if (!superdoc?.whiteboard) return;
superdoc.whiteboard.register('comments', [
{ id: 'great-job', text: 'Great job!' },
{ id: 'expand-this', text: 'Expand this' },
{ id: 'your-references', text: 'Where are your references?' },
]);
}, []);

const attachEventListeners = useCallback(() => {
const superdoc = superdocRef.current;
if (!superdoc) return;
superdoc.on('whiteboard:change', (data) => {
console.log('whiteboard:change', { data });
});
superdoc.on('whiteboard:tool', (tool) => {
setActiveTool(tool);
});
}, []);

const onWhiteboardReady = useCallback((whiteboard) => {
setWhiteboardReady(true);
setActiveTool(whiteboard?.getTool?.() ?? 'select');
registerStickers();
registerComments();
attachEventListeners();
}, [attachEventListeners, registerComments, registerStickers]);

// (Re)initialize SuperDoc with current file.
const initSuperDoc = useCallback(() => {
if (superdocRef.current?.destroy) {
superdocRef.current.destroy();
superdocRef.current = null;
}

const superdocInstance = new SuperDoc({
selector: '#superdoc',
document: { data: docFileRef.current },
toolbar: 'superdoc-toolbar',
licenseKey: 'community-and-eval-agplv3',
modules: {
comments: {},
toolbar: {
selector: '#superdoc-toolbar',
responsiveToContainer: true,
excludeItems: [
'acceptTrackedChangeBySelection',
'rejectTrackedChangeOnSelection',
'zoom',
'documentMode',
],
},
pdf: {
pdfLib: pdfjsLib,
pdfViewer: pdfjsViewer,
setWorker: true,
textLayerMode: 0,
},
whiteboard: {
enabled: whiteboardEnabled,
},
},
user: {
name: 'Sarah Smith',
email: 'sarah.smith@example.com',
},
onCommentsUpdate: (data) => {
console.log(`onCommentsUpdate:`, { data });
},
});

superdocRef.current = superdocInstance;
window.superdoc = superdocInstance;

superdocInstance.on('whiteboard:ready', ({ whiteboard }) => {
onWhiteboardReady(whiteboard);
});
}, [onWhiteboardReady]);

// Load selected file and boot SuperDoc.
const handleNewFile = useCallback(async (fileName) => {
let url;
let fileType;
let fileNameStr;

switch (fileName) {
case 'nick':
url = NickPDF;
fileType = 'application/pdf';
fileNameStr = 'nick.pdf';
break;
default:
return;
}

try {
const response = await fetch(url);
const blob = await response.blob();
const file = new File([blob], fileNameStr, { type: fileType });
docFileRef.current = file;
initSuperDoc();
} catch (err) {
console.error('Error fetching file:', err);
}
}, [initSuperDoc]);

const handleToolSelect = useCallback((tool) => {
setActiveTool(tool);
superdocRef.current?.whiteboard?.setTool(tool);
}, []);

const toggleWhiteboard = useCallback(() => {
setWhiteboardEnabled((prev) => {
const enabled = !prev;
const opacity = enabled ? defaultWhiteboardOpacity : disabledWhiteboardOpacity;
superdocRef.current?.whiteboard?.setEnabled(enabled);
superdocRef.current?.whiteboard?.setOpacity(opacity);
return enabled;
});
}, []);

const exportWhiteboard = useCallback(() => {
const data = superdocRef.current?.whiteboard?.getWhiteboardData();
if (!data) return;
console.log('[Whiteboard] export', { data });
console.log('[Whiteboard] export json:', JSON.stringify(data, null, 2));
}, []);

const importWhiteboard = useCallback(() => {
const json = window.prompt('Paste whiteboard JSON');
if (!json) return;
try {
const data = JSON.parse(json);
superdocRef.current?.whiteboard?.setWhiteboardData(data);
} catch (err) {
console.error('Invalid JSON', err);
}
}, []);

// Initial load + cleanup on unmount.
useEffect(() => {
handleNewFile('nick');
return () => {
if (superdocRef.current?.destroy) {
superdocRef.current.destroy();
superdocRef.current = null;
}
};
}, [handleNewFile]);

return (
<div className="app">
<Header />

<div className="app-container">
<div className="app-container-view">
<div className="container">
<AssignmentHeader />

<div className="main-content">
<div className="document-viewer">
<div className="viewer-header">
<h3>Document Viewer</h3>
{whiteboardReady && (
<div className="whiteboard-toolbar">
<div className="whiteboard-tools">
{toolButtons.map((tool) => (
<button
key={tool.id}
className={`whiteboard-tool-btn${activeTool === tool.id ? ' is-active' : ''}`}
onClick={() => handleToolSelect(tool.id)}
>
{tool.label}
</button>
))}
</div>

<div className="whiteboard-controls">
<button className="whiteboard-toggle" onClick={toggleWhiteboard}>
{whiteboardEnabled ? 'Whiteboard On' : 'Whiteboard Off'}
</button>
<button className="whiteboard-action" onClick={exportWhiteboard}>
Export
</button>
<button className="whiteboard-action" onClick={importWhiteboard}>
Import
</button>
</div>
</div>
)}
</div>
<div id="superdoc-toolbar"></div>
<div id="superdoc"></div>
</div>
</div>
</div>
</div>

<Drawer onSelectFile={handleNewFile} />
</div>
</div>
);
};

export default App;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const AssignmentHeader = () => {
return (
<div className="assignment-header">
<h2 className="assignment-title">Midterm Assignment</h2>
<div className="assignment-meta">
<div className="meta-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
<span>
Due: <span className="due-date" id="dueDate">August 16, 2026 at 11:59 PM</span>
</span>
</div>
<div className="meta-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14,2 14,8 20,8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10,9 9,9 8,9" />
</svg>
<span>Format: PDF</span>
</div>
<div className="meta-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
<span>Instructor: Dr. Sarah Smith</span>
</div>
</div>
<p>
Submit your midterm paper on "The Impact of Technology on Modern Education". The paper should be 2000-3000 words
and include proper citations.
</p>
<div className="submission-status status-pending" id="submissionStatus">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" />
<polyline points="12,6 12,12 16,14" />
</svg>
<span>Submission Pending</span>
</div>
</div>
);
};

export default AssignmentHeader;
Loading