186 lines
8.7 KiB
JavaScript
186 lines
8.7 KiB
JavaScript
/**
|
|
* Client Penpot API Direct — accès port 9003 (backend sans nginx)
|
|
* update-file avec changes add-obj pour contrôle pixel-perfect
|
|
*
|
|
* Clés API response : kebab-case ("default-team-id")
|
|
* Clés envoi body : kebab-case (via toKebab())
|
|
* Clés obj shapes : camelCase → converties par toKebab()
|
|
*/
|
|
import http from 'http';
|
|
import { randomUUID } from 'crypto';
|
|
|
|
const TOKEN = process.env.PENPOT_TOKEN;
|
|
const HOST = '192.168.1.150';
|
|
const PORT = 9003;
|
|
|
|
// ─── RPC ─────────────────────────────────────────────────────────────────────
|
|
function toKebab(obj) {
|
|
if (Array.isArray(obj)) return obj.map(toKebab);
|
|
if (obj && typeof obj === 'object') {
|
|
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [
|
|
k.replace(/([a-z0-9])([A-Z])/g,'$1-$2').toLowerCase(), toKebab(v)
|
|
]));
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
function rpc(cmd, body = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const payload = JSON.stringify(toKebab(body));
|
|
const req = http.request({
|
|
hostname: HOST, port: PORT,
|
|
path: '/api/rpc/command/' + cmd, method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Token ' + TOKEN,
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'Content-Length': Buffer.byteLength(payload),
|
|
}
|
|
}, res => {
|
|
let d = ''; res.on('data', c => d += c);
|
|
res.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve(d); } });
|
|
});
|
|
req.on('error', reject);
|
|
req.write(payload); req.end();
|
|
});
|
|
}
|
|
|
|
// ─── Geometry ────────────────────────────────────────────────────────────────
|
|
function geo(x, y, w, h) {
|
|
return {
|
|
selrect: { x, y, width: w, height: h, x1: x, y1: y, x2: x+w, y2: y+h },
|
|
points: [{x,y},{x:x+w,y},{x:x+w,y:y+h},{x,y:y+h}],
|
|
transform: {a:1,b:0,c:0,d:1,e:0,f:0},
|
|
transformInverse: {a:1,b:0,c:0,d:1,e:0,f:0},
|
|
};
|
|
}
|
|
|
|
const ROOT = '00000000-0000-0000-0000-000000000000';
|
|
|
|
// ─── Builders ────────────────────────────────────────────────────────────────
|
|
function rect({ name, x, y, w, h, color, opacity=1, r=0, parentId, frameId: fid }) {
|
|
const id = randomUUID();
|
|
const pid = parentId || fid || ROOT;
|
|
const fi = fid || pid;
|
|
return { type:'add-obj', id, pageId: PAGE_ID, frameId: fi, parentId: pid,
|
|
obj: { id, type:'rect', name, x, y, width:w, height:h, parentId:pid, frameId:fi,
|
|
fills:[{fillColor:color, fillOpacity:opacity}],
|
|
...(r > 0 && {r1:r,r2:r,r3:r,r4:r}),
|
|
...geo(x,y,w,h) }};
|
|
}
|
|
|
|
function text({ name, text: textContent, x, y, w, h, size, weight='400', color='#FFFFFF', align='center', spacing=0, parentId, frameId: fid }) {
|
|
const t = textContent;
|
|
const id = randomUUID();
|
|
const pid = parentId || fid || ROOT;
|
|
const fi = fid || pid;
|
|
return { type:'add-obj', id, pageId: PAGE_ID, frameId: fi, parentId: pid,
|
|
obj: { id, type:'text', name, x, y, width:w, height:h, parentId:pid, frameId:fi,
|
|
fills:[{fillColor:color, fillOpacity:1}],
|
|
fontFamily:'Work Sans', fontSize:String(size), fontWeight:String(weight),
|
|
fontStyle:'normal', verticalAlign:'top',
|
|
content: { type:'root', children:[{ type:'paragraph-set', children:[{
|
|
type:'paragraph', textAlign:align, children:[{
|
|
text: t,
|
|
fontId:'gfont-work-sans', fontFamily:'Work Sans',
|
|
fontSize: String(size), fontWeight: String(weight),
|
|
fontStyle:'normal', letterSpacing: String(spacing),
|
|
lineHeight: 1.2, textDecoration:'none',
|
|
fills:[{fillColor:color, fillOpacity:1}],
|
|
}]
|
|
}]}]},
|
|
...geo(x,y,w,h) }};
|
|
}
|
|
|
|
// ─── MAIN ────────────────────────────────────────────────────────────────────
|
|
// 1. Récupérer profil + projet
|
|
const prof = await rpc('get-profile');
|
|
// Le backend retourne les clés en kebab-case
|
|
const teamId = prof['default-team-id'] || prof.defaultTeamId;
|
|
const projectId = prof['default-project-id'] || prof.defaultProjectId;
|
|
console.log(`👤 ${prof.fullname} teamId=${teamId?.slice(0,8)} projectId=${projectId?.slice(0,8)}`);
|
|
if (!teamId || !projectId) { console.error('❌ Profil incomplet:', JSON.stringify(prof)); process.exit(1); }
|
|
|
|
// 2. Supprimer ancien fichier si présent
|
|
const filesResult = await rpc('get-project-files', { projectId });
|
|
const filesArr = Array.isArray(filesResult) ? filesResult : (filesResult?.files || []);
|
|
for (const f of filesArr.filter(f => f.name?.includes('SOLDES Direct'))) {
|
|
await rpc('delete-file', { id: f.id });
|
|
console.log('🗑 Supprimé:', f.name);
|
|
}
|
|
|
|
// 3. Créer nouveau fichier
|
|
const file = await rpc('create-file', { projectId, name:'Affiche SOLDES Direct', isShared:false });
|
|
const FILE_ID = file.id;
|
|
const PAGE_ID = Object.keys(file.data?.pagesIndex || {})[0] || file.data?.pages?.[0];
|
|
console.log(`📄 File ${FILE_ID?.slice(0,8)} Page ${PAGE_ID?.slice(0,8)}`);
|
|
if (!FILE_ID || !PAGE_ID) { console.error('❌ IDs manquants', file); process.exit(1); }
|
|
|
|
let revn = file.revn ?? 0;
|
|
const W = 600, H = 900;
|
|
|
|
// ─── 4. Shapes ───────────────────────────────────────────────────────────────
|
|
// Fond principal : rect rouge (pas frame — frame nécessite shapes[] explicite)
|
|
const FRAME_ID = ROOT; // On place tout au root, le rect rouge sert de fond visuel
|
|
const F = { parentId:ROOT, frameId:ROOT };
|
|
|
|
const changes = [
|
|
// Fond rouge (rectangle plein cadre)
|
|
rect({...F, name:'Fond rouge', x:0, y:0, w:W, h:H, color:'#C0392B'}),
|
|
|
|
// ── Rectangles ──────────────────────────────────────────────────────────
|
|
rect({...F, name:'Bande haut', x:0, y:0, w:W, h:14, color:'#F1C40F'}),
|
|
rect({...F, name:'Bande bas', x:0, y:886, w:W, h:14, color:'#F1C40F'}),
|
|
rect({...F, name:'Header bg', x:0, y:14, w:W, h:92, color:'#7B241C'}),
|
|
rect({...F, name:'Sep1', x:80, y:445, w:440, h:3, color:'#F1C40F'}),
|
|
rect({...F, name:'Badge', x:90, y:460, w:420, h:70, color:'#F1C40F', r:35}),
|
|
rect({...F, name:'Sep2', x:80, y:775, w:440, h:3, color:'#F8C471'}),
|
|
|
|
// ── Textes (width=W → textAlign:center centré sur 600px) ─────────────────
|
|
text({...F, name:'Collection', text:'✦ Collection Printemps 2026 ✦',
|
|
x:0, y:32, w:W, h:48, size:14, weight:'700', color:'#F1C40F', align:'center', spacing:4}),
|
|
|
|
text({...F, name:'SOLDES', text:'SOLDES',
|
|
x:0, y:118, w:W, h:100, size:88, weight:'900', color:'#F1C40F', align:'center', spacing:8}),
|
|
|
|
text({...F, name:'-50%', text:'-50%',
|
|
x:0, y:218, w:W, h:220, size:185, weight:'900', color:'#FFFFFF', align:'center'}),
|
|
|
|
text({...F, name:'Sous-titre', text:'SUR TOUTES NOS CHAUSSURES',
|
|
x:0, y:400, w:W, h:42, size:21, weight:'700', color:'#F1C40F', align:'center', spacing:3}),
|
|
|
|
text({...F, name:'Offre', text:'⚡ OFFRE LIMITÉE ⚡',
|
|
x:90, y:476, w:420, h:40, size:22, weight:'900', color:'#7B241C', align:'center'}),
|
|
|
|
text({...F, name:'Dates', text:'du 1er au 31 mars 2026',
|
|
x:0, y:552, w:W, h:36, size:16, weight:'400', color:'#FADBD8', align:'center', spacing:2}),
|
|
|
|
text({...F, name:'Chaussures', text:'👟 👠 👞',
|
|
x:0, y:598, w:W, h:155, size:90, align:'center'}),
|
|
|
|
text({...F, name:'Site web', text:'www.votre-boutique.fr',
|
|
x:0, y:788, w:W, h:30, size:14, weight:'400', color:'#F8C471', align:'center', spacing:3}),
|
|
|
|
text({...F, name:'Etoiles', text:'★ ★ ★ ★ ★',
|
|
x:0, y:838, w:W, h:36, size:20, weight:'400', color:'#F1C40F', align:'center'}),
|
|
];
|
|
|
|
// 5. Envoyer shape par shape (revn incrémental)
|
|
let currentRevn = revn;
|
|
let ok = 0;
|
|
for (const change of changes) {
|
|
const sid = randomUUID();
|
|
const result = await rpc('update-file', {
|
|
id: FILE_ID, sessionId: sid, revn: currentRevn, vern: 0, changes: [change]
|
|
});
|
|
if (result?.type === 'error' || result?.code) {
|
|
console.error(`❌ "${change.obj?.name}":`, (result.explain || result.message || '').slice(0,120));
|
|
} else {
|
|
currentRevn = result?.revn ?? (currentRevn + 1);
|
|
ok++;
|
|
process.stdout.write(` ✓ ${change.obj?.name || change.type}\n`);
|
|
}
|
|
}
|
|
console.log(`\n✅ ${ok}/${changes.length} shapes créés`);
|
|
console.log(`🔗 http://192.168.1.150:9001 → Drafts → "Affiche SOLDES Direct"`);
|