- React + TipTap editor with formatting toolbar (bold, italic, underline, strikethrough, alignment, highlight, scene breaks) - Custom image node view with resize and alignment controls; server-side WebP conversion via sharp - Express + SQLite backend with JWT auth and admin user management - Export to PDF, EPUB, and ODT - Five themes (Midnight, Gothic Night, Enchanted Forest, Aged Manuscript, Neon Noir); Lora body font for readability - Writing streak, daily word goal, milestones, and Ollama writing prompts - Docker Compose setup for self-hosted deployment behind NPMplus Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
45 lines
1.4 KiB
JavaScript
45 lines
1.4 KiB
JavaScript
import { Router } from 'express'
|
|
import multer from 'multer'
|
|
import sharp from 'sharp'
|
|
import path from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
import { mkdirSync } from 'fs'
|
|
import db from '../db.js'
|
|
import { auth } from '../middleware/auth.js'
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
const uploadDir = path.join(__dirname, '..', 'uploads')
|
|
mkdirSync(uploadDir, { recursive: true })
|
|
|
|
const upload = multer({
|
|
storage: multer.memoryStorage(),
|
|
limits: { fileSize: 20 * 1024 * 1024 },
|
|
fileFilter: (req, file, cb) =>
|
|
file.mimetype.startsWith('image/') ? cb(null, true) : cb(new Error('Images only')),
|
|
})
|
|
|
|
const router = Router()
|
|
router.use(auth)
|
|
|
|
router.post('/', upload.single('file'), async (req, res) => {
|
|
if (!req.file) return res.status(400).json({ error: 'No file uploaded' })
|
|
|
|
const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}.webp`
|
|
const outputPath = path.join(uploadDir, filename)
|
|
|
|
try {
|
|
await sharp(req.file.buffer)
|
|
.resize(2400, 2400, { fit: 'inside', withoutEnlargement: true })
|
|
.webp({ quality: 88 })
|
|
.toFile(outputPath)
|
|
|
|
db.prepare('INSERT INTO images (user_id, filename) VALUES (?, ?)').run(req.user.id, filename)
|
|
res.json({ url: `/uploads/${filename}` })
|
|
} catch (err) {
|
|
console.error('Image processing error:', err)
|
|
res.status(500).json({ error: 'Image processing failed' })
|
|
}
|
|
})
|
|
|
|
export default router
|