Textes projet éditables suivant Grav + écriture atomique du store

- Champ Présentation prérempli avec le texte Grav, suivi modifié/synchro
  (textOverride undefined = suit Grav, sinon figé ; bouton Réinitialiser)
- Fix génération PDF : savePortfolio écrit en atomique (temp+rename) pour
  éviter la corruption de lecture par l'autosave concurrent du rendu print
- Toast d'échec de génération explicite (message serveur)
- Refonte architecture visuelle, fond unifié white/tint/full, dispositions
  couverture bandeau/colonne, instantané public complet, index 3 lignes
This commit is contained in:
2026-07-06 13:26:06 +02:00
parent 59dfbe4155
commit ef1f14335d
22 changed files with 750 additions and 334 deletions

View File

@@ -15,5 +15,15 @@ export default defineEventHandler(async (event): Promise<{ portfolio: Portfolio;
throw createError({ statusCode: 404, statusMessage: 'Portfolio introuvable' })
}
return { portfolio, projects: portfolio.frozenContent.projects }
// Vrai instantané : on rend la composition figée (pages + mise en forme), pas
// la composition live. Repli sur le live pour les instantanés antérieurs.
const frozen = portfolio.frozenContent
const snapshot: Portfolio = {
...portfolio,
name: frozen.name ?? portfolio.name,
cover: frozen.cover ?? portfolio.cover,
pages: frozen.pages ?? portfolio.pages,
}
return { portfolio: snapshot, projects: frozen.projects }
})

View File

@@ -68,7 +68,16 @@ export async function freezeContent(portfolio: Portfolio, fileName: string): Pro
}
}
return { generatedAt: new Date().toISOString(), fileName, projects }
// Instantané de la composition (mise en forme incluse) : la vue publique
// rendra CETTE version figée, indépendante des éditions ultérieures.
return {
generatedAt: new Date().toISOString(),
fileName,
projects,
name: portfolio.name,
cover: structuredClone(portfolio.cover),
pages: structuredClone(portfolio.pages),
}
}
export interface GenerateResult {

View File

@@ -1,5 +1,5 @@
// Bibliothèque de portfolios : un fichier JSON par portfolio (spec §13).
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'
import { mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { nanoid } from 'nanoid'
import type { Portfolio, PortfolioSummary } from '~~/types'
@@ -61,7 +61,16 @@ export async function savePortfolio(portfolio: Portfolio, touch = true): Promise
// aligner updatedAt sur lastExport.at et marquer le PDF « à jour »).
if (touch) portfolio.updatedAt = new Date().toISOString()
await mkdir(portfoliosDir(), { recursive: true })
await writeFile(pathFor(portfolio.id), JSON.stringify(portfolio, null, 2))
// Écriture ATOMIQUE (fichier temporaire + rename) : l'autosave de l'éditeur
// peut écrire pendant que la génération PDF relit le portfolio via /print.
// Un writeFile en place tronque le fichier → lecture concurrente = JSON
// partiel → getPortfolio renvoie null → rendu print en échec. Le rename est
// atomique sur le même volume : les lecteurs voient toujours l'ancien OU le
// nouveau fichier complet, jamais un état intermédiaire.
const dest = pathFor(portfolio.id)
const tmp = `${dest}.tmp-${nanoid(6)}`
await writeFile(tmp, JSON.stringify(portfolio, null, 2))
await rename(tmp, dest)
return portfolio
}