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+SSave journal
Ctrl+ZUndo
Ctrl+YRedo
TabIndent text
EscClose side panels
+
+ +
+

Markdown Formatting

+ + + + + + + + + + + + + + + + + + + + + + + + + +
# TitleHeading level 1
## TitleHeading level 2
**bold**Bold text
*italic*Italic text
- itemBullet 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} + + + + + +
+
+

${title}

+
${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
+
+
+
+ + + +`; + + // 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 + + + + + + + + + + +
+
+

Design Journal

+
${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
+
+
+
+ + + + + `); +}); + // 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() {
-

Start with a default template for your design journal.

+

Create a new document or start with a template.

-
+
+ @@ -57,7 +60,11 @@ function getRightPanel() {
@@ -72,6 +79,25 @@ function getRightPanel() {
+ + + + `; 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() { +