diff --git a/assets/css/github-preview.css b/assets/css/github-preview.css
index 1bdc089..76f1849 100644
--- a/assets/css/github-preview.css
+++ b/assets/css/github-preview.css
@@ -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 {
diff --git a/assets/css/style.css b/assets/css/style.css
index 7b4944c..507dc9e 100644
--- a/assets/css/style.css
+++ b/assets/css/style.css
@@ -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 */
diff --git a/assets/js/app.js b/assets/js/app.js
index 5a2c19c..4681ff9 100644
--- a/assets/js/app.js
+++ b/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 = `
+
+
+
Document Management
+
+
+ | Ctrl+S |
+ Save journal |
+
+
+ | Ctrl+Z |
+ Undo |
+
+
+ | Ctrl+Y |
+ Redo |
+
+
+ | Tab |
+ Indent text |
+
+
+ | Esc |
+ Close side panels |
+
+
+
+
+
+
Markdown Formatting
+
+
+ | # Title |
+ Heading level 1 |
+
+
+ | ## Title |
+ Heading level 2 |
+
+
+ | **bold** |
+ Bold text |
+
+
+ | *italic* |
+ Italic text |
+
+
+ | - item |
+ Bullet list |
+
+
+ | \`code\` |
+ Inline code |
+
+
+
+
+
+
+
+
+ `;
+
+ // 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');
diff --git a/data/704e7897-8f42-4007-b5e2-5c43a1237a23.md b/data/704e7897-8f42-4007-b5e2-5c43a1237a23.md
new file mode 100644
index 0000000..d481121
--- /dev/null
+++ b/data/704e7897-8f42-4007-b5e2-5c43a1237a23.md
@@ -0,0 +1,96 @@
+# TD/TP 5 - Persistance Objet-Relationnel
+
+## Objectif
+
+Implémenter un système de persistance objet-relationnel pour gérer des livres (Book) et leurs éditeurs (Publisher) en utilisant une base de données Derby.
+
+## Architecture
+
+Le projet suit le pattern Data Mapper pour séparer la logique métier de la persistance:
+
+- **Classes du domaine**: objets métier (Book, Publisher)
+- **Classes Mapper**: gestion de la persistance (BookMapper, PublisherMapper)
+- **Interfaces**: contrats pour les objets persistants (DomainObject, Mapper)
+- **Utilitaire DB**: gestion de la connexion à Derby
+
+## Structure du code
+
+```mermaid
+classDiagram
+ class DomainObject {
+ <>
+ +getId() Object
+ +setId(Object id) void
+ }
+
+ class Mapper~T~ {
+ <>
+ +insert(T newObject) Object
+ +find(Object oid) T
+ +findMany(Object criterion) Set~T~
+ +update(T objToUpdate) void
+ +delete(T objToDelete) void
+ +deleteAll() void
+ }
+
+ class Book {
+ -String isbn
+ -String title
+ -String author
+ -float price
+ +getId() Object
+ +setId(Object id) void
+ +getIsbn() String
+ +getTitle() String
+ +getAuthor() String
+ +getPrice() float
+ }
+
+ class BookMapper {
+ -DB db
+ +insert(Book newBook) Object
+ +find(Object isbn) Book
+ +findMany(Object authorName) Set~Book~
+ +update(Book bookToUpdate) void
+ +delete(Book bookToDelete) void
+ +deleteAll() void
+ }
+
+ class DB {
+ +getConnection() Connection
+ +createTable(String sql) void
+ }
+
+ DomainObject <|.. Book
+ Mapper <|.. BookMapper
+ BookMapper --> Book : persiste
+ BookMapper --> DB : utilise
+```
+
+## Travail à réaliser
+
+### Partie 1: Persistance de Book
+1. Compléter `Book.getId()` et `Book.setId()` (DomainObject)
+2. Implémenter les méthodes CRUD dans `BookMapper`
+3. Définir le SQL de création de table dans `BookMapper.createTableStmt`
+
+### Partie 2: Association Book-Publisher
+1. Créer `PublisherMapper`
+2. Modifier `Book` pour ajouter un attribut `Publisher`
+3. Modifier `BookMapper` pour gérer l'association
+4. Gérer les règles: sauvegarde en cascade, suppression en cascade
+
+## Commandes Maven
+
+```bash
+mvn clean # Nettoyer les compilations
+mvn compile # Compiler le projet
+mvn test # Exécuter les tests
+```
+
+## Base de données
+
+- Nom: Roux
+- SGBD: Apache Derby 10.16.1.1
+- Port: 1527
+- Driver: `jdbc:derby://localhost:1527/Roux`
diff --git a/data/uuid_map.json b/data/uuid_map.json
new file mode 100644
index 0000000..09afeda
--- /dev/null
+++ b/data/uuid_map.json
@@ -0,0 +1,3 @@
+{
+ "0": "704e7897-8f42-4007-b5e2-5c43a1237a23"
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index b8e5d18..9dcd960 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/routes/export.js b/routes/export.js
index 7c4f264..850f231 100644
--- a/routes/export.js
+++ b/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 = `
+
+
+
+
+ ${title}
+
+
+
+
+
+
+
+
+
+`;
+
+ // 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;
\ No newline at end of file
diff --git a/routes/index.js b/routes/index.js
index 0f3ef0a..f7ef40e 100644
--- a/routes/index.js
+++ b/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(`
+
+
+
+
+ Presentation - Design Journal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `);
+});
+
// About route
router.get('/about', (req, res) => {
const { getHeader } = require('../views/header');
diff --git a/views/header.js b/views/header.js
index 46841ce..2c0ff35 100644
--- a/views/header.js
+++ b/views/header.js
@@ -19,10 +19,13 @@ function getLeftPanel() {
+
+
+
Statistics
+
+
Words: 0
+
Characters: 0
+
Reading time: 0 min
+
+
+
+
+
Help
+
+
+
+
`;
diff --git a/views/main.js b/views/main.js
index 3aec281..d35e5cf 100644
--- a/views/main.js
+++ b/views/main.js
@@ -17,6 +17,7 @@ function getMain() {
+