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 Code
|
||||||
.claude
|
.claude
|
||||||
|
|
||||||
|
# Data
|
||||||
|
data/*
|
||||||
|
|||||||
@ -114,7 +114,8 @@ body.dark-theme .markdown-preview {
|
|||||||
.markdown-preview ol {
|
.markdown-preview ol {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
padding-left: 2em;
|
padding-left: 32px;
|
||||||
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview li {
|
.markdown-preview li {
|
||||||
|
|||||||
@ -534,12 +534,25 @@ main {
|
|||||||
min-height: calc(100vh - 140px);
|
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) {
|
@media (max-width: 1200px) {
|
||||||
main {
|
main {
|
||||||
grid-template-columns: 250px 1fr;
|
grid-template-columns: 250px 1fr;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main.preview-mode {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
#ai-assistant {
|
#ai-assistant {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
@ -552,6 +565,10 @@ main {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main.preview-mode {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sections */
|
/* Sections */
|
||||||
|
|||||||
219
assets/js/app.js
219
assets/js/app.js
@ -16,6 +16,7 @@ class ConceptionAssistant {
|
|||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
await this.loadJournalList();
|
await this.loadJournalList();
|
||||||
this.generateTOC();
|
this.generateTOC();
|
||||||
|
this.updateStatistics();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEditor() {
|
setupEditor() {
|
||||||
@ -24,6 +25,7 @@ class ConceptionAssistant {
|
|||||||
// Generate TOC with debounce to avoid interrupting typing
|
// Generate TOC with debounce to avoid interrupting typing
|
||||||
this.editor.addEventListener('input', () => {
|
this.editor.addEventListener('input', () => {
|
||||||
this.debounceTOC();
|
this.debounceTOC();
|
||||||
|
this.updateStatistics();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keyboard shortcut handling
|
// Keyboard shortcut handling
|
||||||
@ -69,12 +71,14 @@ class ConceptionAssistant {
|
|||||||
document.getElementById('save-journal')?.addEventListener('click', () => this.saveJournal());
|
document.getElementById('save-journal')?.addEventListener('click', () => this.saveJournal());
|
||||||
document.getElementById('load-journal')?.addEventListener('click', () => this.showJournalSelector());
|
document.getElementById('load-journal')?.addEventListener('click', () => this.showJournalSelector());
|
||||||
document.getElementById('preview-toggle')?.addEventListener('click', async () => await this.togglePreview());
|
document.getElementById('preview-toggle')?.addEventListener('click', async () => await this.togglePreview());
|
||||||
|
document.getElementById('new-blank-document')?.addEventListener('click', () => this.createBlankDocument());
|
||||||
|
|
||||||
// Table of contents
|
// Table of contents
|
||||||
document.getElementById('refresh-toc')?.addEventListener('click', () => this.generateTOC());
|
document.getElementById('refresh-toc')?.addEventListener('click', () => this.generateTOC());
|
||||||
|
|
||||||
// Export/Import
|
// Export/Import
|
||||||
document.getElementById('export-md')?.addEventListener('click', () => this.exportMarkdown());
|
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));
|
document.getElementById('import-md')?.addEventListener('change', (e) => this.importMarkdown(e));
|
||||||
|
|
||||||
// Theme
|
// Theme
|
||||||
@ -87,6 +91,12 @@ class ConceptionAssistant {
|
|||||||
document.getElementById('give-advice')?.addEventListener('click', () => this.handleAI('advice'));
|
document.getElementById('give-advice')?.addEventListener('click', () => this.handleAI('advice'));
|
||||||
document.getElementById('liberty-mode')?.addEventListener('click', () => this.handleAI('liberty'));
|
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
|
// Loading modal
|
||||||
document.getElementById('close-journal-modal')?.addEventListener('click', () => this.closeModal());
|
document.getElementById('close-journal-modal')?.addEventListener('click', () => this.closeModal());
|
||||||
|
|
||||||
@ -365,6 +375,33 @@ class ConceptionAssistant {
|
|||||||
this.showNotification('New journal created', 'success');
|
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() {
|
debounceTOC() {
|
||||||
if (this.tocTimer) {
|
if (this.tocTimer) {
|
||||||
clearTimeout(this.tocTimer);
|
clearTimeout(this.tocTimer);
|
||||||
@ -617,6 +654,43 @@ class ConceptionAssistant {
|
|||||||
this.showNotification('Markdown file exported', 'success');
|
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) {
|
importMarkdown(event) {
|
||||||
@ -649,6 +723,140 @@ class ConceptionAssistant {
|
|||||||
this.showNotification(`${isDark ? 'Dark' : 'Light'} mode activated`, 'success');
|
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) {
|
async handleAI(action) {
|
||||||
const selection = window.getSelection().toString().trim();
|
const selection = window.getSelection().toString().trim();
|
||||||
const fullContent = this.editor.innerText;
|
const fullContent = this.editor.innerText;
|
||||||
@ -1086,12 +1294,18 @@ class ConceptionAssistant {
|
|||||||
|
|
||||||
async togglePreview() {
|
async togglePreview() {
|
||||||
const previewBtn = document.getElementById('preview-toggle');
|
const previewBtn = document.getElementById('preview-toggle');
|
||||||
|
const mainElement = document.querySelector('main');
|
||||||
|
|
||||||
if (!this.isPreviewMode) {
|
if (!this.isPreviewMode) {
|
||||||
// Switch to preview mode
|
// Switch to preview mode
|
||||||
this.originalContent = this.editor.innerHTML;
|
this.originalContent = this.editor.innerHTML;
|
||||||
const markdownContent = this.editor.innerText;
|
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
|
// Configure Marked for GitHub-compatible rendering
|
||||||
if (typeof marked !== 'undefined') {
|
if (typeof marked !== 'undefined') {
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
@ -1185,6 +1399,11 @@ class ConceptionAssistant {
|
|||||||
this.editor.style.border = '';
|
this.editor.style.border = '';
|
||||||
this.editor.style.borderRadius = '';
|
this.editor.style.borderRadius = '';
|
||||||
|
|
||||||
|
// Remove preview-mode class to restore normal grid layout
|
||||||
|
if (mainElement) {
|
||||||
|
mainElement.classList.remove('preview-mode');
|
||||||
|
}
|
||||||
|
|
||||||
// Change button
|
// Change button
|
||||||
previewBtn.innerHTML = 'Preview';
|
previewBtn.innerHTML = 'Preview';
|
||||||
previewBtn.classList.remove('secondary');
|
previewBtn.classList.remove('secondary');
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"archiver": "^7.0.1",
|
||||||
"dotenv": "^17.2.2",
|
"dotenv": "^17.2.2",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"puppeteer": "^24.22.3",
|
"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 router = express.Router();
|
||||||
const puppeteer = require('puppeteer');
|
const puppeteer = require('puppeteer');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const archiver = require('archiver');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
// POST /api/export/pdf - Generate PDF from markdown content
|
// POST /api/export/pdf - Generate PDF from markdown content
|
||||||
router.post('/pdf', async (req, res) => {
|
router.post('/pdf', async (req, res) => {
|
||||||
@ -509,4 +511,163 @@ function sanitizeFilename(filename) {
|
|||||||
.substring(0, 100); // Limit length
|
.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;
|
module.exports = router;
|
||||||
135
routes/index.js
135
routes/index.js
@ -7,6 +7,141 @@ router.get('/', (req, res) => {
|
|||||||
res.send(getPage());
|
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
|
// About route
|
||||||
router.get('/about', (req, res) => {
|
router.get('/about', (req, res) => {
|
||||||
const { getHeader } = require('../views/header');
|
const { getHeader } = require('../views/header');
|
||||||
|
|||||||
@ -19,10 +19,13 @@ function getLeftPanel() {
|
|||||||
|
|
||||||
<div class="template-form">
|
<div class="template-form">
|
||||||
<div class="form-group">
|
<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>
|
||||||
|
|
||||||
<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">
|
<button id="load-template" class="btn success" title="Load default template">
|
||||||
Load Default Template
|
Load Default Template
|
||||||
</button>
|
</button>
|
||||||
@ -57,7 +60,11 @@ function getRightPanel() {
|
|||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<button class="nav-btn" id="export-md" title="Export as Markdown">
|
<button class="nav-btn" id="export-md" title="Export as Markdown">
|
||||||
<span class="icon">↓</span>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -72,6 +79,25 @@ function getRightPanel() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -17,6 +17,7 @@ function getMain() {
|
|||||||
<button id="save-journal" class="btn success">Save</button>
|
<button id="save-journal" class="btn success">Save</button>
|
||||||
<button id="load-journal" class="btn">Load</button>
|
<button id="load-journal" class="btn">Load</button>
|
||||||
<button id="preview-toggle" class="btn primary">Preview</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>
|
<span id="save-status" class="text-light"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user