Générateur de portfolios PDF Figures Libres (app Nuxt autonome)
App Nuxt 3 + Nuxt UI qui lit les projets du CMS Grav et compose des portfolios A3 paysage exportés en PDF (gabarit book_v3) : composeur WYSIWYG, templates cover/toc/projet/grille/page libre, pipeline Playwright + Ghostscript, gel du contenu à la génération, partage public opt-in. Dépôt autonome : Dockerfile (base Playwright + Ghostscript + ffmpeg + rsync) inclus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import { duplicatePortfolio } from '../../../lib/portfolio-store'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const copy = await duplicatePortfolio(getRouterParam(event, 'id')!)
|
||||
if (!copy) throw createError({ statusCode: 404, statusMessage: 'Portfolio introuvable' })
|
||||
return copy
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getPortfolio } from '../../../lib/portfolio-store'
|
||||
import { generatePdf } from '../../../lib/pdf-pipeline'
|
||||
|
||||
// Génération séquentielle : une seule instance Chromium à la fois.
|
||||
let queue: Promise<unknown> = Promise.resolve()
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const portfolio = await getPortfolio(getRouterParam(event, 'id')!)
|
||||
if (!portfolio) throw createError({ statusCode: 404, statusMessage: 'Portfolio introuvable' })
|
||||
|
||||
const { fileName } = await readBody<{ fileName?: string }>(event).catch(() => ({ fileName: undefined }))
|
||||
|
||||
const run = queue.then(() => generatePdf(portfolio, fileName))
|
||||
queue = run.catch(() => {})
|
||||
try {
|
||||
return await run
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: `Échec de la génération : ${err instanceof Error ? err.message : String(err)}`,
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
import { deletePortfolio } from '../../../lib/portfolio-store'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await deletePortfolio(getRouterParam(event, 'id')!)
|
||||
return { ok: true }
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import { getPortfolio } from '../../../lib/portfolio-store'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const portfolio = await getPortfolio(getRouterParam(event, 'id')!)
|
||||
if (!portfolio) throw createError({ statusCode: 404, statusMessage: 'Portfolio introuvable' })
|
||||
return portfolio
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Portfolio } from '~~/types'
|
||||
import { getPortfolio, savePortfolio } from '../../../lib/portfolio-store'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, 'id')!
|
||||
const existing = await getPortfolio(id)
|
||||
if (!existing) throw createError({ statusCode: 404, statusMessage: 'Portfolio introuvable' })
|
||||
|
||||
const body = await readBody<Portfolio>(event)
|
||||
if (!body || !Array.isArray(body.pages) || typeof body.name !== 'string') {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Portfolio invalide' })
|
||||
}
|
||||
// L'id et les dates de création restent ceux du serveur.
|
||||
const portfolio: Portfolio = {
|
||||
...existing,
|
||||
...body,
|
||||
id,
|
||||
createdAt: existing.createdAt,
|
||||
}
|
||||
return await savePortfolio(portfolio)
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
// Active / coupe le lien public d'un portfolio (bouton « Partager un lien public »).
|
||||
// - body { enabled: false } → coupe le partage (publicShare = false), sans
|
||||
// toucher au snapshot figé (réutilisé par le PDF).
|
||||
// - sinon → (re)fige le contenu courant et active le partage. Re-cliquer
|
||||
// « republie » : l'instantané reflète l'état courant du portfolio.
|
||||
import { getPortfolio, savePortfolio } from '../../../lib/portfolio-store'
|
||||
import { freezeContent, sanitizeFileName } from '../../../lib/pdf-pipeline'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, 'id') ?? ''
|
||||
const portfolio = await getPortfolio(id)
|
||||
if (!portfolio) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Portfolio introuvable' })
|
||||
}
|
||||
|
||||
const body = await readBody<{ enabled?: boolean }>(event).catch(() => ({}))
|
||||
|
||||
if (body?.enabled === false) {
|
||||
portfolio.publicShare = false
|
||||
// Renvoie le portfolio complet pour resynchroniser le client (évite qu'une
|
||||
// sauvegarde auto ultérieure ne réécrase publicShare/frozenContent).
|
||||
return await savePortfolio(portfolio)
|
||||
}
|
||||
|
||||
// (Re)geler le contenu courant puis activer le partage
|
||||
const fileName = portfolio.frozenContent?.fileName || sanitizeFileName(portfolio.name)
|
||||
portfolio.frozenContent = await freezeContent(portfolio, fileName)
|
||||
portfolio.publicShare = true
|
||||
return await savePortfolio(portfolio)
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
// « Réactualiser depuis Grav » (spec §7.3) : supprime le snapshot figé ;
|
||||
// la prochaine génération repartira du contenu live.
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { getPortfolio, savePortfolio } from '../../../lib/portfolio-store'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, 'id')!
|
||||
const portfolio = await getPortfolio(id)
|
||||
if (!portfolio) throw createError({ statusCode: 404, statusMessage: 'Portfolio introuvable' })
|
||||
|
||||
portfolio.frozenContent = undefined
|
||||
await rm(join(useRuntimeConfig().dataDir, 'frozen', portfolio.id), { recursive: true, force: true })
|
||||
await savePortfolio(portfolio)
|
||||
return { ok: true }
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import { listPortfolios } from '../../lib/portfolio-store'
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
return { portfolios: await listPortfolios() }
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createPortfolio } from '../../lib/portfolio-store'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { name } = await readBody<{ name?: string }>(event)
|
||||
return await createPortfolio(name?.trim() ?? '')
|
||||
})
|
||||
Reference in New Issue
Block a user