Add Rédaction view: Binder + TipTap editor
Phase 1 of Solution 3 (Scrivener-inspired writing environment): - Binder panel: scenes grouped by act, status indicators, word counts - TipTap rich text editor with toolbar (bold, italic, headings, lists, blockquote) - Auto-save after 1.5s of inactivity - New NocoDB table "Contenu" (SceneId, Texte, Mots) - API routes: GET/PUT /api/contenu/:sceneId, GET /api/contenu (word counts) - Total word count in binder footer - Navigation: Vision / Frise / Rédaction Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ const NOCODB_TOKEN = process.env.NOCODB_TOKEN || '';
|
||||
const NOCODB_BASE_ID = process.env.NOCODB_BASE_ID || '';
|
||||
const NOCODB_TABLE_ID = process.env.NOCODB_TABLE_ID || '';
|
||||
const NOCODB_VISION_TABLE_ID = process.env.NOCODB_VISION_TABLE_ID || '';
|
||||
const NOCODB_CONTENU_TABLE_ID = process.env.NOCODB_CONTENU_TABLE_ID || '';
|
||||
|
||||
// Auth
|
||||
function makeToken(ts) {
|
||||
@@ -173,6 +174,71 @@ app.patch('/api/vision/:id', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Contenu API (scene text for Rédaction view) ──
|
||||
|
||||
// List all word counts (for binder display)
|
||||
app.get('/api/contenu', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_CONTENU_TABLE_ID}?limit=200&fields=SceneId,Mots`;
|
||||
const r = await fetch(url, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
const data = await r.json();
|
||||
res.json(data);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get content for a scene
|
||||
app.get('/api/contenu/:sceneId', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_CONTENU_TABLE_ID}?where=(SceneId,eq,${req.params.sceneId})&limit=1`;
|
||||
const r = await fetch(url, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
const data = await r.json();
|
||||
const row = data.list?.[0] || null;
|
||||
res.json(row);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Save content for a scene (create or update)
|
||||
app.put('/api/contenu/:sceneId', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const sceneId = Number(req.params.sceneId);
|
||||
// Check if row exists
|
||||
const findUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_CONTENU_TABLE_ID}?where=(SceneId,eq,${sceneId})&limit=1`;
|
||||
const findR = await fetch(findUrl, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
const findData = await findR.json();
|
||||
const existing = findData.list?.[0];
|
||||
|
||||
const payload = { SceneId: sceneId, Texte: req.body.Texte, Mots: req.body.Mots || 0 };
|
||||
|
||||
if (existing) {
|
||||
// Update
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_CONTENU_TABLE_ID}/${existing.Id}`;
|
||||
const r = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await r.json();
|
||||
res.json(data);
|
||||
} else {
|
||||
// Create
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_CONTENU_TABLE_ID}`;
|
||||
const r = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await r.json();
|
||||
res.status(r.status).json(data);
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Auth gate — redirect to login page if not authenticated
|
||||
app.get('/', (req, res) => {
|
||||
if (isAuth(req)) {
|
||||
|
||||
Reference in New Issue
Block a user