@jsonpages/cli 3.0.45 → 3.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/src_tenant_alpha.sh +292 -28
- package/package.json +1 -1
|
@@ -41,6 +41,7 @@ import { addSectionConfig } from '@/lib/addSectionConfig';
|
|
|
41
41
|
import { getHydratedData } from '@/lib/draftStorage';
|
|
42
42
|
import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
|
|
43
43
|
import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
|
|
44
|
+
import { SaveDrawer } from '@/components/SaveDrawer';
|
|
44
45
|
|
|
45
46
|
import siteData from '@/data/config/site.json';
|
|
46
47
|
import themeData from '@/data/config/theme.json';
|
|
@@ -69,6 +70,7 @@ function getInitialData() {
|
|
|
69
70
|
function App() {
|
|
70
71
|
const [{ pages, siteConfig }] = useState(getInitialData);
|
|
71
72
|
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
73
|
+
const [saveDrawer, setSaveDrawer] = useState<{ state: ProjectState; slug: string } | null>(null);
|
|
72
74
|
|
|
73
75
|
useEffect(() => {
|
|
74
76
|
// In Cloud mode, listing assets might be different or disabled for MVP
|
|
@@ -78,11 +80,7 @@ function App() {
|
|
|
78
80
|
.then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
|
|
79
81
|
.catch(() => setAssetsManifest([]));
|
|
80
82
|
}, []);
|
|
81
|
-
|
|
82
|
-
console.log("🔍 DEBUG ENV:", {
|
|
83
|
-
URL: import.meta.env.VITE_JSONPAGES_CLOUD_URL,
|
|
84
|
-
KEY: import.meta.env.VITE_JSONPAGES_API_KEY ? "PRESENT" : "MISSING"
|
|
85
|
-
});
|
|
83
|
+
|
|
86
84
|
const config: JsonPagesConfig = {
|
|
87
85
|
tenantId: TENANT_ID,
|
|
88
86
|
registry: ComponentRegistry as JsonPagesConfig['registry'],
|
|
@@ -96,29 +94,9 @@ console.log("🔍 DEBUG ENV:", {
|
|
|
96
94
|
persistence: {
|
|
97
95
|
async saveToFile(state: ProjectState, slug: string): Promise<void> {
|
|
98
96
|
|
|
99
|
-
// ☁️ SCENARIO A: CLOUD BRIDGE (Production)
|
|
97
|
+
// ☁️ SCENARIO A: CLOUD BRIDGE (Production) — open SaveDrawer, stream save-stream
|
|
100
98
|
if (CLOUD_API_URL && CLOUD_API_KEY) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const res = await fetch(`${CLOUD_API_URL}/save`, {
|
|
104
|
-
method: 'POST',
|
|
105
|
-
headers: {
|
|
106
|
-
'Content-Type': 'application/json',
|
|
107
|
-
'Authorization': `Bearer ${CLOUD_API_KEY}`
|
|
108
|
-
},
|
|
109
|
-
body: JSON.stringify({
|
|
110
|
-
// Mapping logical slug to physical path in repo
|
|
111
|
-
path: `src/data/pages/${slug}.json`,
|
|
112
|
-
// We save the page config specifically
|
|
113
|
-
content: state.page,
|
|
114
|
-
message: `Content update for ${slug} via Visual Editor`
|
|
115
|
-
}),
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
if (!res.ok) {
|
|
119
|
-
const err = await res.json().catch(() => ({}));
|
|
120
|
-
throw new Error(err.error || `Cloud save failed: ${res.status}`);
|
|
121
|
-
}
|
|
99
|
+
setSaveDrawer({ state, slug });
|
|
122
100
|
return;
|
|
123
101
|
}
|
|
124
102
|
|
|
@@ -164,7 +142,20 @@ console.log("🔍 DEBUG ENV:", {
|
|
|
164
142
|
},
|
|
165
143
|
};
|
|
166
144
|
|
|
167
|
-
return
|
|
145
|
+
return (
|
|
146
|
+
<>
|
|
147
|
+
<JsonPagesEngine config={config} />
|
|
148
|
+
{saveDrawer && CLOUD_API_URL && CLOUD_API_KEY && (
|
|
149
|
+
<SaveDrawer
|
|
150
|
+
cloudUrl={CLOUD_API_URL}
|
|
151
|
+
apiKey={CLOUD_API_KEY}
|
|
152
|
+
state={saveDrawer.state}
|
|
153
|
+
slug={saveDrawer.slug}
|
|
154
|
+
onClose={() => setSaveDrawer(null)}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
</>
|
|
158
|
+
);
|
|
168
159
|
}
|
|
169
160
|
|
|
170
161
|
export default App;
|
|
@@ -301,6 +292,279 @@ export const NotFound: React.FC = () => {
|
|
|
301
292
|
|
|
302
293
|
|
|
303
294
|
|
|
295
|
+
END_OF_FILE_CONTENT
|
|
296
|
+
mkdir -p "src/components/SaveDrawer"
|
|
297
|
+
echo "Creating src/components/SaveDrawer/SaveDrawer.tsx..."
|
|
298
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/SaveDrawer.tsx"
|
|
299
|
+
/**
|
|
300
|
+
* SaveDrawer — Cloud save flow with real SSE (commit → push → build → live).
|
|
301
|
+
* Opens when user saves in Cloud mode; consumes POST save-stream and shows
|
|
302
|
+
* the four steps. On done: "Refresh page" + "Close" only (no redirect).
|
|
303
|
+
*/
|
|
304
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
305
|
+
import type { ProjectState } from '@jsonpages/core';
|
|
306
|
+
|
|
307
|
+
const SAVE_STEPS = [
|
|
308
|
+
{ id: 'commit' as const, label: 'Commit' },
|
|
309
|
+
{ id: 'push' as const, label: 'Push' },
|
|
310
|
+
{ id: 'build' as const, label: 'Build' },
|
|
311
|
+
{ id: 'live' as const, label: 'Live' },
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
function parseSSE(buffer: string): { event: string; data: string }[] {
|
|
315
|
+
const lines = buffer.split('\n');
|
|
316
|
+
const out: { event: string; data: string }[] = [];
|
|
317
|
+
let event = 'message';
|
|
318
|
+
let data = '';
|
|
319
|
+
for (const line of lines) {
|
|
320
|
+
if (line.startsWith('event:')) event = line.slice(6).trim();
|
|
321
|
+
else if (line.startsWith('data:')) data = line.slice(5).trim();
|
|
322
|
+
else if (line === '' && data) {
|
|
323
|
+
out.push({ event, data });
|
|
324
|
+
data = '';
|
|
325
|
+
event = 'message';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (data) out.push({ event, data });
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export interface SaveDrawerProps {
|
|
333
|
+
cloudUrl: string;
|
|
334
|
+
apiKey: string;
|
|
335
|
+
state: ProjectState;
|
|
336
|
+
slug: string;
|
|
337
|
+
onClose: () => void;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawerProps) {
|
|
341
|
+
const [completedSteps, setCompletedSteps] = useState<string[]>([]);
|
|
342
|
+
const [activeStepId, setActiveStepId] = useState<string | null>('commit');
|
|
343
|
+
const [logs, setLogs] = useState<Record<string, string[]>>({});
|
|
344
|
+
const [phase, setPhase] = useState<'running' | 'done' | 'error'>('running');
|
|
345
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
346
|
+
|
|
347
|
+
const addLog = useCallback((stepId: string, message: string) => {
|
|
348
|
+
setLogs((prev) => ({ ...prev, [stepId]: [...(prev[stepId] || []), message] }));
|
|
349
|
+
}, []);
|
|
350
|
+
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
let cancelled = false;
|
|
353
|
+
let gotDone = false;
|
|
354
|
+
|
|
355
|
+
const run = async () => {
|
|
356
|
+
const url = `${cloudUrl.replace(/\/$/, '')}/save-stream`;
|
|
357
|
+
const res = await fetch(url, {
|
|
358
|
+
method: 'POST',
|
|
359
|
+
headers: {
|
|
360
|
+
'Content-Type': 'application/json',
|
|
361
|
+
Authorization: `Bearer ${apiKey}`,
|
|
362
|
+
},
|
|
363
|
+
body: JSON.stringify({
|
|
364
|
+
path: `src/data/pages/${slug}.json`,
|
|
365
|
+
content: state.page,
|
|
366
|
+
message: `Content update for ${slug} via Visual Editor`,
|
|
367
|
+
}),
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
if (!res.ok || !res.body) {
|
|
371
|
+
const data = await res.json().catch(() => ({}));
|
|
372
|
+
setErrorMessage((data as { error?: string }).error || `Request failed: ${res.status}`);
|
|
373
|
+
setPhase('error');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const reader = res.body.getReader();
|
|
378
|
+
const dec = new TextDecoder();
|
|
379
|
+
let buffer = '';
|
|
380
|
+
|
|
381
|
+
while (true) {
|
|
382
|
+
const { value, done: streamDone } = await reader.read();
|
|
383
|
+
if (streamDone || cancelled) break;
|
|
384
|
+
buffer += dec.decode(value, { stream: true });
|
|
385
|
+
const parts = buffer.split('\n\n');
|
|
386
|
+
buffer = parts.pop() || '';
|
|
387
|
+
|
|
388
|
+
for (const part of parts) {
|
|
389
|
+
const parsed = parseSSE(part + '\n\n');
|
|
390
|
+
for (const { event: ev, data: raw } of parsed) {
|
|
391
|
+
if (cancelled) return;
|
|
392
|
+
try {
|
|
393
|
+
const data = JSON.parse(raw) as Record<string, unknown>;
|
|
394
|
+
if (ev === 'step') {
|
|
395
|
+
const id = data.id as string | undefined;
|
|
396
|
+
const status = data.status as string | undefined;
|
|
397
|
+
if (id && status === 'running') setActiveStepId(id);
|
|
398
|
+
if (id && status === 'done') {
|
|
399
|
+
setCompletedSteps((prev) => [...prev, id].filter(Boolean));
|
|
400
|
+
setActiveStepId(null);
|
|
401
|
+
}
|
|
402
|
+
} else if (ev === 'log' && data.stepId && data.message) {
|
|
403
|
+
addLog(data.stepId as string, data.message as string);
|
|
404
|
+
} else if (ev === 'error') {
|
|
405
|
+
setErrorMessage((data.message as string) || 'Error');
|
|
406
|
+
setPhase('error');
|
|
407
|
+
return;
|
|
408
|
+
} else if (ev === 'done') {
|
|
409
|
+
gotDone = true;
|
|
410
|
+
setCompletedSteps((prev) => [...new Set([...prev, 'commit', 'push', 'build', 'live'])]);
|
|
411
|
+
setActiveStepId(null);
|
|
412
|
+
setPhase('done');
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
// ignore parse errors
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!cancelled && !gotDone) {
|
|
423
|
+
setErrorMessage('Connection closed before completion');
|
|
424
|
+
setPhase('error');
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
run();
|
|
429
|
+
return () => {
|
|
430
|
+
cancelled = true;
|
|
431
|
+
};
|
|
432
|
+
}, [cloudUrl, apiKey, state.page, slug, addLog]);
|
|
433
|
+
|
|
434
|
+
const progressPct =
|
|
435
|
+
phase === 'done' ? 100 : SAVE_STEPS.length > 0 ? Math.round((completedSteps.length / SAVE_STEPS.length) * 100) : 0;
|
|
436
|
+
|
|
437
|
+
const stepState = (id: string) => {
|
|
438
|
+
if (completedSteps.includes(id)) return 'done';
|
|
439
|
+
if (activeStepId === id) return 'active';
|
|
440
|
+
return 'pending';
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<>
|
|
445
|
+
<div
|
|
446
|
+
className="fixed inset-0 z-40 bg-black/40 backdrop-blur-[2px]"
|
|
447
|
+
onClick={phase === 'done' || phase === 'error' ? onClose : undefined}
|
|
448
|
+
aria-hidden
|
|
449
|
+
/>
|
|
450
|
+
<div
|
|
451
|
+
role="status"
|
|
452
|
+
aria-live="polite"
|
|
453
|
+
className="fixed inset-x-0 bottom-0 z-50 flex justify-center pb-6 px-4"
|
|
454
|
+
aria-label={phase === 'done' ? 'Save completed' : phase === 'error' ? 'Save failed' : 'Saving'}
|
|
455
|
+
>
|
|
456
|
+
<div
|
|
457
|
+
className="relative w-full max-w-2xl overflow-hidden rounded-2xl border border-white/[0.07] bg-[hsl(222,18%,7%)] shadow-lg"
|
|
458
|
+
onClick={(e) => e.stopPropagation()}
|
|
459
|
+
>
|
|
460
|
+
<div className="relative z-10 px-6 pt-6 pb-5">
|
|
461
|
+
<div className="flex items-center justify-between mb-4">
|
|
462
|
+
<h2 className="text-lg font-semibold text-white">
|
|
463
|
+
{phase === 'done' ? 'Content is live' : phase === 'error' ? 'Save failed' : 'Saving to Cloud'}
|
|
464
|
+
</h2>
|
|
465
|
+
{phase === 'running' && (
|
|
466
|
+
<span className="text-xs font-mono text-white/50">{completedSteps.length} / {SAVE_STEPS.length}</span>
|
|
467
|
+
)}
|
|
468
|
+
</div>
|
|
469
|
+
|
|
470
|
+
{phase === 'error' && errorMessage && (
|
|
471
|
+
<p className="text-sm text-red-400 mb-4">{errorMessage}</p>
|
|
472
|
+
)}
|
|
473
|
+
|
|
474
|
+
<div className="flex items-center gap-2 mb-4">
|
|
475
|
+
{SAVE_STEPS.map((step, i) => (
|
|
476
|
+
<div key={step.id} className="flex items-center flex-1">
|
|
477
|
+
<div
|
|
478
|
+
className={`flex items-center justify-center w-8 h-8 rounded-full border-2 shrink-0 ${
|
|
479
|
+
stepState(step.id) === 'done'
|
|
480
|
+
? 'bg-emerald-500/20 border-emerald-500'
|
|
481
|
+
: stepState(step.id) === 'active'
|
|
482
|
+
? 'bg-blue-500/20 border-blue-500'
|
|
483
|
+
: 'bg-white/10 border-white/20'
|
|
484
|
+
}`}
|
|
485
|
+
>
|
|
486
|
+
{stepState(step.id) === 'done' ? (
|
|
487
|
+
<span className="text-emerald-400" aria-hidden>✓</span>
|
|
488
|
+
) : stepState(step.id) === 'active' ? (
|
|
489
|
+
<span className="w-3 h-3 border-2 border-blue-400 border-t-transparent rounded-full animate-spin" aria-hidden />
|
|
490
|
+
) : (
|
|
491
|
+
<span className="text-xs text-white/40">{i + 1}</span>
|
|
492
|
+
)}
|
|
493
|
+
</div>
|
|
494
|
+
<span
|
|
495
|
+
className={`ml-2 text-xs font-medium truncate ${
|
|
496
|
+
stepState(step.id) !== 'pending' ? 'text-white' : 'text-white/40'
|
|
497
|
+
}`}
|
|
498
|
+
>
|
|
499
|
+
{step.label}
|
|
500
|
+
</span>
|
|
501
|
+
{i < SAVE_STEPS.length - 1 && (
|
|
502
|
+
<div
|
|
503
|
+
className={`flex-1 h-0.5 mx-2 min-w-[8px] ${
|
|
504
|
+
stepState(step.id) === 'done' ? 'bg-emerald-500/50' : 'bg-white/10'
|
|
505
|
+
}`}
|
|
506
|
+
/>
|
|
507
|
+
)}
|
|
508
|
+
</div>
|
|
509
|
+
))}
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<div className="h-1 bg-white/10 rounded-full overflow-hidden mb-4">
|
|
513
|
+
<div
|
|
514
|
+
className="h-full bg-emerald-500 rounded-full transition-all duration-300"
|
|
515
|
+
style={{ width: `${progressPct}%` }}
|
|
516
|
+
/>
|
|
517
|
+
</div>
|
|
518
|
+
|
|
519
|
+
{Object.keys(logs).length > 0 && (
|
|
520
|
+
<div className="mb-4 p-2 rounded bg-black/30 border border-white/10 font-mono text-xs text-white/60 space-y-0.5 max-h-20 overflow-y-auto">
|
|
521
|
+
{(logs[activeStepId || ''] || []).slice(-3).map((line, i) => (
|
|
522
|
+
<div key={i}>$ {line}</div>
|
|
523
|
+
))}
|
|
524
|
+
</div>
|
|
525
|
+
)}
|
|
526
|
+
|
|
527
|
+
<div className="flex items-center gap-2 justify-end">
|
|
528
|
+
{phase === 'done' && (
|
|
529
|
+
<>
|
|
530
|
+
<button
|
|
531
|
+
type="button"
|
|
532
|
+
onClick={() => window.location.reload()}
|
|
533
|
+
className="px-4 py-2 rounded-lg bg-emerald-500 text-zinc-900 font-medium text-sm hover:bg-emerald-400"
|
|
534
|
+
>
|
|
535
|
+
Refresh page
|
|
536
|
+
</button>
|
|
537
|
+
<button
|
|
538
|
+
type="button"
|
|
539
|
+
onClick={onClose}
|
|
540
|
+
className="px-4 py-2 rounded-lg border border-white/20 text-white/90 text-sm hover:bg-white/10"
|
|
541
|
+
>
|
|
542
|
+
Close
|
|
543
|
+
</button>
|
|
544
|
+
</>
|
|
545
|
+
)}
|
|
546
|
+
{phase === 'error' && (
|
|
547
|
+
<button
|
|
548
|
+
type="button"
|
|
549
|
+
onClick={onClose}
|
|
550
|
+
className="px-4 py-2 rounded-lg border border-white/20 text-white/90 text-sm hover:bg-white/10"
|
|
551
|
+
>
|
|
552
|
+
Close
|
|
553
|
+
</button>
|
|
554
|
+
)}
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
</>
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
END_OF_FILE_CONTENT
|
|
564
|
+
echo "Creating src/components/SaveDrawer/index.ts..."
|
|
565
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/index.ts"
|
|
566
|
+
export { SaveDrawer } from './SaveDrawer';
|
|
567
|
+
|
|
304
568
|
END_OF_FILE_CONTENT
|
|
305
569
|
mkdir -p "src/components/arch-layers"
|
|
306
570
|
echo "Creating src/components/arch-layers/View.tsx..."
|