Compare commits
2 Commits
aa0a3863b6
...
daef2bbf1a
| Author | SHA1 | Date | |
|---|---|---|---|
| daef2bbf1a | |||
| 21f165afb0 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -112,3 +112,6 @@ Thumbs.db
|
||||
|
||||
# Claude Code
|
||||
.claude
|
||||
|
||||
# Data
|
||||
data/*
|
||||
|
||||
@ -114,7 +114,8 @@ body.dark-theme .markdown-preview {
|
||||
.markdown-preview ol {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 2em;
|
||||
padding-left: 32px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.markdown-preview li {
|
||||
|
||||
@ -534,12 +534,25 @@ main {
|
||||
min-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
/* Preview mode - TOC is hidden, journal takes more space */
|
||||
main.preview-mode {
|
||||
grid-template-columns: 1fr 400px;
|
||||
}
|
||||
|
||||
main.preview-mode #table-of-contents {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
main {
|
||||
grid-template-columns: 250px 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
main.preview-mode {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
#ai-assistant {
|
||||
grid-column: 1 / -1;
|
||||
margin-top: 2rem;
|
||||
@ -552,6 +565,10 @@ main {
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
main.preview-mode {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
|
||||
219
assets/js/app.js
219
assets/js/app.js
@ -16,6 +16,7 @@ class ConceptionAssistant {
|
||||
this.setupEventListeners();
|
||||
await this.loadJournalList();
|
||||
this.generateTOC();
|
||||
this.updateStatistics();
|
||||
}
|
||||
|
||||
setupEditor() {
|
||||
@ -24,6 +25,7 @@ class ConceptionAssistant {
|
||||
// Generate TOC with debounce to avoid interrupting typing
|
||||
this.editor.addEventListener('input', () => {
|
||||
this.debounceTOC();
|
||||
this.updateStatistics();
|
||||
});
|
||||
|
||||
// Keyboard shortcut handling
|
||||
@ -69,12 +71,14 @@ class ConceptionAssistant {
|
||||
document.getElementById('save-journal')?.addEventListener('click', () => this.saveJournal());
|
||||
document.getElementById('load-journal')?.addEventListener('click', () => this.showJournalSelector());
|
||||
document.getElementById('preview-toggle')?.addEventListener('click', async () => await this.togglePreview());
|
||||
document.getElementById('new-blank-document')?.addEventListener('click', () => this.createBlankDocument());
|
||||
|
||||
// Table of contents
|
||||
document.getElementById('refresh-toc')?.addEventListener('click', () => this.generateTOC());
|
||||
|
||||
// Export/Import
|
||||
document.getElementById('export-md')?.addEventListener('click', () => this.exportMarkdown());
|
||||
document.getElementById('export-web')?.addEventListener('click', () => this.exportWeb());
|
||||
document.getElementById('import-md')?.addEventListener('change', (e) => this.importMarkdown(e));
|
||||
|
||||
// Theme
|
||||
@ -87,6 +91,12 @@ class ConceptionAssistant {
|
||||
document.getElementById('give-advice')?.addEventListener('click', () => this.handleAI('advice'));
|
||||
document.getElementById('liberty-mode')?.addEventListener('click', () => this.handleAI('liberty'));
|
||||
|
||||
// Present mode
|
||||
document.getElementById('present-mode')?.addEventListener('click', () => this.openPresentMode());
|
||||
|
||||
// Shortcuts help
|
||||
document.getElementById('show-shortcuts')?.addEventListener('click', () => this.showShortcutsModal());
|
||||
|
||||
// Loading modal
|
||||
document.getElementById('close-journal-modal')?.addEventListener('click', () => this.closeModal());
|
||||
|
||||
@ -365,6 +375,33 @@ class ConceptionAssistant {
|
||||
this.showNotification('New journal created', 'success');
|
||||
}
|
||||
|
||||
createBlankDocument() {
|
||||
// Clear the editor completely
|
||||
this.editor.innerText = '';
|
||||
|
||||
// Reset currentJournalId to null
|
||||
this.currentJournalId = null;
|
||||
|
||||
// Reset undo/redo stacks
|
||||
this.undoStack = [''];
|
||||
this.redoStack = [];
|
||||
|
||||
// Generate TOC
|
||||
this.generateTOC();
|
||||
|
||||
// Update statistics
|
||||
this.updateStatistics();
|
||||
|
||||
// Show notification
|
||||
this.showNotification('New blank document created', 'success');
|
||||
|
||||
// Close all panels
|
||||
closeAllPanels();
|
||||
|
||||
// Ensure editor is in edit mode
|
||||
this.ensureEditMode();
|
||||
}
|
||||
|
||||
debounceTOC() {
|
||||
if (this.tocTimer) {
|
||||
clearTimeout(this.tocTimer);
|
||||
@ -617,6 +654,43 @@ class ConceptionAssistant {
|
||||
this.showNotification('Markdown file exported', 'success');
|
||||
}
|
||||
|
||||
async exportWeb() {
|
||||
const content = this.editor.innerText;
|
||||
if (!content.trim()) {
|
||||
this.showNotification('No content to export', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.showNotification('Generating web export...', 'success');
|
||||
|
||||
const response = await fetch('/api/export/web', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content, title: 'Design Journal' })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Export failed');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'design-journal-web.zip';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
this.showNotification('Web export (ZIP) downloaded', 'success');
|
||||
} catch (error) {
|
||||
console.error('Export error:', error);
|
||||
this.showNotification('Error exporting web: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
importMarkdown(event) {
|
||||
@ -649,6 +723,140 @@ class ConceptionAssistant {
|
||||
this.showNotification(`${isDark ? 'Dark' : 'Light'} mode activated`, 'success');
|
||||
}
|
||||
|
||||
updateStatistics() {
|
||||
const content = this.editor.innerText.trim();
|
||||
|
||||
// Count words
|
||||
const words = content.split(/\s+/).filter(word => word.length > 0);
|
||||
const wordCount = words.length;
|
||||
|
||||
// Count characters
|
||||
const charCount = content.length;
|
||||
|
||||
// Calculate reading time (200 words per minute)
|
||||
const readingTime = Math.ceil(wordCount / 200);
|
||||
|
||||
// Update DOM elements
|
||||
const wordCountEl = document.getElementById('word-count');
|
||||
const charCountEl = document.getElementById('char-count');
|
||||
const readingTimeEl = document.getElementById('reading-time');
|
||||
|
||||
if (wordCountEl) wordCountEl.textContent = wordCount;
|
||||
if (charCountEl) charCountEl.textContent = charCount;
|
||||
if (readingTimeEl) readingTimeEl.textContent = `${readingTime} min`;
|
||||
}
|
||||
|
||||
async openPresentMode() {
|
||||
const content = this.editor.innerText;
|
||||
|
||||
if (!content.trim()) {
|
||||
this.showNotification('No content to present', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.showNotification('Opening presentation mode...', 'success');
|
||||
|
||||
// Create a form to POST the content
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/present';
|
||||
form.target = '_blank';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'content';
|
||||
input.value = content;
|
||||
|
||||
form.appendChild(input);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
document.body.removeChild(form);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Present mode error:', error);
|
||||
this.showNotification('Error opening presentation mode', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
showShortcutsModal() {
|
||||
const modalBody = document.getElementById('journal-modal-body');
|
||||
const modalHeader = document.querySelector('#journal-modal .modal-header h3');
|
||||
|
||||
if (modalHeader) {
|
||||
modalHeader.textContent = 'Keyboard Shortcuts';
|
||||
}
|
||||
|
||||
modalBody.innerHTML = `
|
||||
<div style="padding: 1rem;">
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<h4 style="color: var(--primary-color); margin-bottom: 0.75rem;">Document Management</h4>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||
<td style="padding: 0.5rem; font-weight: bold;">Ctrl+S</td>
|
||||
<td style="padding: 0.5rem;">Save journal</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||
<td style="padding: 0.5rem; font-weight: bold;">Ctrl+Z</td>
|
||||
<td style="padding: 0.5rem;">Undo</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||
<td style="padding: 0.5rem; font-weight: bold;">Ctrl+Y</td>
|
||||
<td style="padding: 0.5rem;">Redo</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||
<td style="padding: 0.5rem; font-weight: bold;">Tab</td>
|
||||
<td style="padding: 0.5rem;">Indent text</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0.5rem; font-weight: bold;">Esc</td>
|
||||
<td style="padding: 0.5rem;">Close side panels</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1.5rem;">
|
||||
<h4 style="color: var(--primary-color); margin-bottom: 0.75rem;">Markdown Formatting</h4>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||
<td style="padding: 0.5rem; font-family: monospace;"># Title</td>
|
||||
<td style="padding: 0.5rem;">Heading level 1</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||
<td style="padding: 0.5rem; font-family: monospace;">## Title</td>
|
||||
<td style="padding: 0.5rem;">Heading level 2</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||
<td style="padding: 0.5rem; font-family: monospace;">**bold**</td>
|
||||
<td style="padding: 0.5rem;">Bold text</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||
<td style="padding: 0.5rem; font-family: monospace;">*italic*</td>
|
||||
<td style="padding: 0.5rem;">Italic text</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||
<td style="padding: 0.5rem; font-family: monospace;">- item</td>
|
||||
<td style="padding: 0.5rem;">Bullet list</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0.5rem; font-family: monospace;">\`code\`</td>
|
||||
<td style="padding: 0.5rem;">Inline code</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 2rem;">
|
||||
<button class="btn primary" onclick="app.closeModal()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Show modal
|
||||
const modal = document.getElementById('journal-modal');
|
||||
modal.style.display = 'flex';
|
||||
setTimeout(() => modal.classList.add('show'), 10);
|
||||
}
|
||||
|
||||
async handleAI(action) {
|
||||
const selection = window.getSelection().toString().trim();
|
||||
const fullContent = this.editor.innerText;
|
||||
@ -1086,12 +1294,18 @@ class ConceptionAssistant {
|
||||
|
||||
async togglePreview() {
|
||||
const previewBtn = document.getElementById('preview-toggle');
|
||||
const mainElement = document.querySelector('main');
|
||||
|
||||
if (!this.isPreviewMode) {
|
||||
// Switch to preview mode
|
||||
this.originalContent = this.editor.innerHTML;
|
||||
const markdownContent = this.editor.innerText;
|
||||
|
||||
// Add preview-mode class to main to adjust grid layout
|
||||
if (mainElement) {
|
||||
mainElement.classList.add('preview-mode');
|
||||
}
|
||||
|
||||
// Configure Marked for GitHub-compatible rendering
|
||||
if (typeof marked !== 'undefined') {
|
||||
marked.setOptions({
|
||||
@ -1185,6 +1399,11 @@ class ConceptionAssistant {
|
||||
this.editor.style.border = '';
|
||||
this.editor.style.borderRadius = '';
|
||||
|
||||
// Remove preview-mode class to restore normal grid layout
|
||||
if (mainElement) {
|
||||
mainElement.classList.remove('preview-mode');
|
||||
}
|
||||
|
||||
// Change button
|
||||
previewBtn.innerHTML = 'Preview';
|
||||
previewBtn.classList.remove('secondary');
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"express": "^4.18.2",
|
||||
"puppeteer": "^24.22.3",
|
||||
|
||||
161
routes/export.js
161
routes/export.js
@ -2,6 +2,8 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const puppeteer = require('puppeteer');
|
||||
const path = require('path');
|
||||
const archiver = require('archiver');
|
||||
const fs = require('fs');
|
||||
|
||||
// POST /api/export/pdf - Generate PDF from markdown content
|
||||
router.post('/pdf', async (req, res) => {
|
||||
@ -509,4 +511,163 @@ function sanitizeFilename(filename) {
|
||||
.substring(0, 100); // Limit length
|
||||
}
|
||||
|
||||
// POST /api/export/web - Generate ZIP with HTML + CSS
|
||||
router.post('/web', async (req, res) => {
|
||||
const { content, title = 'Design Journal' } = req.body;
|
||||
|
||||
if (!content || content.trim() === '') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Content required to generate web export'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Read CSS files
|
||||
const styleCssPath = path.join(__dirname, '../assets/css/style.css');
|
||||
const githubCssPath = path.join(__dirname, '../assets/css/github-preview.css');
|
||||
|
||||
let styleCSS = '';
|
||||
let githubCSS = '';
|
||||
|
||||
try {
|
||||
styleCSS = fs.readFileSync(styleCssPath, 'utf8');
|
||||
githubCSS = fs.readFileSync(githubCssPath, 'utf8');
|
||||
} catch (err) {
|
||||
console.error('Error reading CSS files:', err);
|
||||
}
|
||||
|
||||
// Generate HTML content
|
||||
const htmlContent = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title}</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="presentation-container">
|
||||
<div class="presentation-header">
|
||||
<h1>${title}</h1>
|
||||
<div class="date">${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</div>
|
||||
</div>
|
||||
<div class="presentation-content markdown-body markdown-preview" id="content"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Render markdown content first
|
||||
const content = ${JSON.stringify(content)};
|
||||
const contentDiv = document.getElementById('content');
|
||||
contentDiv.innerHTML = marked.parse(content);
|
||||
|
||||
// Initialize Mermaid
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'default',
|
||||
securityLevel: 'loose'
|
||||
});
|
||||
|
||||
// Process Mermaid diagrams
|
||||
setTimeout(() => {
|
||||
const mermaidBlocks = contentDiv.querySelectorAll('code.language-mermaid, pre code.language-mermaid');
|
||||
|
||||
mermaidBlocks.forEach((block, index) => {
|
||||
try {
|
||||
const mermaidCode = block.textContent;
|
||||
const uniqueId = 'mermaid-' + Date.now() + '-' + index;
|
||||
|
||||
const mermaidDiv = document.createElement('div');
|
||||
mermaidDiv.id = uniqueId;
|
||||
mermaidDiv.className = 'mermaid';
|
||||
mermaidDiv.textContent = mermaidCode;
|
||||
|
||||
const pre = block.closest('pre') || block;
|
||||
pre.parentNode.replaceChild(mermaidDiv, pre);
|
||||
|
||||
mermaid.render(uniqueId + '-svg', mermaidCode).then(function(result) {
|
||||
mermaidDiv.innerHTML = result.svg;
|
||||
}).catch(function(err) {
|
||||
console.warn('Mermaid rendering error:', err);
|
||||
mermaidDiv.innerHTML = '<pre style="color: red;">Mermaid error: ' + err.message + '</pre>';
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Mermaid processing error:', error);
|
||||
}
|
||||
});
|
||||
}, 200);
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
// Combine CSS with additional presentation styles
|
||||
const combinedCSS = `
|
||||
${styleCSS}
|
||||
|
||||
${githubCSS}
|
||||
|
||||
/* Additional presentation styles */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#presentation-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 2rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.presentation-header {
|
||||
text-align: center;
|
||||
padding: 2rem 0;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.presentation-header h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.presentation-header .date {
|
||||
color: var(--text-light);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.presentation-content {
|
||||
line-height: 1.8;
|
||||
}
|
||||
`;
|
||||
|
||||
// Create ZIP archive
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 9 } // Maximum compression
|
||||
});
|
||||
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${sanitizeFilename(title)}-web.zip"`);
|
||||
|
||||
// Pipe archive to response
|
||||
archive.pipe(res);
|
||||
|
||||
// Add files to archive
|
||||
archive.append(htmlContent, { name: 'index.html' });
|
||||
archive.append(combinedCSS, { name: 'style.css' });
|
||||
|
||||
// Finalize archive
|
||||
await archive.finalize();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Web export error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Error during web export'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
135
routes/index.js
135
routes/index.js
@ -7,6 +7,141 @@ router.get('/', (req, res) => {
|
||||
res.send(getPage());
|
||||
});
|
||||
|
||||
// Present route - Display document in presentation mode
|
||||
router.post('/present', (req, res) => {
|
||||
const { content } = req.body;
|
||||
|
||||
res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Presentation - Design Journal</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
<link rel="stylesheet" href="/assets/css/github-preview.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
#presentation-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 2rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.presentation-header {
|
||||
text-align: center;
|
||||
padding: 2rem 0;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
.presentation-header h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.presentation-header .date {
|
||||
color: var(--text-light);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.presentation-content {
|
||||
line-height: 1.8;
|
||||
}
|
||||
.print-button {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.print-button:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
}
|
||||
@media print {
|
||||
.print-button {
|
||||
display: none;
|
||||
}
|
||||
#presentation-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button class="print-button" onclick="window.print()">Print / Save PDF</button>
|
||||
|
||||
<div id="presentation-container">
|
||||
<div class="presentation-header">
|
||||
<h1>Design Journal</h1>
|
||||
<div class="date">${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</div>
|
||||
</div>
|
||||
<div class="presentation-content markdown-body" id="content"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Render markdown content first
|
||||
const content = ${JSON.stringify(content)};
|
||||
const contentDiv = document.getElementById('content');
|
||||
contentDiv.innerHTML = marked.parse(content);
|
||||
|
||||
// Initialize Mermaid
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'default',
|
||||
securityLevel: 'loose'
|
||||
});
|
||||
|
||||
// Process Mermaid diagrams
|
||||
setTimeout(() => {
|
||||
// Find all code blocks with 'mermaid' language
|
||||
const mermaidBlocks = contentDiv.querySelectorAll('code.language-mermaid, pre code.language-mermaid');
|
||||
|
||||
mermaidBlocks.forEach((block, index) => {
|
||||
try {
|
||||
const mermaidCode = block.textContent;
|
||||
const uniqueId = 'mermaid-' + Date.now() + '-' + index;
|
||||
|
||||
// Create div with unique ID for Mermaid
|
||||
const mermaidDiv = document.createElement('div');
|
||||
mermaidDiv.id = uniqueId;
|
||||
mermaidDiv.className = 'mermaid';
|
||||
mermaidDiv.textContent = mermaidCode;
|
||||
|
||||
// Replace code block with Mermaid div
|
||||
const pre = block.closest('pre') || block;
|
||||
pre.parentNode.replaceChild(mermaidDiv, pre);
|
||||
|
||||
// Render diagram
|
||||
mermaid.render(uniqueId + '-svg', mermaidCode).then(function(result) {
|
||||
mermaidDiv.innerHTML = result.svg;
|
||||
}).catch(function(err) {
|
||||
console.warn('Mermaid rendering error:', err);
|
||||
mermaidDiv.innerHTML = '<pre style="color: red; padding: 1rem; background: #ffe6e6; border-radius: 6px;">Mermaid rendering error: ' + err.message + '</pre>';
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Mermaid processing error:', error);
|
||||
}
|
||||
});
|
||||
}, 200);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
// About route
|
||||
router.get('/about', (req, res) => {
|
||||
const { getHeader } = require('../views/header');
|
||||
|
||||
@ -19,10 +19,13 @@ function getLeftPanel() {
|
||||
|
||||
<div class="template-form">
|
||||
<div class="form-group">
|
||||
<p style="margin-bottom: 1rem; color: var(--text-light);">Start with a default template for your design journal.</p>
|
||||
<p style="margin-bottom: 1rem; color: var(--text-light); font-size: 0.9rem;">Create a new document or start with a template.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<div class="form-actions" style="display: flex; flex-direction: column; gap: 0.75rem;">
|
||||
<button id="new-blank-document" class="btn" title="Start with a blank document">
|
||||
New Blank Document
|
||||
</button>
|
||||
<button id="load-template" class="btn success" title="Load default template">
|
||||
Load Default Template
|
||||
</button>
|
||||
@ -57,7 +60,11 @@ function getRightPanel() {
|
||||
<div class="nav-buttons">
|
||||
<button class="nav-btn" id="export-md" title="Export as Markdown">
|
||||
<span class="icon">↓</span>
|
||||
<span>Export as Markdown</span>
|
||||
<span>Markdown</span>
|
||||
</button>
|
||||
<button class="nav-btn" id="export-web" title="Export as Web (HTML + CSS in ZIP)">
|
||||
<span class="icon">↓</span>
|
||||
<span>Web (ZIP)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -72,6 +79,25 @@ function getRightPanel() {
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h4>Statistics</h4>
|
||||
<div class="stats-display" style="padding: 0.75rem; background: var(--surface-color); border-radius: 8px; font-size: 0.85rem;">
|
||||
<div style="margin-bottom: 0.5rem;"><strong>Words:</strong> <span id="word-count">0</span></div>
|
||||
<div style="margin-bottom: 0.5rem;"><strong>Characters:</strong> <span id="char-count">0</span></div>
|
||||
<div><strong>Reading time:</strong> <span id="reading-time">0 min</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h4>Help</h4>
|
||||
<div class="nav-buttons">
|
||||
<button class="nav-btn" id="show-shortcuts" title="Keyboard shortcuts">
|
||||
<span class="icon">?</span>
|
||||
<span>Shortcuts</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -17,6 +17,7 @@ function getMain() {
|
||||
<button id="save-journal" class="btn success">Save</button>
|
||||
<button id="load-journal" class="btn">Load</button>
|
||||
<button id="preview-toggle" class="btn primary">Preview</button>
|
||||
<button id="present-mode" class="btn" title="Open in presentation mode (new window)">Present</button>
|
||||
<span id="save-status" class="text-light"></span>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user