Installatie pagina omgezet van hardcoded JSX naar Markdown-driven rendering. Browser-based editor toegevoegd (alleen in dev mode) met split-pane layout, sneltoetsen, toolbar en drag & drop afbeeldingen. Nieuwe afbeeldingen voor Git en Python installatie-instructies. Workshop op uitverkocht gezet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
5.8 KiB
JavaScript
169 lines
5.8 KiB
JavaScript
/**
|
|
* vite-plugin-editor-api.js - Vite dev server plugin voor de Markdown editor
|
|
*
|
|
* Biedt API endpoints voor het opslaan van Markdown en het uploaden van afbeeldingen.
|
|
* Alleen actief tijdens development (npm run dev), niet in productie builds.
|
|
*
|
|
* Endpoints:
|
|
* - POST /__editor/save - Slaat Markdown content op naar content/installatie.md
|
|
* - POST /__editor/upload - Upload een afbeelding naar public/images/installatie/
|
|
* - GET /__editor/images - Lijst beschikbare afbeeldingen
|
|
*/
|
|
|
|
/* eslint-disable no-undef */
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
export function editorApiPlugin() {
|
|
return {
|
|
name: 'editor-api',
|
|
configureServer(server) {
|
|
const rootDir = server.config.root;
|
|
|
|
// POST /__editor/save - Markdown content opslaan
|
|
server.middlewares.use('/__editor/save', (req, res, next) => {
|
|
if (req.method !== 'POST') return next();
|
|
|
|
let body = '';
|
|
req.on('data', chunk => { body += chunk; });
|
|
req.on('end', () => {
|
|
try {
|
|
const { content } = JSON.parse(body);
|
|
const filePath = path.join(rootDir, 'content', 'installatie.md');
|
|
|
|
// Zorg dat de content directory bestaat
|
|
const dir = path.dirname(filePath);
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ success: true, path: filePath }));
|
|
} catch (err) {
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: err.message }));
|
|
}
|
|
});
|
|
});
|
|
|
|
// POST /__editor/upload - Afbeelding uploaden
|
|
server.middlewares.use('/__editor/upload', (req, res, next) => {
|
|
if (req.method !== 'POST') return next();
|
|
|
|
const chunks = [];
|
|
req.on('data', chunk => chunks.push(chunk));
|
|
req.on('end', () => {
|
|
try {
|
|
const buffer = Buffer.concat(chunks);
|
|
const boundary = req.headers['content-type'].split('boundary=')[1];
|
|
const { filename, fileData } = parseMultipart(buffer, boundary);
|
|
|
|
if (!filename) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Geen bestand gevonden' }));
|
|
return;
|
|
}
|
|
|
|
// Valideer bestandstype
|
|
const ext = path.extname(filename).toLowerCase();
|
|
const allowed = ['.png', '.jpg', '.jpeg', '.gif', '.webp'];
|
|
if (!allowed.includes(ext)) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: `Bestandstype ${ext} niet toegestaan` }));
|
|
return;
|
|
}
|
|
|
|
// Bepaal bestandsnaam (voeg timestamp toe bij duplicaat)
|
|
const imageDir = path.join(rootDir, 'public', 'images', 'installatie');
|
|
if (!fs.existsSync(imageDir)) {
|
|
fs.mkdirSync(imageDir, { recursive: true });
|
|
}
|
|
|
|
let finalName = filename;
|
|
const destPath = path.join(imageDir, finalName);
|
|
if (fs.existsSync(destPath)) {
|
|
const base = path.basename(filename, ext);
|
|
const timestamp = Date.now();
|
|
finalName = `${base}-${timestamp}${ext}`;
|
|
}
|
|
|
|
fs.writeFileSync(path.join(imageDir, finalName), fileData);
|
|
|
|
const relativePath = `images/installatie/${finalName}`;
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ success: true, path: relativePath, filename: finalName }));
|
|
} catch (err) {
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: err.message }));
|
|
}
|
|
});
|
|
});
|
|
|
|
// GET /__editor/images - Lijst beschikbare afbeeldingen
|
|
server.middlewares.use('/__editor/images', (req, res, next) => {
|
|
if (req.method !== 'GET') return next();
|
|
|
|
try {
|
|
const imageDir = path.join(rootDir, 'public', 'images', 'installatie');
|
|
if (!fs.existsSync(imageDir)) {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ images: [] }));
|
|
return;
|
|
}
|
|
|
|
const files = fs.readdirSync(imageDir)
|
|
.filter(f => /\.(png|jpg|jpeg|gif|webp)$/i.test(f))
|
|
.map(f => ({
|
|
filename: f,
|
|
path: `images/installatie/${f}`,
|
|
}));
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ images: files }));
|
|
} catch (err) {
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: err.message }));
|
|
}
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Simpele multipart/form-data parser.
|
|
* Extraheert het eerste bestand uit de multipart body.
|
|
*/
|
|
function parseMultipart(buffer, boundary) {
|
|
const boundaryBuffer = Buffer.from(`--${boundary}`);
|
|
const parts = [];
|
|
let start = 0;
|
|
|
|
while (true) {
|
|
const idx = buffer.indexOf(boundaryBuffer, start);
|
|
if (idx === -1) break;
|
|
if (start > 0) {
|
|
// Verwijder de trailing \r\n voor de boundary
|
|
parts.push(buffer.slice(start, idx - 2));
|
|
}
|
|
start = idx + boundaryBuffer.length + 2; // Skip boundary + \r\n
|
|
}
|
|
|
|
for (const part of parts) {
|
|
const headerEnd = part.indexOf('\r\n\r\n');
|
|
if (headerEnd === -1) continue;
|
|
|
|
const headers = part.slice(0, headerEnd).toString('utf-8');
|
|
const filenameMatch = headers.match(/filename="(.+?)"/);
|
|
|
|
if (filenameMatch) {
|
|
return {
|
|
filename: filenameMatch[1],
|
|
fileData: part.slice(headerEnd + 4),
|
|
};
|
|
}
|
|
}
|
|
|
|
return { filename: null, fileData: null };
|
|
}
|