@olonjs/cli 3.0.82 → 3.0.84
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.
|
@@ -596,7 +596,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
596
596
|
"@tiptap/extension-link": "^2.11.5",
|
|
597
597
|
"@tiptap/react": "^2.11.5",
|
|
598
598
|
"@tiptap/starter-kit": "^2.11.5",
|
|
599
|
-
"@olonjs/core": "^1.0.
|
|
599
|
+
"@olonjs/core": "^1.0.72",
|
|
600
600
|
"clsx": "^2.1.1",
|
|
601
601
|
"lucide-react": "^0.474.0",
|
|
602
602
|
"react": "^19.0.0",
|
|
@@ -941,6 +941,7 @@ import themeData from '@/data/config/theme.json';
|
|
|
941
941
|
import menuData from '@/data/config/menu.json';
|
|
942
942
|
import { getFilePages } from '@/lib/getFilePages';
|
|
943
943
|
import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
|
|
944
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
944
945
|
|
|
945
946
|
import tenantCss from './index.css?inline';
|
|
946
947
|
|
|
@@ -1194,6 +1195,16 @@ function cloudFingerprint(apiBase: string, apiKey: string): string {
|
|
|
1194
1195
|
return `${normalizeApiBase(apiBase)}::${apiKey.slice(-8)}`;
|
|
1195
1196
|
}
|
|
1196
1197
|
|
|
1198
|
+
function normalizeSlugForCache(slug: string): string {
|
|
1199
|
+
return (
|
|
1200
|
+
slug
|
|
1201
|
+
.trim()
|
|
1202
|
+
.toLowerCase()
|
|
1203
|
+
.replace(/[^a-z0-9/_-]/g, '-')
|
|
1204
|
+
.replace(/^\/+|\/+$/g, '') || 'home'
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1197
1208
|
function readCachedCloudContent(fingerprint: string): CachedCloudContent | null {
|
|
1198
1209
|
try {
|
|
1199
1210
|
const raw = localStorage.getItem(CLOUD_CACHE_KEY);
|
|
@@ -1248,7 +1259,6 @@ function App() {
|
|
|
1248
1259
|
const activeCloudSaveController = useRef<AbortController | null>(null);
|
|
1249
1260
|
const contentLoadInFlight = useRef<Promise<void> | null>(null);
|
|
1250
1261
|
const pendingCloudSave = useRef<{ state: ProjectState; slug: string } | null>(null);
|
|
1251
|
-
const warmBootFromCloudCache = useRef(false);
|
|
1252
1262
|
const cloudApiCandidates = useMemo(
|
|
1253
1263
|
() => (isCloudMode && CLOUD_API_URL ? buildApiCandidates(CLOUD_API_URL) : []),
|
|
1254
1264
|
[isCloudMode, CLOUD_API_URL]
|
|
@@ -1288,26 +1298,16 @@ function App() {
|
|
|
1288
1298
|
const primaryApiBase = cloudApiCandidates[0] ?? normalizeApiBase(CLOUD_API_URL);
|
|
1289
1299
|
const fingerprint = cloudFingerprint(primaryApiBase, CLOUD_API_KEY);
|
|
1290
1300
|
const cached = readCachedCloudContent(fingerprint);
|
|
1301
|
+
const cachedPages = cached ? toPagesRecord(cached.pages) : null;
|
|
1302
|
+
const cachedSite = cached ? coerceSiteConfig(cached.siteConfig) : null;
|
|
1303
|
+
const hasCachedFallback = Boolean((cachedPages && Object.keys(cachedPages).length > 0) || cachedSite);
|
|
1291
1304
|
if (cached) {
|
|
1292
|
-
const cachedPages = toPagesRecord(cached.pages);
|
|
1293
|
-
const cachedSite = coerceSiteConfig(cached.siteConfig);
|
|
1294
|
-
if (cachedPages && Object.keys(cachedPages).length > 0) {
|
|
1295
|
-
setPages(cachedPages);
|
|
1296
|
-
}
|
|
1297
|
-
if (cachedSite) {
|
|
1298
|
-
setSiteConfig(cachedSite);
|
|
1299
|
-
}
|
|
1300
|
-
setContentMode('cloud');
|
|
1301
|
-
setContentFallback(null);
|
|
1302
|
-
warmBootFromCloudCache.current = true;
|
|
1303
|
-
setShowTopProgress(false);
|
|
1304
|
-
setHasInitialCloudResolved(true);
|
|
1305
1305
|
logBootstrapEvent('boot.cloud.cache_hit', { ageMs: Date.now() - cached.savedAt });
|
|
1306
|
-
} else {
|
|
1307
|
-
warmBootFromCloudCache.current = false;
|
|
1308
|
-
setShowTopProgress(true);
|
|
1309
|
-
setHasInitialCloudResolved(false);
|
|
1310
1306
|
}
|
|
1307
|
+
setContentMode('cloud');
|
|
1308
|
+
setContentFallback(null);
|
|
1309
|
+
setShowTopProgress(true);
|
|
1310
|
+
setHasInitialCloudResolved(false);
|
|
1311
1311
|
logBootstrapEvent('boot.start', { mode: 'cloud', apiCandidates: cloudApiCandidates.length });
|
|
1312
1312
|
|
|
1313
1313
|
const loadCloudContent = async () => {
|
|
@@ -1320,6 +1320,7 @@ function App() {
|
|
|
1320
1320
|
try {
|
|
1321
1321
|
const res = await fetch(`${apiBase}/content`, {
|
|
1322
1322
|
method: 'GET',
|
|
1323
|
+
cache: 'no-store',
|
|
1323
1324
|
headers: {
|
|
1324
1325
|
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
1325
1326
|
},
|
|
@@ -1422,7 +1423,13 @@ function App() {
|
|
|
1422
1423
|
} catch (error: unknown) {
|
|
1423
1424
|
if (controller.signal.aborted) return;
|
|
1424
1425
|
const failure = toCloudLoadFailure(error);
|
|
1425
|
-
if (
|
|
1426
|
+
if (hasCachedFallback) {
|
|
1427
|
+
if (cachedPages && Object.keys(cachedPages).length > 0) {
|
|
1428
|
+
setPages(cachedPages);
|
|
1429
|
+
}
|
|
1430
|
+
if (cachedSite) {
|
|
1431
|
+
setSiteConfig(cachedSite);
|
|
1432
|
+
}
|
|
1426
1433
|
setContentMode('cloud');
|
|
1427
1434
|
setContentFallback({
|
|
1428
1435
|
reasonCode: 'CLOUD_REFRESH_FAILED',
|
|
@@ -1589,14 +1596,26 @@ function App() {
|
|
|
1589
1596
|
},
|
|
1590
1597
|
body: JSON.stringify({
|
|
1591
1598
|
slug,
|
|
1592
|
-
|
|
1593
|
-
|
|
1599
|
+
page: state.page,
|
|
1600
|
+
siteConfig: state.site,
|
|
1594
1601
|
}),
|
|
1595
1602
|
});
|
|
1596
1603
|
const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
|
|
1597
1604
|
if (!res.ok) {
|
|
1598
1605
|
throw new Error(body.error || body.code || `Hot save failed: ${res.status}`);
|
|
1599
1606
|
}
|
|
1607
|
+
const keyFingerprint = cloudFingerprint(apiBase, CLOUD_API_KEY);
|
|
1608
|
+
const normalizedSlug = normalizeSlugForCache(slug);
|
|
1609
|
+
const existing = readCachedCloudContent(keyFingerprint);
|
|
1610
|
+
writeCachedCloudContent({
|
|
1611
|
+
keyFingerprint,
|
|
1612
|
+
savedAt: Date.now(),
|
|
1613
|
+
siteConfig: state.site ?? null,
|
|
1614
|
+
pages: {
|
|
1615
|
+
...(existing?.pages ?? {}),
|
|
1616
|
+
[normalizedSlug]: state.page,
|
|
1617
|
+
},
|
|
1618
|
+
});
|
|
1600
1619
|
},
|
|
1601
1620
|
showLegacySave: !isCloudMode,
|
|
1602
1621
|
showHotSave: isCloudMode,
|
|
@@ -1667,6 +1686,26 @@ function App() {
|
|
|
1667
1686
|
</div>
|
|
1668
1687
|
</>
|
|
1669
1688
|
) : null}
|
|
1689
|
+
{isCloudMode && !hasInitialCloudResolved ? (
|
|
1690
|
+
<div className="fixed inset-0 z-[1290] bg-background/80 backdrop-blur-sm">
|
|
1691
|
+
<div className="mx-auto w-full max-w-[1600px] p-6">
|
|
1692
|
+
<div className="grid gap-4 lg:grid-cols-[1fr_420px]">
|
|
1693
|
+
<div className="space-y-4">
|
|
1694
|
+
<Skeleton className="h-10 w-64" />
|
|
1695
|
+
<Skeleton className="h-[220px] w-full rounded-xl" />
|
|
1696
|
+
<Skeleton className="h-[220px] w-full rounded-xl" />
|
|
1697
|
+
</div>
|
|
1698
|
+
<div className="space-y-3 rounded-xl border border-border/50 bg-card/60 p-4">
|
|
1699
|
+
<Skeleton className="h-8 w-32" />
|
|
1700
|
+
<Skeleton className="h-5 w-full" />
|
|
1701
|
+
<Skeleton className="h-5 w-5/6" />
|
|
1702
|
+
<Skeleton className="h-5 w-4/6" />
|
|
1703
|
+
<Skeleton className="h-24 w-full rounded-lg" />
|
|
1704
|
+
</div>
|
|
1705
|
+
</div>
|
|
1706
|
+
</div>
|
|
1707
|
+
</div>
|
|
1708
|
+
) : null}
|
|
1670
1709
|
{shouldRenderEngine ? <JsonPagesEngine config={config} /> : null}
|
|
1671
1710
|
{isCloudMode && (contentMode === 'error' || contentFallback?.reasonCode === 'CLOUD_REFRESH_FAILED') ? (
|
|
1672
1711
|
<div
|
|
@@ -1741,102 +1780,6 @@ function App() {
|
|
|
1741
1780
|
export default App;
|
|
1742
1781
|
|
|
1743
1782
|
|
|
1744
|
-
END_OF_FILE_CONTENT
|
|
1745
|
-
# SKIP: src/App.tsx:Zone.Identifier is binary and cannot be embedded as text.
|
|
1746
|
-
echo "Creating src/App_.tsx..."
|
|
1747
|
-
cat << 'END_OF_FILE_CONTENT' > "src/App_.tsx"
|
|
1748
|
-
import { useState, useEffect } from 'react';
|
|
1749
|
-
import { JsonPagesEngine } from '@jsonpages/core';
|
|
1750
|
-
import type { LibraryImageEntry } from '@jsonpages/core';
|
|
1751
|
-
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
1752
|
-
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
1753
|
-
import { addSectionConfig } from '@/lib/addSectionConfig';
|
|
1754
|
-
import { getHydratedData } from '@/lib/draftStorage';
|
|
1755
|
-
import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
|
|
1756
|
-
import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
|
|
1757
|
-
|
|
1758
|
-
import siteData from '@/data/config/site.json';
|
|
1759
|
-
import themeData from '@/data/config/theme.json';
|
|
1760
|
-
import menuData from '@/data/config/menu.json';
|
|
1761
|
-
import { getFilePages } from '@/lib/getFilePages';
|
|
1762
|
-
|
|
1763
|
-
import fontsCss from './fonts.css?inline';
|
|
1764
|
-
import tenantCss from './index.css?inline';
|
|
1765
|
-
|
|
1766
|
-
const themeConfig = themeData as unknown as ThemeConfig;
|
|
1767
|
-
const menuConfig = menuData as unknown as MenuConfig;
|
|
1768
|
-
const TENANT_ID = 'alpha';
|
|
1769
|
-
|
|
1770
|
-
const filePages = getFilePages();
|
|
1771
|
-
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
1772
|
-
const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
|
|
1773
|
-
|
|
1774
|
-
function getInitialData() {
|
|
1775
|
-
return getHydratedData(TENANT_ID, filePages, fileSiteConfig);
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
function App() {
|
|
1779
|
-
const [{ pages, siteConfig }] = useState(getInitialData);
|
|
1780
|
-
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
1781
|
-
|
|
1782
|
-
useEffect(() => {
|
|
1783
|
-
fetch('/api/list-assets')
|
|
1784
|
-
.then((r) => (r.ok ? r.json() : []))
|
|
1785
|
-
.then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
|
|
1786
|
-
.catch(() => setAssetsManifest([]));
|
|
1787
|
-
}, []);
|
|
1788
|
-
|
|
1789
|
-
const config: JsonPagesConfig = {
|
|
1790
|
-
tenantId: TENANT_ID,
|
|
1791
|
-
registry: ComponentRegistry as JsonPagesConfig['registry'],
|
|
1792
|
-
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
1793
|
-
pages,
|
|
1794
|
-
siteConfig,
|
|
1795
|
-
themeConfig,
|
|
1796
|
-
menuConfig,
|
|
1797
|
-
themeCss: { tenant: fontsCss + '\n' + tenantCss },
|
|
1798
|
-
addSection: addSectionConfig,
|
|
1799
|
-
persistence: {
|
|
1800
|
-
async saveToFile(state: ProjectState, slug: string): Promise<void> {
|
|
1801
|
-
const res = await fetch('/api/save-to-file', {
|
|
1802
|
-
method: 'POST',
|
|
1803
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1804
|
-
body: JSON.stringify({ projectState: state, slug }),
|
|
1805
|
-
});
|
|
1806
|
-
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
1807
|
-
if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
|
|
1808
|
-
},
|
|
1809
|
-
},
|
|
1810
|
-
assets: {
|
|
1811
|
-
assetsBaseUrl: '/assets',
|
|
1812
|
-
assetsManifest,
|
|
1813
|
-
async onAssetUpload(file: File): Promise<string> {
|
|
1814
|
-
if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
|
|
1815
|
-
if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
|
|
1816
|
-
const base64 = await new Promise<string>((resolve, reject) => {
|
|
1817
|
-
const reader = new FileReader();
|
|
1818
|
-
reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
|
|
1819
|
-
reader.onerror = () => reject(reader.error);
|
|
1820
|
-
reader.readAsDataURL(file);
|
|
1821
|
-
});
|
|
1822
|
-
const res = await fetch('/api/upload-asset', {
|
|
1823
|
-
method: 'POST',
|
|
1824
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1825
|
-
body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
|
|
1826
|
-
});
|
|
1827
|
-
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
|
|
1828
|
-
if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
|
|
1829
|
-
if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
|
|
1830
|
-
return body.url;
|
|
1831
|
-
},
|
|
1832
|
-
},
|
|
1833
|
-
};
|
|
1834
|
-
|
|
1835
|
-
return <JsonPagesEngine config={config} />;
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
export default App;
|
|
1839
|
-
|
|
1840
1783
|
END_OF_FILE_CONTENT
|
|
1841
1784
|
mkdir -p "src/components"
|
|
1842
1785
|
echo "Creating src/components/NotFound.tsx..."
|
|
@@ -7186,6 +7129,26 @@ export { Separator }
|
|
|
7186
7129
|
|
|
7187
7130
|
|
|
7188
7131
|
|
|
7132
|
+
END_OF_FILE_CONTENT
|
|
7133
|
+
echo "Creating src/components/ui/skeleton.tsx..."
|
|
7134
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/skeleton.tsx"
|
|
7135
|
+
import { cn } from '@/lib/utils';
|
|
7136
|
+
import type { HTMLAttributes } from 'react';
|
|
7137
|
+
|
|
7138
|
+
function Skeleton({
|
|
7139
|
+
className,
|
|
7140
|
+
...props
|
|
7141
|
+
}: HTMLAttributes<HTMLDivElement>) {
|
|
7142
|
+
return (
|
|
7143
|
+
<div
|
|
7144
|
+
className={cn('animate-pulse rounded-md bg-muted', className)}
|
|
7145
|
+
{...props}
|
|
7146
|
+
/>
|
|
7147
|
+
);
|
|
7148
|
+
}
|
|
7149
|
+
|
|
7150
|
+
export { Skeleton };
|
|
7151
|
+
|
|
7189
7152
|
END_OF_FILE_CONTENT
|
|
7190
7153
|
echo "Creating src/components/ui/textarea.tsx..."
|
|
7191
7154
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/textarea.tsx"
|
|
@@ -9335,24 +9298,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
9335
9298
|
|
|
9336
9299
|
|
|
9337
9300
|
|
|
9338
|
-
END_OF_FILE_CONTENT
|
|
9339
|
-
echo "Creating src/main_.tsx..."
|
|
9340
|
-
cat << 'END_OF_FILE_CONTENT' > "src/main_.tsx"
|
|
9341
|
-
import '@/types'; // TBP: load type augmentation from capsule-driven types
|
|
9342
|
-
import React from 'react';
|
|
9343
|
-
import ReactDOM from 'react-dom/client';
|
|
9344
|
-
import App from './App';
|
|
9345
|
-
// ... resto del file
|
|
9346
|
-
|
|
9347
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
9348
|
-
<React.StrictMode>
|
|
9349
|
-
<App />
|
|
9350
|
-
</React.StrictMode>
|
|
9351
|
-
);
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
9301
|
END_OF_FILE_CONTENT
|
|
9357
9302
|
# SKIP: src/registry-types.ts is binary and cannot be embedded as text.
|
|
9358
9303
|
mkdir -p "src/server"
|
|
@@ -596,7 +596,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
596
596
|
"@tiptap/extension-link": "^2.11.5",
|
|
597
597
|
"@tiptap/react": "^2.11.5",
|
|
598
598
|
"@tiptap/starter-kit": "^2.11.5",
|
|
599
|
-
"@olonjs/core": "^1.0.
|
|
599
|
+
"@olonjs/core": "^1.0.72",
|
|
600
600
|
"clsx": "^2.1.1",
|
|
601
601
|
"lucide-react": "^0.474.0",
|
|
602
602
|
"react": "^19.0.0",
|
|
@@ -1074,6 +1074,30 @@ function App() {
|
|
|
1074
1074
|
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
1075
1075
|
if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
|
|
1076
1076
|
},
|
|
1077
|
+
async hotSave(state: ProjectState, slug: string): Promise<void> {
|
|
1078
|
+
if (!isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
|
|
1079
|
+
throw new Error('Cloud mode is not configured for hot save.');
|
|
1080
|
+
}
|
|
1081
|
+
const apiBase = CLOUD_API_URL.replace(/\/$/, '');
|
|
1082
|
+
const res = await fetch(`${apiBase}/hotSave`, {
|
|
1083
|
+
method: 'POST',
|
|
1084
|
+
headers: {
|
|
1085
|
+
'Content-Type': 'application/json',
|
|
1086
|
+
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
1087
|
+
},
|
|
1088
|
+
body: JSON.stringify({
|
|
1089
|
+
slug,
|
|
1090
|
+
page: state.page,
|
|
1091
|
+
siteConfig: state.site,
|
|
1092
|
+
}),
|
|
1093
|
+
});
|
|
1094
|
+
const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
|
|
1095
|
+
if (!res.ok) {
|
|
1096
|
+
throw new Error(body.error || body.code || `Hot save failed: ${res.status}`);
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
1099
|
+
showLegacySave: !isCloudMode,
|
|
1100
|
+
showHotSave: isCloudMode,
|
|
1077
1101
|
},
|
|
1078
1102
|
assets: {
|
|
1079
1103
|
assetsBaseUrl: '/assets',
|
|
@@ -1120,264 +1144,6 @@ function App() {
|
|
|
1120
1144
|
|
|
1121
1145
|
export default App;
|
|
1122
1146
|
|
|
1123
|
-
END_OF_FILE_CONTENT
|
|
1124
|
-
echo "Creating src/App_.tsx..."
|
|
1125
|
-
cat << 'END_OF_FILE_CONTENT' > "src/App_.tsx"
|
|
1126
|
-
import { useState, useEffect } from 'react';
|
|
1127
|
-
import { JsonPagesEngine } from '@jsonpages/core';
|
|
1128
|
-
import type { LibraryImageEntry } from '@jsonpages/core';
|
|
1129
|
-
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
1130
|
-
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
1131
|
-
import { addSectionConfig } from '@/lib/addSectionConfig';
|
|
1132
|
-
import { getHydratedData } from '@/lib/draftStorage';
|
|
1133
|
-
import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
|
|
1134
|
-
import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
|
|
1135
|
-
|
|
1136
|
-
import siteData from '@/data/config/site.json';
|
|
1137
|
-
import themeData from '@/data/config/theme.json';
|
|
1138
|
-
import menuData from '@/data/config/menu.json';
|
|
1139
|
-
import { getFilePages } from '@/lib/getFilePages';
|
|
1140
|
-
|
|
1141
|
-
import fontsCss from './fonts.css?inline';
|
|
1142
|
-
import tenantCss from './index.css?inline';
|
|
1143
|
-
|
|
1144
|
-
const themeConfig = themeData as unknown as ThemeConfig;
|
|
1145
|
-
const menuConfig = menuData as unknown as MenuConfig;
|
|
1146
|
-
const TENANT_ID = 'alpha';
|
|
1147
|
-
|
|
1148
|
-
const filePages = getFilePages();
|
|
1149
|
-
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
1150
|
-
const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
|
|
1151
|
-
|
|
1152
|
-
function getInitialData() {
|
|
1153
|
-
return getHydratedData(TENANT_ID, filePages, fileSiteConfig);
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
function App() {
|
|
1157
|
-
const [{ pages, siteConfig }] = useState(getInitialData);
|
|
1158
|
-
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
1159
|
-
|
|
1160
|
-
useEffect(() => {
|
|
1161
|
-
fetch('/api/list-assets')
|
|
1162
|
-
.then((r) => (r.ok ? r.json() : []))
|
|
1163
|
-
.then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
|
|
1164
|
-
.catch(() => setAssetsManifest([]));
|
|
1165
|
-
}, []);
|
|
1166
|
-
|
|
1167
|
-
const config: JsonPagesConfig = {
|
|
1168
|
-
tenantId: TENANT_ID,
|
|
1169
|
-
registry: ComponentRegistry as JsonPagesConfig['registry'],
|
|
1170
|
-
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
1171
|
-
pages,
|
|
1172
|
-
siteConfig,
|
|
1173
|
-
themeConfig,
|
|
1174
|
-
menuConfig,
|
|
1175
|
-
themeCss: { tenant: fontsCss + '\n' + tenantCss },
|
|
1176
|
-
addSection: addSectionConfig,
|
|
1177
|
-
persistence: {
|
|
1178
|
-
async saveToFile(state: ProjectState, slug: string): Promise<void> {
|
|
1179
|
-
const res = await fetch('/api/save-to-file', {
|
|
1180
|
-
method: 'POST',
|
|
1181
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1182
|
-
body: JSON.stringify({ projectState: state, slug }),
|
|
1183
|
-
});
|
|
1184
|
-
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
1185
|
-
if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
|
|
1186
|
-
},
|
|
1187
|
-
},
|
|
1188
|
-
assets: {
|
|
1189
|
-
assetsBaseUrl: '/assets',
|
|
1190
|
-
assetsManifest,
|
|
1191
|
-
async onAssetUpload(file: File): Promise<string> {
|
|
1192
|
-
if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
|
|
1193
|
-
if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
|
|
1194
|
-
const base64 = await new Promise<string>((resolve, reject) => {
|
|
1195
|
-
const reader = new FileReader();
|
|
1196
|
-
reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
|
|
1197
|
-
reader.onerror = () => reject(reader.error);
|
|
1198
|
-
reader.readAsDataURL(file);
|
|
1199
|
-
});
|
|
1200
|
-
const res = await fetch('/api/upload-asset', {
|
|
1201
|
-
method: 'POST',
|
|
1202
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1203
|
-
body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
|
|
1204
|
-
});
|
|
1205
|
-
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
|
|
1206
|
-
if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
|
|
1207
|
-
if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
|
|
1208
|
-
return body.url;
|
|
1209
|
-
},
|
|
1210
|
-
},
|
|
1211
|
-
};
|
|
1212
|
-
|
|
1213
|
-
return <JsonPagesEngine config={config} />;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
export default App;
|
|
1217
|
-
|
|
1218
|
-
END_OF_FILE_CONTENT
|
|
1219
|
-
echo "Creating src/_App.tsx..."
|
|
1220
|
-
cat << 'END_OF_FILE_CONTENT' > "src/_App.tsx"
|
|
1221
|
-
/**
|
|
1222
|
-
* Thin Entry Point (Tenant).
|
|
1223
|
-
* Data from getHydratedData (file-backed or draft); assets from public/assets/images.
|
|
1224
|
-
* Supports Hybrid Persistence: Local Filesystem (Dev) or Cloud Bridge (Prod).
|
|
1225
|
-
*/
|
|
1226
|
-
import { useState, useEffect } from 'react';
|
|
1227
|
-
import { JsonPagesEngine } from '@jsonpages/core';
|
|
1228
|
-
import type { LibraryImageEntry } from '@jsonpages/core';
|
|
1229
|
-
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
1230
|
-
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
1231
|
-
import { addSectionConfig } from '@/lib/addSectionConfig';
|
|
1232
|
-
import { getHydratedData } from '@/lib/draftStorage';
|
|
1233
|
-
import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
|
|
1234
|
-
import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
|
|
1235
|
-
|
|
1236
|
-
import siteData from '@/data/config/site.json';
|
|
1237
|
-
import themeData from '@/data/config/theme.json';
|
|
1238
|
-
import menuData from '@/data/config/menu.json';
|
|
1239
|
-
import { getFilePages } from '@/lib/getFilePages';
|
|
1240
|
-
|
|
1241
|
-
import fontsCss from './fonts.css?inline';
|
|
1242
|
-
import tenantCss from './index.css?inline';
|
|
1243
|
-
|
|
1244
|
-
// Cloud Configuration (Injected by Vercel/Netlify Env Vars)
|
|
1245
|
-
const CLOUD_API_URL = import.meta.env.VITE_JSONPAGES_CLOUD_URL;
|
|
1246
|
-
const CLOUD_API_KEY = import.meta.env.VITE_JSONPAGES_API_KEY;
|
|
1247
|
-
|
|
1248
|
-
const themeConfig = themeData as unknown as ThemeConfig;
|
|
1249
|
-
const menuConfig = menuData as unknown as MenuConfig;
|
|
1250
|
-
const TENANT_ID = 'alpha';
|
|
1251
|
-
|
|
1252
|
-
const filePages = getFilePages();
|
|
1253
|
-
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
1254
|
-
const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
|
|
1255
|
-
|
|
1256
|
-
function getInitialData() {
|
|
1257
|
-
return getHydratedData(TENANT_ID, filePages, fileSiteConfig);
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
function App() {
|
|
1261
|
-
const [{ pages, siteConfig }] = useState(getInitialData);
|
|
1262
|
-
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
1263
|
-
|
|
1264
|
-
useEffect(() => {
|
|
1265
|
-
// In Cloud mode, listing assets might be different or disabled for MVP
|
|
1266
|
-
// For now, we keep the local fetch which will fail gracefully on Vercel (404)
|
|
1267
|
-
fetch('/api/list-assets')
|
|
1268
|
-
.then((r) => (r.ok ? r.json() : []))
|
|
1269
|
-
.then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
|
|
1270
|
-
.catch(() => setAssetsManifest([]));
|
|
1271
|
-
}, []);
|
|
1272
|
-
|
|
1273
|
-
console.log("🔍 DEBUG ENV:", {
|
|
1274
|
-
URL: import.meta.env.VITE_JSONPAGES_CLOUD_URL,
|
|
1275
|
-
KEY: import.meta.env.VITE_JSONPAGES_API_KEY ? "PRESENT" : "MISSING"
|
|
1276
|
-
});
|
|
1277
|
-
const config: JsonPagesConfig = {
|
|
1278
|
-
tenantId: TENANT_ID,
|
|
1279
|
-
registry: ComponentRegistry as JsonPagesConfig['registry'],
|
|
1280
|
-
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
1281
|
-
pages,
|
|
1282
|
-
siteConfig,
|
|
1283
|
-
themeConfig,
|
|
1284
|
-
menuConfig,
|
|
1285
|
-
themeCss: { tenant: fontsCss + '\n' + tenantCss },
|
|
1286
|
-
addSection: addSectionConfig,
|
|
1287
|
-
persistence: {
|
|
1288
|
-
async saveToFile(state: ProjectState, slug: string): Promise<void> {
|
|
1289
|
-
|
|
1290
|
-
// ☁️ SCENARIO A: CLOUD BRIDGE (Production)
|
|
1291
|
-
if (CLOUD_API_URL && CLOUD_API_KEY) {
|
|
1292
|
-
console.log(`☁️ Saving ${slug} via Cloud Bridge...`);
|
|
1293
|
-
|
|
1294
|
-
const res = await fetch(`${CLOUD_API_URL}/save`, {
|
|
1295
|
-
method: 'POST',
|
|
1296
|
-
headers: {
|
|
1297
|
-
'Content-Type': 'application/json',
|
|
1298
|
-
'Authorization': `Bearer ${CLOUD_API_KEY}`
|
|
1299
|
-
},
|
|
1300
|
-
body: JSON.stringify({
|
|
1301
|
-
// Mapping logical slug to physical path in repo
|
|
1302
|
-
path: `src/data/pages/${slug}.json`,
|
|
1303
|
-
// We save the page config specifically
|
|
1304
|
-
content: state.page,
|
|
1305
|
-
message: `Content update for ${slug} via Visual Editor`
|
|
1306
|
-
}),
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
if (!res.ok) {
|
|
1310
|
-
const err = await res.json().catch(() => ({}));
|
|
1311
|
-
throw new Error(err.error || `Cloud save failed: ${res.status}`);
|
|
1312
|
-
}
|
|
1313
|
-
return;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
// 💻 SCENARIO B: LOCAL FILESYSTEM (Development)
|
|
1317
|
-
console.log(`💻 Saving ${slug} to Local Filesystem...`);
|
|
1318
|
-
const res = await fetch('/api/save-to-file', {
|
|
1319
|
-
method: 'POST',
|
|
1320
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1321
|
-
body: JSON.stringify({ projectState: state, slug }),
|
|
1322
|
-
});
|
|
1323
|
-
|
|
1324
|
-
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
1325
|
-
if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
|
|
1326
|
-
},
|
|
1327
|
-
},
|
|
1328
|
-
assets: {
|
|
1329
|
-
assetsBaseUrl: '/assets',
|
|
1330
|
-
assetsManifest,
|
|
1331
|
-
async onAssetUpload(file: File): Promise<string> {
|
|
1332
|
-
// Note: Asset upload in Cloud Mode requires the R2 Bridge (Next Step in Roadmap)
|
|
1333
|
-
// For now, this works in Local Mode.
|
|
1334
|
-
if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
|
|
1335
|
-
if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
|
|
1336
|
-
|
|
1337
|
-
const base64 = await new Promise<string>((resolve, reject) => {
|
|
1338
|
-
const reader = new FileReader();
|
|
1339
|
-
reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
|
|
1340
|
-
reader.onerror = () => reject(reader.error);
|
|
1341
|
-
reader.readAsDataURL(file);
|
|
1342
|
-
});
|
|
1343
|
-
|
|
1344
|
-
const res = await fetch('/api/upload-asset', {
|
|
1345
|
-
method: 'POST',
|
|
1346
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1347
|
-
body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
|
|
1348
|
-
});
|
|
1349
|
-
|
|
1350
|
-
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
|
|
1351
|
-
if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
|
|
1352
|
-
if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
|
|
1353
|
-
return body.url;
|
|
1354
|
-
},
|
|
1355
|
-
},
|
|
1356
|
-
};
|
|
1357
|
-
|
|
1358
|
-
return <JsonPagesEngine config={config} />;
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
export default App;
|
|
1362
|
-
|
|
1363
|
-
END_OF_FILE_CONTENT
|
|
1364
|
-
echo "Creating src/_main.tsx..."
|
|
1365
|
-
cat << 'END_OF_FILE_CONTENT' > "src/_main.tsx"
|
|
1366
|
-
import '@/types'; // TBP: load type augmentation from capsule-driven types
|
|
1367
|
-
import React from 'react';
|
|
1368
|
-
import ReactDOM from 'react-dom/client';
|
|
1369
|
-
import App from './App';
|
|
1370
|
-
// ... resto del file
|
|
1371
|
-
|
|
1372
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
1373
|
-
<React.StrictMode>
|
|
1374
|
-
<App />
|
|
1375
|
-
</React.StrictMode>
|
|
1376
|
-
);
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
1147
|
END_OF_FILE_CONTENT
|
|
1382
1148
|
mkdir -p "src/components"
|
|
1383
1149
|
echo "Creating src/components/NotFound.tsx..."
|
|
@@ -10909,24 +10675,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
10909
10675
|
|
|
10910
10676
|
|
|
10911
10677
|
|
|
10912
|
-
END_OF_FILE_CONTENT
|
|
10913
|
-
echo "Creating src/main_.tsx..."
|
|
10914
|
-
cat << 'END_OF_FILE_CONTENT' > "src/main_.tsx"
|
|
10915
|
-
import '@/types'; // TBP: load type augmentation from capsule-driven types
|
|
10916
|
-
import React from 'react';
|
|
10917
|
-
import ReactDOM from 'react-dom/client';
|
|
10918
|
-
import App from './App';
|
|
10919
|
-
// ... resto del file
|
|
10920
|
-
|
|
10921
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
10922
|
-
<React.StrictMode>
|
|
10923
|
-
<App />
|
|
10924
|
-
</React.StrictMode>
|
|
10925
|
-
);
|
|
10926
|
-
|
|
10927
|
-
|
|
10928
|
-
|
|
10929
|
-
|
|
10930
10678
|
END_OF_FILE_CONTENT
|
|
10931
10679
|
# SKIP: src/registry-types.ts is binary and cannot be embedded as text.
|
|
10932
10680
|
mkdir -p "src/server"
|
|
@@ -596,7 +596,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
596
596
|
"@tiptap/extension-link": "^2.11.5",
|
|
597
597
|
"@tiptap/react": "^2.11.5",
|
|
598
598
|
"@tiptap/starter-kit": "^2.11.5",
|
|
599
|
-
"@olonjs/core": "^1.0.
|
|
599
|
+
"@olonjs/core": "^1.0.72",
|
|
600
600
|
"clsx": "^2.1.1",
|
|
601
601
|
"lucide-react": "^0.474.0",
|
|
602
602
|
"react": "^19.0.0",
|
|
@@ -941,6 +941,7 @@ import themeData from '@/data/config/theme.json';
|
|
|
941
941
|
import menuData from '@/data/config/menu.json';
|
|
942
942
|
import { getFilePages } from '@/lib/getFilePages';
|
|
943
943
|
import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
|
|
944
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
944
945
|
|
|
945
946
|
import tenantCss from './index.css?inline';
|
|
946
947
|
|
|
@@ -1194,6 +1195,16 @@ function cloudFingerprint(apiBase: string, apiKey: string): string {
|
|
|
1194
1195
|
return `${normalizeApiBase(apiBase)}::${apiKey.slice(-8)}`;
|
|
1195
1196
|
}
|
|
1196
1197
|
|
|
1198
|
+
function normalizeSlugForCache(slug: string): string {
|
|
1199
|
+
return (
|
|
1200
|
+
slug
|
|
1201
|
+
.trim()
|
|
1202
|
+
.toLowerCase()
|
|
1203
|
+
.replace(/[^a-z0-9/_-]/g, '-')
|
|
1204
|
+
.replace(/^\/+|\/+$/g, '') || 'home'
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1197
1208
|
function readCachedCloudContent(fingerprint: string): CachedCloudContent | null {
|
|
1198
1209
|
try {
|
|
1199
1210
|
const raw = localStorage.getItem(CLOUD_CACHE_KEY);
|
|
@@ -1248,7 +1259,6 @@ function App() {
|
|
|
1248
1259
|
const activeCloudSaveController = useRef<AbortController | null>(null);
|
|
1249
1260
|
const contentLoadInFlight = useRef<Promise<void> | null>(null);
|
|
1250
1261
|
const pendingCloudSave = useRef<{ state: ProjectState; slug: string } | null>(null);
|
|
1251
|
-
const warmBootFromCloudCache = useRef(false);
|
|
1252
1262
|
const cloudApiCandidates = useMemo(
|
|
1253
1263
|
() => (isCloudMode && CLOUD_API_URL ? buildApiCandidates(CLOUD_API_URL) : []),
|
|
1254
1264
|
[isCloudMode, CLOUD_API_URL]
|
|
@@ -1288,26 +1298,16 @@ function App() {
|
|
|
1288
1298
|
const primaryApiBase = cloudApiCandidates[0] ?? normalizeApiBase(CLOUD_API_URL);
|
|
1289
1299
|
const fingerprint = cloudFingerprint(primaryApiBase, CLOUD_API_KEY);
|
|
1290
1300
|
const cached = readCachedCloudContent(fingerprint);
|
|
1301
|
+
const cachedPages = cached ? toPagesRecord(cached.pages) : null;
|
|
1302
|
+
const cachedSite = cached ? coerceSiteConfig(cached.siteConfig) : null;
|
|
1303
|
+
const hasCachedFallback = Boolean((cachedPages && Object.keys(cachedPages).length > 0) || cachedSite);
|
|
1291
1304
|
if (cached) {
|
|
1292
|
-
const cachedPages = toPagesRecord(cached.pages);
|
|
1293
|
-
const cachedSite = coerceSiteConfig(cached.siteConfig);
|
|
1294
|
-
if (cachedPages && Object.keys(cachedPages).length > 0) {
|
|
1295
|
-
setPages(cachedPages);
|
|
1296
|
-
}
|
|
1297
|
-
if (cachedSite) {
|
|
1298
|
-
setSiteConfig(cachedSite);
|
|
1299
|
-
}
|
|
1300
|
-
setContentMode('cloud');
|
|
1301
|
-
setContentFallback(null);
|
|
1302
|
-
warmBootFromCloudCache.current = true;
|
|
1303
|
-
setShowTopProgress(false);
|
|
1304
|
-
setHasInitialCloudResolved(true);
|
|
1305
1305
|
logBootstrapEvent('boot.cloud.cache_hit', { ageMs: Date.now() - cached.savedAt });
|
|
1306
|
-
} else {
|
|
1307
|
-
warmBootFromCloudCache.current = false;
|
|
1308
|
-
setShowTopProgress(true);
|
|
1309
|
-
setHasInitialCloudResolved(false);
|
|
1310
1306
|
}
|
|
1307
|
+
setContentMode('cloud');
|
|
1308
|
+
setContentFallback(null);
|
|
1309
|
+
setShowTopProgress(true);
|
|
1310
|
+
setHasInitialCloudResolved(false);
|
|
1311
1311
|
logBootstrapEvent('boot.start', { mode: 'cloud', apiCandidates: cloudApiCandidates.length });
|
|
1312
1312
|
|
|
1313
1313
|
const loadCloudContent = async () => {
|
|
@@ -1320,6 +1320,7 @@ function App() {
|
|
|
1320
1320
|
try {
|
|
1321
1321
|
const res = await fetch(`${apiBase}/content`, {
|
|
1322
1322
|
method: 'GET',
|
|
1323
|
+
cache: 'no-store',
|
|
1323
1324
|
headers: {
|
|
1324
1325
|
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
1325
1326
|
},
|
|
@@ -1422,7 +1423,13 @@ function App() {
|
|
|
1422
1423
|
} catch (error: unknown) {
|
|
1423
1424
|
if (controller.signal.aborted) return;
|
|
1424
1425
|
const failure = toCloudLoadFailure(error);
|
|
1425
|
-
if (
|
|
1426
|
+
if (hasCachedFallback) {
|
|
1427
|
+
if (cachedPages && Object.keys(cachedPages).length > 0) {
|
|
1428
|
+
setPages(cachedPages);
|
|
1429
|
+
}
|
|
1430
|
+
if (cachedSite) {
|
|
1431
|
+
setSiteConfig(cachedSite);
|
|
1432
|
+
}
|
|
1426
1433
|
setContentMode('cloud');
|
|
1427
1434
|
setContentFallback({
|
|
1428
1435
|
reasonCode: 'CLOUD_REFRESH_FAILED',
|
|
@@ -1589,14 +1596,26 @@ function App() {
|
|
|
1589
1596
|
},
|
|
1590
1597
|
body: JSON.stringify({
|
|
1591
1598
|
slug,
|
|
1592
|
-
|
|
1593
|
-
|
|
1599
|
+
page: state.page,
|
|
1600
|
+
siteConfig: state.site,
|
|
1594
1601
|
}),
|
|
1595
1602
|
});
|
|
1596
1603
|
const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
|
|
1597
1604
|
if (!res.ok) {
|
|
1598
1605
|
throw new Error(body.error || body.code || `Hot save failed: ${res.status}`);
|
|
1599
1606
|
}
|
|
1607
|
+
const keyFingerprint = cloudFingerprint(apiBase, CLOUD_API_KEY);
|
|
1608
|
+
const normalizedSlug = normalizeSlugForCache(slug);
|
|
1609
|
+
const existing = readCachedCloudContent(keyFingerprint);
|
|
1610
|
+
writeCachedCloudContent({
|
|
1611
|
+
keyFingerprint,
|
|
1612
|
+
savedAt: Date.now(),
|
|
1613
|
+
siteConfig: state.site ?? null,
|
|
1614
|
+
pages: {
|
|
1615
|
+
...(existing?.pages ?? {}),
|
|
1616
|
+
[normalizedSlug]: state.page,
|
|
1617
|
+
},
|
|
1618
|
+
});
|
|
1600
1619
|
},
|
|
1601
1620
|
showLegacySave: !isCloudMode,
|
|
1602
1621
|
showHotSave: isCloudMode,
|
|
@@ -1667,6 +1686,26 @@ function App() {
|
|
|
1667
1686
|
</div>
|
|
1668
1687
|
</>
|
|
1669
1688
|
) : null}
|
|
1689
|
+
{isCloudMode && !hasInitialCloudResolved ? (
|
|
1690
|
+
<div className="fixed inset-0 z-[1290] bg-background/80 backdrop-blur-sm">
|
|
1691
|
+
<div className="mx-auto w-full max-w-[1600px] p-6">
|
|
1692
|
+
<div className="grid gap-4 lg:grid-cols-[1fr_420px]">
|
|
1693
|
+
<div className="space-y-4">
|
|
1694
|
+
<Skeleton className="h-10 w-64" />
|
|
1695
|
+
<Skeleton className="h-[220px] w-full rounded-xl" />
|
|
1696
|
+
<Skeleton className="h-[220px] w-full rounded-xl" />
|
|
1697
|
+
</div>
|
|
1698
|
+
<div className="space-y-3 rounded-xl border border-border/50 bg-card/60 p-4">
|
|
1699
|
+
<Skeleton className="h-8 w-32" />
|
|
1700
|
+
<Skeleton className="h-5 w-full" />
|
|
1701
|
+
<Skeleton className="h-5 w-5/6" />
|
|
1702
|
+
<Skeleton className="h-5 w-4/6" />
|
|
1703
|
+
<Skeleton className="h-24 w-full rounded-lg" />
|
|
1704
|
+
</div>
|
|
1705
|
+
</div>
|
|
1706
|
+
</div>
|
|
1707
|
+
</div>
|
|
1708
|
+
) : null}
|
|
1670
1709
|
{shouldRenderEngine ? <JsonPagesEngine config={config} /> : null}
|
|
1671
1710
|
{isCloudMode && (contentMode === 'error' || contentFallback?.reasonCode === 'CLOUD_REFRESH_FAILED') ? (
|
|
1672
1711
|
<div
|
|
@@ -1741,102 +1780,6 @@ function App() {
|
|
|
1741
1780
|
export default App;
|
|
1742
1781
|
|
|
1743
1782
|
|
|
1744
|
-
END_OF_FILE_CONTENT
|
|
1745
|
-
# SKIP: src/App.tsx:Zone.Identifier is binary and cannot be embedded as text.
|
|
1746
|
-
echo "Creating src/App_.tsx..."
|
|
1747
|
-
cat << 'END_OF_FILE_CONTENT' > "src/App_.tsx"
|
|
1748
|
-
import { useState, useEffect } from 'react';
|
|
1749
|
-
import { JsonPagesEngine } from '@jsonpages/core';
|
|
1750
|
-
import type { LibraryImageEntry } from '@jsonpages/core';
|
|
1751
|
-
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
1752
|
-
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
1753
|
-
import { addSectionConfig } from '@/lib/addSectionConfig';
|
|
1754
|
-
import { getHydratedData } from '@/lib/draftStorage';
|
|
1755
|
-
import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
|
|
1756
|
-
import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
|
|
1757
|
-
|
|
1758
|
-
import siteData from '@/data/config/site.json';
|
|
1759
|
-
import themeData from '@/data/config/theme.json';
|
|
1760
|
-
import menuData from '@/data/config/menu.json';
|
|
1761
|
-
import { getFilePages } from '@/lib/getFilePages';
|
|
1762
|
-
|
|
1763
|
-
import fontsCss from './fonts.css?inline';
|
|
1764
|
-
import tenantCss from './index.css?inline';
|
|
1765
|
-
|
|
1766
|
-
const themeConfig = themeData as unknown as ThemeConfig;
|
|
1767
|
-
const menuConfig = menuData as unknown as MenuConfig;
|
|
1768
|
-
const TENANT_ID = 'alpha';
|
|
1769
|
-
|
|
1770
|
-
const filePages = getFilePages();
|
|
1771
|
-
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
1772
|
-
const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
|
|
1773
|
-
|
|
1774
|
-
function getInitialData() {
|
|
1775
|
-
return getHydratedData(TENANT_ID, filePages, fileSiteConfig);
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
function App() {
|
|
1779
|
-
const [{ pages, siteConfig }] = useState(getInitialData);
|
|
1780
|
-
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
1781
|
-
|
|
1782
|
-
useEffect(() => {
|
|
1783
|
-
fetch('/api/list-assets')
|
|
1784
|
-
.then((r) => (r.ok ? r.json() : []))
|
|
1785
|
-
.then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
|
|
1786
|
-
.catch(() => setAssetsManifest([]));
|
|
1787
|
-
}, []);
|
|
1788
|
-
|
|
1789
|
-
const config: JsonPagesConfig = {
|
|
1790
|
-
tenantId: TENANT_ID,
|
|
1791
|
-
registry: ComponentRegistry as JsonPagesConfig['registry'],
|
|
1792
|
-
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
1793
|
-
pages,
|
|
1794
|
-
siteConfig,
|
|
1795
|
-
themeConfig,
|
|
1796
|
-
menuConfig,
|
|
1797
|
-
themeCss: { tenant: fontsCss + '\n' + tenantCss },
|
|
1798
|
-
addSection: addSectionConfig,
|
|
1799
|
-
persistence: {
|
|
1800
|
-
async saveToFile(state: ProjectState, slug: string): Promise<void> {
|
|
1801
|
-
const res = await fetch('/api/save-to-file', {
|
|
1802
|
-
method: 'POST',
|
|
1803
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1804
|
-
body: JSON.stringify({ projectState: state, slug }),
|
|
1805
|
-
});
|
|
1806
|
-
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
1807
|
-
if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
|
|
1808
|
-
},
|
|
1809
|
-
},
|
|
1810
|
-
assets: {
|
|
1811
|
-
assetsBaseUrl: '/assets',
|
|
1812
|
-
assetsManifest,
|
|
1813
|
-
async onAssetUpload(file: File): Promise<string> {
|
|
1814
|
-
if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
|
|
1815
|
-
if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
|
|
1816
|
-
const base64 = await new Promise<string>((resolve, reject) => {
|
|
1817
|
-
const reader = new FileReader();
|
|
1818
|
-
reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
|
|
1819
|
-
reader.onerror = () => reject(reader.error);
|
|
1820
|
-
reader.readAsDataURL(file);
|
|
1821
|
-
});
|
|
1822
|
-
const res = await fetch('/api/upload-asset', {
|
|
1823
|
-
method: 'POST',
|
|
1824
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1825
|
-
body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
|
|
1826
|
-
});
|
|
1827
|
-
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
|
|
1828
|
-
if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
|
|
1829
|
-
if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
|
|
1830
|
-
return body.url;
|
|
1831
|
-
},
|
|
1832
|
-
},
|
|
1833
|
-
};
|
|
1834
|
-
|
|
1835
|
-
return <JsonPagesEngine config={config} />;
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
export default App;
|
|
1839
|
-
|
|
1840
1783
|
END_OF_FILE_CONTENT
|
|
1841
1784
|
mkdir -p "src/components"
|
|
1842
1785
|
echo "Creating src/components/NotFound.tsx..."
|
|
@@ -7186,6 +7129,26 @@ export { Separator }
|
|
|
7186
7129
|
|
|
7187
7130
|
|
|
7188
7131
|
|
|
7132
|
+
END_OF_FILE_CONTENT
|
|
7133
|
+
echo "Creating src/components/ui/skeleton.tsx..."
|
|
7134
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/skeleton.tsx"
|
|
7135
|
+
import { cn } from '@/lib/utils';
|
|
7136
|
+
import type { HTMLAttributes } from 'react';
|
|
7137
|
+
|
|
7138
|
+
function Skeleton({
|
|
7139
|
+
className,
|
|
7140
|
+
...props
|
|
7141
|
+
}: HTMLAttributes<HTMLDivElement>) {
|
|
7142
|
+
return (
|
|
7143
|
+
<div
|
|
7144
|
+
className={cn('animate-pulse rounded-md bg-muted', className)}
|
|
7145
|
+
{...props}
|
|
7146
|
+
/>
|
|
7147
|
+
);
|
|
7148
|
+
}
|
|
7149
|
+
|
|
7150
|
+
export { Skeleton };
|
|
7151
|
+
|
|
7189
7152
|
END_OF_FILE_CONTENT
|
|
7190
7153
|
echo "Creating src/components/ui/textarea.tsx..."
|
|
7191
7154
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/textarea.tsx"
|
|
@@ -9335,24 +9298,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
9335
9298
|
|
|
9336
9299
|
|
|
9337
9300
|
|
|
9338
|
-
END_OF_FILE_CONTENT
|
|
9339
|
-
echo "Creating src/main_.tsx..."
|
|
9340
|
-
cat << 'END_OF_FILE_CONTENT' > "src/main_.tsx"
|
|
9341
|
-
import '@/types'; // TBP: load type augmentation from capsule-driven types
|
|
9342
|
-
import React from 'react';
|
|
9343
|
-
import ReactDOM from 'react-dom/client';
|
|
9344
|
-
import App from './App';
|
|
9345
|
-
// ... resto del file
|
|
9346
|
-
|
|
9347
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
9348
|
-
<React.StrictMode>
|
|
9349
|
-
<App />
|
|
9350
|
-
</React.StrictMode>
|
|
9351
|
-
);
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
9301
|
END_OF_FILE_CONTENT
|
|
9357
9302
|
# SKIP: src/registry-types.ts is binary and cannot be embedded as text.
|
|
9358
9303
|
mkdir -p "src/server"
|