@olonjs/cli 3.0.144 → 3.0.146
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 +1481 -43
- package/assets/templates/alpha/src_tenant.sh +1481 -43
- package/package.json +1 -1
|
@@ -1817,7 +1817,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1817
1817
|
"@tiptap/extension-link": "^2.11.5",
|
|
1818
1818
|
"@tiptap/react": "^2.11.5",
|
|
1819
1819
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1820
|
-
"@olonjs/core": "^1.1.
|
|
1820
|
+
"@olonjs/core": "^1.1.12",
|
|
1821
1821
|
"class-variance-authority": "^0.7.1",
|
|
1822
1822
|
"clsx": "^2.1.1",
|
|
1823
1823
|
"lucide-react": "^0.474.0",
|
|
@@ -2131,7 +2131,7 @@ import { build } from 'vite';
|
|
|
2131
2131
|
import path from 'path';
|
|
2132
2132
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
2133
2133
|
import fs from 'fs/promises';
|
|
2134
|
-
import { webmcp } from '@olonjs/core';
|
|
2134
|
+
import { resolvePageMatchFromRegistry, resolvePublicPageDocument, webmcp } from '@olonjs/core';
|
|
2135
2135
|
|
|
2136
2136
|
const {
|
|
2137
2137
|
buildPageContract,
|
|
@@ -2144,6 +2144,7 @@ const {
|
|
|
2144
2144
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
2145
2145
|
const root = path.resolve(__dirname, '..');
|
|
2146
2146
|
const pagesDir = path.resolve(root, 'src/data/pages');
|
|
2147
|
+
const collectionsDir = path.resolve(root, 'src/data/collections');
|
|
2147
2148
|
const publicDir = path.resolve(root, 'public');
|
|
2148
2149
|
const distDir = path.resolve(root, 'dist');
|
|
2149
2150
|
const distSsrDir = path.resolve(root, 'dist-ssr');
|
|
@@ -2184,6 +2185,42 @@ function toCanonicalSlug(relativeJsonPath) {
|
|
|
2184
2185
|
return slug;
|
|
2185
2186
|
}
|
|
2186
2187
|
|
|
2188
|
+
async function readJsonFile(filePath) {
|
|
2189
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
2190
|
+
return JSON.parse(raw);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
async function expandCollectionTarget(slug, pageFilePath) {
|
|
2194
|
+
if (!/\[[^\]]+\]/.test(slug)) return [slug];
|
|
2195
|
+
|
|
2196
|
+
let pageConfig;
|
|
2197
|
+
try {
|
|
2198
|
+
pageConfig = await readJsonFile(pageFilePath);
|
|
2199
|
+
} catch {
|
|
2200
|
+
return [slug];
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
const binding = pageConfig?.collection;
|
|
2204
|
+
if (!binding || typeof binding.source !== 'string' || typeof binding.paramKey !== 'string') {
|
|
2205
|
+
return [slug];
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
const token = `[${binding.paramKey}]`;
|
|
2209
|
+
if (!slug.includes(token)) return [slug];
|
|
2210
|
+
|
|
2211
|
+
const collectionPath = path.resolve(collectionsDir, binding.source, `${binding.source}.json`);
|
|
2212
|
+
let collection;
|
|
2213
|
+
try {
|
|
2214
|
+
collection = await readJsonFile(collectionPath);
|
|
2215
|
+
} catch {
|
|
2216
|
+
return [slug];
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
if (!collection || typeof collection !== 'object' || Array.isArray(collection)) return [slug];
|
|
2220
|
+
const itemIds = Object.keys(collection).sort((a, b) => a.localeCompare(b));
|
|
2221
|
+
return itemIds.length > 0 ? itemIds.map((itemId) => slug.replace(token, itemId)) : [slug];
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2187
2224
|
async function listJsonFilesRecursive(dir) {
|
|
2188
2225
|
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
2189
2226
|
const files = [];
|
|
@@ -2206,7 +2243,14 @@ async function discoverTargets() {
|
|
|
2206
2243
|
files = [];
|
|
2207
2244
|
}
|
|
2208
2245
|
|
|
2209
|
-
const rawSlugs =
|
|
2246
|
+
const rawSlugs = (
|
|
2247
|
+
await Promise.all(
|
|
2248
|
+
files.map(async (fullPath) => {
|
|
2249
|
+
const slug = toCanonicalSlug(path.relative(pagesDir, fullPath));
|
|
2250
|
+
return expandCollectionTarget(slug, fullPath);
|
|
2251
|
+
})
|
|
2252
|
+
)
|
|
2253
|
+
).flat();
|
|
2210
2254
|
const slugs = Array.from(new Set(rawSlugs)).sort((a, b) => a.localeCompare(b));
|
|
2211
2255
|
|
|
2212
2256
|
return slugs.map((slug) => {
|
|
@@ -2264,15 +2308,26 @@ const remoteStylesheetTags = getRemoteStylesheets()
|
|
|
2264
2308
|
const webMcpBuildState = getWebMcpBuildState();
|
|
2265
2309
|
|
|
2266
2310
|
for (const { slug } of targets) {
|
|
2267
|
-
const pageConfig = webMcpBuildState.pages
|
|
2311
|
+
const pageConfig = resolvePageMatchFromRegistry(webMcpBuildState.pages, slug)?.page;
|
|
2268
2312
|
if (!pageConfig) continue;
|
|
2313
|
+
const resolvedPageDocument = resolvePublicPageDocument({
|
|
2314
|
+
slug,
|
|
2315
|
+
pages: webMcpBuildState.pages,
|
|
2316
|
+
siteConfig: webMcpBuildState.siteConfig,
|
|
2317
|
+
themeConfig: webMcpBuildState.themeConfig,
|
|
2318
|
+
menuConfig: webMcpBuildState.menuConfig,
|
|
2319
|
+
collections: webMcpBuildState.collections,
|
|
2320
|
+
collectionSchemas: webMcpBuildState.collectionSchemas,
|
|
2321
|
+
refDocuments: webMcpBuildState.refDocuments,
|
|
2322
|
+
});
|
|
2323
|
+
const publicPageConfig = resolvedPageDocument?.page ?? pageConfig;
|
|
2269
2324
|
|
|
2270
|
-
// Export the
|
|
2271
|
-
await writeJsonTargets(`pages/${slug}.json`,
|
|
2325
|
+
// Export the resolved public JSON data for the agentic web (so readResource matches runtime).
|
|
2326
|
+
await writeJsonTargets(`pages/${slug}.json`, publicPageConfig);
|
|
2272
2327
|
|
|
2273
2328
|
const contract = buildPageContract({
|
|
2274
2329
|
slug,
|
|
2275
|
-
pageConfig,
|
|
2330
|
+
pageConfig: publicPageConfig,
|
|
2276
2331
|
schemas: webMcpBuildState.schemas,
|
|
2277
2332
|
submissionSchemas: webMcpBuildState.submissionSchemas,
|
|
2278
2333
|
siteConfig: webMcpBuildState.siteConfig,
|
|
@@ -2280,7 +2335,7 @@ for (const { slug } of targets) {
|
|
|
2280
2335
|
await writeSsrJson(`schemas/${slug}.schema.json`, contract);
|
|
2281
2336
|
const pageManifest = buildPageManifest({
|
|
2282
2337
|
slug,
|
|
2283
|
-
pageConfig,
|
|
2338
|
+
pageConfig: publicPageConfig,
|
|
2284
2339
|
schemas: webMcpBuildState.schemas,
|
|
2285
2340
|
siteConfig: webMcpBuildState.siteConfig,
|
|
2286
2341
|
});
|
|
@@ -2549,6 +2604,8 @@ const __dirname = path.dirname(__filename);
|
|
|
2549
2604
|
const rootDir = path.resolve(__dirname, '..');
|
|
2550
2605
|
const sourceDir = path.join(rootDir, 'src', 'data', 'pages');
|
|
2551
2606
|
const targetDir = path.join(rootDir, 'public', 'pages');
|
|
2607
|
+
const sourceCollectionsDir = path.join(rootDir, 'src', 'data', 'collections');
|
|
2608
|
+
const targetCollectionsDir = path.join(rootDir, 'public', 'collections');
|
|
2552
2609
|
const sourceSiteConfigPath = path.join(rootDir, 'src', 'data', 'config', 'site.json');
|
|
2553
2610
|
const targetConfigDir = path.join(rootDir, 'public', 'config');
|
|
2554
2611
|
const targetSiteConfigPath = path.join(targetConfigDir, 'site.json');
|
|
@@ -2562,12 +2619,18 @@ fs.rmSync(targetDir, { recursive: true, force: true });
|
|
|
2562
2619
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
2563
2620
|
fs.cpSync(sourceDir, targetDir, { recursive: true });
|
|
2564
2621
|
|
|
2622
|
+
fs.rmSync(targetCollectionsDir, { recursive: true, force: true });
|
|
2623
|
+
if (fs.existsSync(sourceCollectionsDir)) {
|
|
2624
|
+
fs.mkdirSync(targetCollectionsDir, { recursive: true });
|
|
2625
|
+
fs.cpSync(sourceCollectionsDir, targetCollectionsDir, { recursive: true });
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2565
2628
|
if (fs.existsSync(sourceSiteConfigPath)) {
|
|
2566
2629
|
fs.mkdirSync(targetConfigDir, { recursive: true });
|
|
2567
2630
|
fs.cpSync(sourceSiteConfigPath, targetSiteConfigPath);
|
|
2568
2631
|
}
|
|
2569
2632
|
|
|
2570
|
-
console.log('[sync-pages-to-public] Synced pages
|
|
2633
|
+
console.log('[sync-pages-to-public] Synced pages, collections, and site config to public/');
|
|
2571
2634
|
|
|
2572
2635
|
END_OF_FILE_CONTENT
|
|
2573
2636
|
echo "Creating scripts/webmcp-feature-check.mjs..."
|
|
@@ -2885,6 +2948,7 @@ import { JsonPagesEngine } from '@olonjs/core';
|
|
|
2885
2948
|
import type { JsonPagesConfig, LibraryImageEntry, ProjectState } from '@olonjs/core';
|
|
2886
2949
|
import { normalizeBasePath, withBasePath } from '@olonjs/core';
|
|
2887
2950
|
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
2951
|
+
import { CollectionRegistry } from '@/lib/CollectionRegistry';
|
|
2888
2952
|
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
2889
2953
|
import { addSectionConfig } from '@/lib/addSectionConfig';
|
|
2890
2954
|
import { getHydratedData } from '@/lib/draftStorage';
|
|
@@ -2895,6 +2959,7 @@ import { startCloudSaveStream } from '@olonjs/core';
|
|
|
2895
2959
|
import siteData from '@/data/config/site.json';
|
|
2896
2960
|
import themeData from '@/data/config/theme.json';
|
|
2897
2961
|
import menuData from '@/data/config/menu.json';
|
|
2962
|
+
import { getFileCollections } from '@/lib/getFileCollections';
|
|
2898
2963
|
import { getFilePages } from '@/lib/getFilePages';
|
|
2899
2964
|
import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
|
|
2900
2965
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
@@ -2926,6 +2991,7 @@ const TENANT_ID = 'alpha';
|
|
|
2926
2991
|
|
|
2927
2992
|
const filePages = getFilePages();
|
|
2928
2993
|
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
2994
|
+
const collections = getFileCollections();
|
|
2929
2995
|
const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
|
|
2930
2996
|
const ASSET_UPLOAD_MAX_RETRIES = 2;
|
|
2931
2997
|
const ASSET_UPLOAD_TIMEOUT_MS = 20_000;
|
|
@@ -2963,6 +3029,7 @@ type CachedCloudContent = {
|
|
|
2963
3029
|
savedAt: number;
|
|
2964
3030
|
siteConfig: unknown | null;
|
|
2965
3031
|
pages: Record<string, unknown>;
|
|
3032
|
+
collections?: JsonPagesConfig['collections'];
|
|
2966
3033
|
};
|
|
2967
3034
|
|
|
2968
3035
|
const CLOUD_CACHE_KEY = 'jp_cloud_content_cache_v1';
|
|
@@ -3008,7 +3075,7 @@ function asString(value: unknown, fallback: string): string {
|
|
|
3008
3075
|
function normalizeRouteSlug(value: string): string {
|
|
3009
3076
|
return value
|
|
3010
3077
|
.toLowerCase()
|
|
3011
|
-
.replace(/[^a-z0-9/_-]/g, '-')
|
|
3078
|
+
.replace(/[^a-z0-9/_[\]-]/g, '-')
|
|
3012
3079
|
.replace(/^\/+|\/+$/g, '') || 'home';
|
|
3013
3080
|
}
|
|
3014
3081
|
|
|
@@ -3034,6 +3101,7 @@ function coercePageConfig(slug: string, value: unknown): PageConfig | null {
|
|
|
3034
3101
|
slug: normalizedSlug,
|
|
3035
3102
|
meta: { title, description },
|
|
3036
3103
|
sections: input.sections as PageConfig['sections'],
|
|
3104
|
+
...(isObjectRecord(input.collection) ? { collection: input.collection as unknown as PageConfig['collection'] } : {}),
|
|
3037
3105
|
...(typeof input['global-header'] === 'boolean' ? { 'global-header': input['global-header'] } : {}),
|
|
3038
3106
|
};
|
|
3039
3107
|
}
|
|
@@ -3175,7 +3243,7 @@ function normalizeSlugForCache(slug: string): string {
|
|
|
3175
3243
|
slug
|
|
3176
3244
|
.trim()
|
|
3177
3245
|
.toLowerCase()
|
|
3178
|
-
.replace(/[^a-z0-9/_-]/g, '-')
|
|
3246
|
+
.replace(/[^a-z0-9/_[\]-]/g, '-')
|
|
3179
3247
|
.replace(/^\/+|\/+$/g, '') || 'home'
|
|
3180
3248
|
);
|
|
3181
3249
|
}
|
|
@@ -3740,10 +3808,12 @@ function App() {
|
|
|
3740
3808
|
basePath: APP_BASE_PATH,
|
|
3741
3809
|
registry: ComponentRegistry as JsonPagesConfig['registry'],
|
|
3742
3810
|
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
3811
|
+
collectionSchemas: CollectionRegistry as unknown as JsonPagesConfig['collectionSchemas'],
|
|
3743
3812
|
pages,
|
|
3744
3813
|
siteConfig,
|
|
3745
3814
|
themeConfig,
|
|
3746
3815
|
menuConfig,
|
|
3816
|
+
collections,
|
|
3747
3817
|
refDocuments,
|
|
3748
3818
|
themeCss: { tenant: resolvedTenantCss },
|
|
3749
3819
|
iconRegistry: iconMap,
|
|
@@ -3780,6 +3850,7 @@ function App() {
|
|
|
3780
3850
|
slug,
|
|
3781
3851
|
page: state.page,
|
|
3782
3852
|
siteConfig: state.site,
|
|
3853
|
+
collections: state.collections,
|
|
3783
3854
|
}),
|
|
3784
3855
|
});
|
|
3785
3856
|
const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
|
|
@@ -3793,6 +3864,7 @@ function App() {
|
|
|
3793
3864
|
keyFingerprint,
|
|
3794
3865
|
savedAt: Date.now(),
|
|
3795
3866
|
siteConfig: state.site ?? null,
|
|
3867
|
+
collections: state.collections,
|
|
3796
3868
|
pages: {
|
|
3797
3869
|
...(existing?.pages ?? {}),
|
|
3798
3870
|
[normalizedSlug]: state.page,
|
|
@@ -4039,6 +4111,72 @@ export default App;
|
|
|
4039
4111
|
|
|
4040
4112
|
END_OF_FILE_CONTENT
|
|
4041
4113
|
# SKIP: src/App.tsx:Zone.Identifier is binary and cannot be embedded as text.
|
|
4114
|
+
mkdir -p "src/collections"
|
|
4115
|
+
mkdir -p "src/collections/autori"
|
|
4116
|
+
echo "Creating src/collections/autori/index.ts..."
|
|
4117
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/autori/index.ts"
|
|
4118
|
+
export { AutoreSchema, AutoriCollectionSchema } from './schema';
|
|
4119
|
+
export type { Autore, AutoriCollection } from './types';
|
|
4120
|
+
|
|
4121
|
+
END_OF_FILE_CONTENT
|
|
4122
|
+
echo "Creating src/collections/autori/schema.ts..."
|
|
4123
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/autori/schema.ts"
|
|
4124
|
+
import { z } from 'zod';
|
|
4125
|
+
import { BaseCollectionItem } from '@olonjs/core';
|
|
4126
|
+
|
|
4127
|
+
export const AutoreSchema = BaseCollectionItem.extend({
|
|
4128
|
+
name: z.string().describe('ui:text'),
|
|
4129
|
+
});
|
|
4130
|
+
|
|
4131
|
+
export const AutoriCollectionSchema = z.record(z.string(), AutoreSchema);
|
|
4132
|
+
|
|
4133
|
+
END_OF_FILE_CONTENT
|
|
4134
|
+
echo "Creating src/collections/autori/types.ts..."
|
|
4135
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/autori/types.ts"
|
|
4136
|
+
import { z } from 'zod';
|
|
4137
|
+
import { AutoreSchema, AutoriCollectionSchema } from './schema';
|
|
4138
|
+
|
|
4139
|
+
export type Autore = z.infer<typeof AutoreSchema>;
|
|
4140
|
+
export type AutoriCollection = z.infer<typeof AutoriCollectionSchema>;
|
|
4141
|
+
|
|
4142
|
+
END_OF_FILE_CONTENT
|
|
4143
|
+
mkdir -p "src/collections/libri"
|
|
4144
|
+
echo "Creating src/collections/libri/index.ts..."
|
|
4145
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/libri/index.ts"
|
|
4146
|
+
export { LibroSchema, LibriCollectionSchema } from './schema';
|
|
4147
|
+
export type { Libro, LibriCollection } from './types';
|
|
4148
|
+
|
|
4149
|
+
END_OF_FILE_CONTENT
|
|
4150
|
+
echo "Creating src/collections/libri/schema.ts..."
|
|
4151
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/libri/schema.ts"
|
|
4152
|
+
import { z } from 'zod';
|
|
4153
|
+
import { BaseCollectionItem } from '@olonjs/core';
|
|
4154
|
+
import { AutoreSchema } from '@/collections/autori';
|
|
4155
|
+
|
|
4156
|
+
const CollectionRefSchema = z.object({
|
|
4157
|
+
$ref: z.string(),
|
|
4158
|
+
});
|
|
4159
|
+
|
|
4160
|
+
export const LibroSchema = BaseCollectionItem.extend({
|
|
4161
|
+
title: z.string().describe('ui:text'),
|
|
4162
|
+
author: z.union([AutoreSchema, CollectionRefSchema]).describe('ui:collection-ref:autori'),
|
|
4163
|
+
year: z.number().describe('ui:number'),
|
|
4164
|
+
genre: z.string().describe('ui:text'),
|
|
4165
|
+
summary: z.string().describe('ui:textarea'),
|
|
4166
|
+
});
|
|
4167
|
+
|
|
4168
|
+
export const LibriCollectionSchema = z.record(z.string(), LibroSchema);
|
|
4169
|
+
|
|
4170
|
+
END_OF_FILE_CONTENT
|
|
4171
|
+
echo "Creating src/collections/libri/types.ts..."
|
|
4172
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/libri/types.ts"
|
|
4173
|
+
import { z } from 'zod';
|
|
4174
|
+
import { LibroSchema, LibriCollectionSchema } from './schema';
|
|
4175
|
+
|
|
4176
|
+
export type Libro = z.infer<typeof LibroSchema>;
|
|
4177
|
+
export type LibriCollection = z.infer<typeof LibriCollectionSchema>;
|
|
4178
|
+
|
|
4179
|
+
END_OF_FILE_CONTENT
|
|
4042
4180
|
mkdir -p "src/components"
|
|
4043
4181
|
echo "Creating src/components/ThemeProvider.tsx..."
|
|
4044
4182
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ThemeProvider.tsx"
|
|
@@ -4102,6 +4240,370 @@ export function useTheme() {
|
|
|
4102
4240
|
return useContext(ThemeContext)
|
|
4103
4241
|
}
|
|
4104
4242
|
|
|
4243
|
+
END_OF_FILE_CONTENT
|
|
4244
|
+
mkdir -p "src/components/authors-list"
|
|
4245
|
+
echo "Creating src/components/authors-list/View.tsx..."
|
|
4246
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/authors-list/View.tsx"
|
|
4247
|
+
import { useMemo } from 'react';
|
|
4248
|
+
import type { Autore } from '@/collections/autori';
|
|
4249
|
+
import type { AuthorsListData } from './types';
|
|
4250
|
+
|
|
4251
|
+
type AuthorsListViewProps = {
|
|
4252
|
+
data: AuthorsListData;
|
|
4253
|
+
};
|
|
4254
|
+
|
|
4255
|
+
function toAuthors(items: AuthorsListData['items']): Autore[] {
|
|
4256
|
+
return Object.values(items ?? {}).sort((a, b) => a.name.localeCompare(b.name));
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
export function AuthorsListView({ data }: AuthorsListViewProps) {
|
|
4260
|
+
const authors = useMemo(() => toAuthors(data.items), [data.items]);
|
|
4261
|
+
|
|
4262
|
+
return (
|
|
4263
|
+
<main className="min-h-screen bg-background px-6 py-16 text-foreground">
|
|
4264
|
+
<section className="mx-auto flex w-full max-w-5xl flex-col gap-10">
|
|
4265
|
+
<div className="max-w-2xl">
|
|
4266
|
+
{data.eyebrow && (
|
|
4267
|
+
<p
|
|
4268
|
+
data-jp-field="eyebrow"
|
|
4269
|
+
className="text-sm font-medium uppercase tracking-[0.18em] text-muted-foreground"
|
|
4270
|
+
>
|
|
4271
|
+
{data.eyebrow}
|
|
4272
|
+
</p>
|
|
4273
|
+
)}
|
|
4274
|
+
<h1
|
|
4275
|
+
data-jp-field="title"
|
|
4276
|
+
className="mt-3 text-4xl font-semibold tracking-tight sm:text-5xl"
|
|
4277
|
+
>
|
|
4278
|
+
{data.title}
|
|
4279
|
+
</h1>
|
|
4280
|
+
{data.description && (
|
|
4281
|
+
<p
|
|
4282
|
+
data-jp-field="description"
|
|
4283
|
+
className="mt-4 text-base leading-7 text-muted-foreground"
|
|
4284
|
+
>
|
|
4285
|
+
{data.description}
|
|
4286
|
+
</p>
|
|
4287
|
+
)}
|
|
4288
|
+
</div>
|
|
4289
|
+
|
|
4290
|
+
<div data-jp-field="items" className="grid gap-4 sm:grid-cols-2">
|
|
4291
|
+
{authors.map((author) => (
|
|
4292
|
+
<a
|
|
4293
|
+
key={author.id}
|
|
4294
|
+
href={`/authors/${encodeURIComponent(author.id)}/libri`}
|
|
4295
|
+
data-jp-item-id={author.id}
|
|
4296
|
+
data-jp-item-field="items"
|
|
4297
|
+
className="block rounded-xl border border-border bg-card p-5 shadow-sm transition-colors hover:bg-muted/40"
|
|
4298
|
+
>
|
|
4299
|
+
<h2 className="text-xl font-semibold">{author.name}</h2>
|
|
4300
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
4301
|
+
Vedi libri di {author.name}
|
|
4302
|
+
</p>
|
|
4303
|
+
</a>
|
|
4304
|
+
))}
|
|
4305
|
+
</div>
|
|
4306
|
+
</section>
|
|
4307
|
+
</main>
|
|
4308
|
+
);
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
END_OF_FILE_CONTENT
|
|
4312
|
+
echo "Creating src/components/authors-list/index.ts..."
|
|
4313
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/authors-list/index.ts"
|
|
4314
|
+
export { AuthorsListView } from './View';
|
|
4315
|
+
export { AuthorsListSchema, AuthorsListSettingsSchema } from './schema';
|
|
4316
|
+
export type { AuthorsListData, AuthorsListSettings } from './types';
|
|
4317
|
+
|
|
4318
|
+
END_OF_FILE_CONTENT
|
|
4319
|
+
echo "Creating src/components/authors-list/schema.ts..."
|
|
4320
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/authors-list/schema.ts"
|
|
4321
|
+
import { z } from 'zod';
|
|
4322
|
+
import { BaseSectionData } from '@olonjs/core';
|
|
4323
|
+
import { AutoreSchema } from '@/collections/autori';
|
|
4324
|
+
|
|
4325
|
+
export const AuthorsListSchema = BaseSectionData.extend({
|
|
4326
|
+
eyebrow: z.string().optional().describe('ui:text'),
|
|
4327
|
+
title: z.string().describe('ui:text'),
|
|
4328
|
+
description: z.string().optional().describe('ui:textarea'),
|
|
4329
|
+
items: z.record(z.string(), AutoreSchema).describe('ui:collection-ref:autori'),
|
|
4330
|
+
});
|
|
4331
|
+
|
|
4332
|
+
export const AuthorsListSettingsSchema = z.object({});
|
|
4333
|
+
|
|
4334
|
+
END_OF_FILE_CONTENT
|
|
4335
|
+
echo "Creating src/components/authors-list/types.ts..."
|
|
4336
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/authors-list/types.ts"
|
|
4337
|
+
import { z } from 'zod';
|
|
4338
|
+
import { AuthorsListSchema, AuthorsListSettingsSchema } from './schema';
|
|
4339
|
+
|
|
4340
|
+
export type AuthorsListData = z.infer<typeof AuthorsListSchema>;
|
|
4341
|
+
export type AuthorsListSettings = z.infer<typeof AuthorsListSettingsSchema>;
|
|
4342
|
+
|
|
4343
|
+
END_OF_FILE_CONTENT
|
|
4344
|
+
mkdir -p "src/components/book-detail"
|
|
4345
|
+
echo "Creating src/components/book-detail/View.tsx..."
|
|
4346
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/View.tsx"
|
|
4347
|
+
import type { BookDetailData } from './types';
|
|
4348
|
+
|
|
4349
|
+
type BookDetailViewProps = {
|
|
4350
|
+
data: BookDetailData;
|
|
4351
|
+
};
|
|
4352
|
+
|
|
4353
|
+
function getAuthorName(author: BookDetailData['item']['author']): string {
|
|
4354
|
+
if (typeof author === 'object' && author !== null && 'name' in author) {
|
|
4355
|
+
return String(author.name);
|
|
4356
|
+
}
|
|
4357
|
+
return 'Autore';
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
export function BookDetailView({ data }: BookDetailViewProps) {
|
|
4361
|
+
const book = data.item;
|
|
4362
|
+
|
|
4363
|
+
return (
|
|
4364
|
+
<main className="min-h-screen bg-background text-foreground px-6 py-16">
|
|
4365
|
+
<article
|
|
4366
|
+
data-jp-field="item"
|
|
4367
|
+
data-jp-item-id={book.id}
|
|
4368
|
+
data-jp-item-field="item"
|
|
4369
|
+
className="mx-auto w-full max-w-3xl rounded-2xl border border-border bg-card p-8 shadow-sm"
|
|
4370
|
+
>
|
|
4371
|
+
<a
|
|
4372
|
+
href="/"
|
|
4373
|
+
className="text-sm font-medium text-muted-foreground hover:text-foreground"
|
|
4374
|
+
>
|
|
4375
|
+
{data.backLabel}
|
|
4376
|
+
</a>
|
|
4377
|
+
<p className="mt-10 text-sm font-medium uppercase tracking-[0.18em] text-muted-foreground">
|
|
4378
|
+
{book.genre} · {book.year}
|
|
4379
|
+
</p>
|
|
4380
|
+
<h1 className="mt-3 text-4xl font-semibold tracking-tight sm:text-5xl">
|
|
4381
|
+
{book.title}
|
|
4382
|
+
</h1>
|
|
4383
|
+
<p className="mt-4 text-lg text-muted-foreground">
|
|
4384
|
+
{getAuthorName(book.author)}
|
|
4385
|
+
</p>
|
|
4386
|
+
<p className="mt-8 text-base leading-8 text-muted-foreground">
|
|
4387
|
+
{book.summary}
|
|
4388
|
+
</p>
|
|
4389
|
+
</article>
|
|
4390
|
+
</main>
|
|
4391
|
+
);
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4394
|
+
END_OF_FILE_CONTENT
|
|
4395
|
+
echo "Creating src/components/book-detail/index.ts..."
|
|
4396
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/index.ts"
|
|
4397
|
+
export * from './View';
|
|
4398
|
+
export * from './schema';
|
|
4399
|
+
export * from './types';
|
|
4400
|
+
|
|
4401
|
+
END_OF_FILE_CONTENT
|
|
4402
|
+
echo "Creating src/components/book-detail/schema.ts..."
|
|
4403
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/schema.ts"
|
|
4404
|
+
import { z } from 'zod';
|
|
4405
|
+
import { BaseSectionData } from '@olonjs/core';
|
|
4406
|
+
import { LibroSchema } from '@/collections/libri';
|
|
4407
|
+
|
|
4408
|
+
export const BookDetailSchema = BaseSectionData.extend({
|
|
4409
|
+
item: LibroSchema.describe('ui:collection-ref'),
|
|
4410
|
+
backLabel: z.string().default('Torna ai libri').describe('ui:text'),
|
|
4411
|
+
});
|
|
4412
|
+
|
|
4413
|
+
export const BookDetailSettingsSchema = z.object({});
|
|
4414
|
+
|
|
4415
|
+
END_OF_FILE_CONTENT
|
|
4416
|
+
echo "Creating src/components/book-detail/types.ts..."
|
|
4417
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/types.ts"
|
|
4418
|
+
import { z } from 'zod';
|
|
4419
|
+
import { BookDetailSchema, BookDetailSettingsSchema } from './schema';
|
|
4420
|
+
|
|
4421
|
+
export type BookDetailData = z.infer<typeof BookDetailSchema>;
|
|
4422
|
+
export type BookDetailSettings = z.infer<typeof BookDetailSettingsSchema>;
|
|
4423
|
+
|
|
4424
|
+
END_OF_FILE_CONTENT
|
|
4425
|
+
mkdir -p "src/components/books-list"
|
|
4426
|
+
echo "Creating src/components/books-list/View.tsx..."
|
|
4427
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/View.tsx"
|
|
4428
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4429
|
+
import { useLocation } from 'react-router-dom';
|
|
4430
|
+
import type { Libro } from '@/collections/libri';
|
|
4431
|
+
import type { BooksListData } from './types';
|
|
4432
|
+
|
|
4433
|
+
type BooksListViewProps = {
|
|
4434
|
+
data: BooksListData;
|
|
4435
|
+
};
|
|
4436
|
+
|
|
4437
|
+
function toBooks(items: BooksListData['items']): Libro[] {
|
|
4438
|
+
return Object.values(items ?? {}).sort((a, b) => a.title.localeCompare(b.title));
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4441
|
+
function getAuthorName(author: Libro['author']): string {
|
|
4442
|
+
if (typeof author === 'object' && author !== null && 'name' in author) {
|
|
4443
|
+
return String(author.name);
|
|
4444
|
+
}
|
|
4445
|
+
return 'Autore';
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
function getAuthorId(author: Libro['author']): string | null {
|
|
4449
|
+
if (typeof author === 'object' && author !== null && 'id' in author && typeof author.id === 'string') {
|
|
4450
|
+
return author.id;
|
|
4451
|
+
}
|
|
4452
|
+
if (typeof author === 'object' && author !== null && '$ref' in author && typeof author.$ref === 'string') {
|
|
4453
|
+
const pointer = author.$ref.split('#')[1]?.replace(/^\//, '') ?? '';
|
|
4454
|
+
return pointer.split('/')[0] || null;
|
|
4455
|
+
}
|
|
4456
|
+
return null;
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
function getAuthorFilterFromPath(pathname: string): string | null {
|
|
4460
|
+
const match = pathname.match(/^\/authors\/([^/]+)\/libri\/?$/);
|
|
4461
|
+
return match?.[1] ? decodeURIComponent(match[1]) : null;
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
export function BooksListView({ data }: BooksListViewProps) {
|
|
4465
|
+
const location = useLocation();
|
|
4466
|
+
const authorFilter = useMemo(() => {
|
|
4467
|
+
const queryAuthor = new URLSearchParams(location.search).get('author');
|
|
4468
|
+
return queryAuthor || getAuthorFilterFromPath(location.pathname);
|
|
4469
|
+
}, [location.pathname, location.search]);
|
|
4470
|
+
const books = useMemo(() => toBooks(data.items), [data.items]);
|
|
4471
|
+
const filteredBooks = useMemo(
|
|
4472
|
+
() => authorFilter ? books.filter((book) => getAuthorId(book.author) === authorFilter) : books,
|
|
4473
|
+
[authorFilter, books]
|
|
4474
|
+
);
|
|
4475
|
+
const pageSize = Math.max(1, Math.floor(data.pageSize || 10));
|
|
4476
|
+
const totalPages = Math.max(1, Math.ceil(filteredBooks.length / pageSize));
|
|
4477
|
+
const [page, setPage] = useState(1);
|
|
4478
|
+
const currentPage = Math.min(page, totalPages);
|
|
4479
|
+
const startIndex = (currentPage - 1) * pageSize;
|
|
4480
|
+
const visibleBooks = filteredBooks.slice(startIndex, startIndex + pageSize);
|
|
4481
|
+
|
|
4482
|
+
useEffect(() => {
|
|
4483
|
+
setPage(1);
|
|
4484
|
+
}, [authorFilter]);
|
|
4485
|
+
|
|
4486
|
+
return (
|
|
4487
|
+
<main className="min-h-screen bg-background text-foreground px-6 py-16">
|
|
4488
|
+
<section className="mx-auto flex w-full max-w-5xl flex-col gap-10">
|
|
4489
|
+
<div className="max-w-2xl">
|
|
4490
|
+
{data.eyebrow && (
|
|
4491
|
+
<p
|
|
4492
|
+
data-jp-field="eyebrow"
|
|
4493
|
+
className="text-sm font-medium uppercase tracking-[0.18em] text-muted-foreground"
|
|
4494
|
+
>
|
|
4495
|
+
{data.eyebrow}
|
|
4496
|
+
</p>
|
|
4497
|
+
)}
|
|
4498
|
+
<h1
|
|
4499
|
+
data-jp-field="title"
|
|
4500
|
+
className="mt-3 text-4xl font-semibold tracking-tight sm:text-5xl"
|
|
4501
|
+
>
|
|
4502
|
+
{data.title}
|
|
4503
|
+
</h1>
|
|
4504
|
+
{data.description && (
|
|
4505
|
+
<p
|
|
4506
|
+
data-jp-field="description"
|
|
4507
|
+
className="mt-4 text-base leading-7 text-muted-foreground"
|
|
4508
|
+
>
|
|
4509
|
+
{data.description}
|
|
4510
|
+
</p>
|
|
4511
|
+
)}
|
|
4512
|
+
</div>
|
|
4513
|
+
|
|
4514
|
+
<div data-jp-field="items" className="grid gap-4">
|
|
4515
|
+
{authorFilter && (
|
|
4516
|
+
<p className="text-sm text-muted-foreground">
|
|
4517
|
+
Filtro autore: {authorFilter} · {filteredBooks.length} libri
|
|
4518
|
+
</p>
|
|
4519
|
+
)}
|
|
4520
|
+
{visibleBooks.map((book) => (
|
|
4521
|
+
<article
|
|
4522
|
+
key={book.id}
|
|
4523
|
+
data-jp-item-id={book.id}
|
|
4524
|
+
data-jp-item-field="items"
|
|
4525
|
+
className="rounded-xl border border-border bg-card p-5 shadow-sm"
|
|
4526
|
+
>
|
|
4527
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
4528
|
+
<div>
|
|
4529
|
+
<h2 className="text-xl font-semibold">{book.title}</h2>
|
|
4530
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
4531
|
+
{getAuthorName(book.author)} · {book.year} · {book.genre}
|
|
4532
|
+
</p>
|
|
4533
|
+
<p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
4534
|
+
{book.summary}
|
|
4535
|
+
</p>
|
|
4536
|
+
</div>
|
|
4537
|
+
<a
|
|
4538
|
+
href={`/libri/${book.id}`}
|
|
4539
|
+
className="inline-flex shrink-0 items-center justify-center rounded-md border border-border px-3 py-2 text-sm font-medium hover:bg-muted"
|
|
4540
|
+
>
|
|
4541
|
+
Apri scheda
|
|
4542
|
+
</a>
|
|
4543
|
+
</div>
|
|
4544
|
+
</article>
|
|
4545
|
+
))}
|
|
4546
|
+
</div>
|
|
4547
|
+
|
|
4548
|
+
<nav className="flex items-center justify-between border-t border-border pt-6 text-sm">
|
|
4549
|
+
<button
|
|
4550
|
+
type="button"
|
|
4551
|
+
onClick={() => setPage((value) => Math.max(1, value - 1))}
|
|
4552
|
+
disabled={currentPage === 1}
|
|
4553
|
+
className="rounded-md border border-border px-3 py-2 font-medium disabled:cursor-not-allowed disabled:opacity-40 hover:bg-muted"
|
|
4554
|
+
>
|
|
4555
|
+
Precedente
|
|
4556
|
+
</button>
|
|
4557
|
+
<span className="text-muted-foreground">
|
|
4558
|
+
Pagina {currentPage} di {totalPages} · {filteredBooks.length} libri
|
|
4559
|
+
</span>
|
|
4560
|
+
<button
|
|
4561
|
+
type="button"
|
|
4562
|
+
onClick={() => setPage((value) => Math.min(totalPages, value + 1))}
|
|
4563
|
+
disabled={currentPage === totalPages}
|
|
4564
|
+
className="rounded-md border border-border px-3 py-2 font-medium disabled:cursor-not-allowed disabled:opacity-40 hover:bg-muted"
|
|
4565
|
+
>
|
|
4566
|
+
Successiva
|
|
4567
|
+
</button>
|
|
4568
|
+
</nav>
|
|
4569
|
+
</section>
|
|
4570
|
+
</main>
|
|
4571
|
+
);
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
END_OF_FILE_CONTENT
|
|
4575
|
+
echo "Creating src/components/books-list/index.ts..."
|
|
4576
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/index.ts"
|
|
4577
|
+
export * from './View';
|
|
4578
|
+
export * from './schema';
|
|
4579
|
+
export * from './types';
|
|
4580
|
+
|
|
4581
|
+
END_OF_FILE_CONTENT
|
|
4582
|
+
echo "Creating src/components/books-list/schema.ts..."
|
|
4583
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/schema.ts"
|
|
4584
|
+
import { z } from 'zod';
|
|
4585
|
+
import { BaseSectionData } from '@olonjs/core';
|
|
4586
|
+
import { LibroSchema } from '@/collections/libri';
|
|
4587
|
+
|
|
4588
|
+
export const BooksListSchema = BaseSectionData.extend({
|
|
4589
|
+
eyebrow: z.string().optional().describe('ui:text'),
|
|
4590
|
+
title: z.string().describe('ui:text'),
|
|
4591
|
+
description: z.string().optional().describe('ui:textarea'),
|
|
4592
|
+
items: z.record(z.string(), LibroSchema).describe('ui:collection-ref:libri'),
|
|
4593
|
+
pageSize: z.number().default(10).describe('ui:number'),
|
|
4594
|
+
});
|
|
4595
|
+
|
|
4596
|
+
export const BooksListSettingsSchema = z.object({});
|
|
4597
|
+
|
|
4598
|
+
END_OF_FILE_CONTENT
|
|
4599
|
+
echo "Creating src/components/books-list/types.ts..."
|
|
4600
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/types.ts"
|
|
4601
|
+
import { z } from 'zod';
|
|
4602
|
+
import { BooksListSchema, BooksListSettingsSchema } from './schema';
|
|
4603
|
+
|
|
4604
|
+
export type BooksListData = z.infer<typeof BooksListSchema>;
|
|
4605
|
+
export type BooksListSettings = z.infer<typeof BooksListSettingsSchema>;
|
|
4606
|
+
|
|
4105
4607
|
END_OF_FILE_CONTENT
|
|
4106
4608
|
mkdir -p "src/components/empty-tenant"
|
|
4107
4609
|
echo "Creating src/components/empty-tenant/View.tsx..."
|
|
@@ -4155,6 +4657,123 @@ import { EmptyTenantSchema, EmptyTenantSettingsSchema } from './schema';
|
|
|
4155
4657
|
export type EmptyTenantData = z.infer<typeof EmptyTenantSchema>;
|
|
4156
4658
|
export type EmptyTenantSettings = z.infer<typeof EmptyTenantSettingsSchema>;
|
|
4157
4659
|
|
|
4660
|
+
END_OF_FILE_CONTENT
|
|
4661
|
+
mkdir -p "src/components/footer"
|
|
4662
|
+
echo "Creating src/components/footer/View.tsx..."
|
|
4663
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/footer/View.tsx"
|
|
4664
|
+
import type React from 'react';
|
|
4665
|
+
import type { FooterData, FooterSettings } from './types';
|
|
4666
|
+
|
|
4667
|
+
type FooterViewProps = {
|
|
4668
|
+
data: FooterData;
|
|
4669
|
+
settings?: FooterSettings;
|
|
4670
|
+
};
|
|
4671
|
+
|
|
4672
|
+
export function FooterView({ data, settings }: FooterViewProps) {
|
|
4673
|
+
const links = data.links ?? [];
|
|
4674
|
+
|
|
4675
|
+
return (
|
|
4676
|
+
<footer
|
|
4677
|
+
style={{
|
|
4678
|
+
'--local-bg': 'var(--background)',
|
|
4679
|
+
'--local-text': 'var(--foreground)',
|
|
4680
|
+
'--local-muted': 'var(--muted-foreground)',
|
|
4681
|
+
'--local-border': 'var(--border)',
|
|
4682
|
+
} as React.CSSProperties}
|
|
4683
|
+
className="border-t border-[var(--local-border)] bg-[var(--local-bg)] px-6 py-10 text-[var(--local-text)]"
|
|
4684
|
+
>
|
|
4685
|
+
<div className="mx-auto flex w-full max-w-5xl flex-col gap-8 md:flex-row md:items-end md:justify-between">
|
|
4686
|
+
<div className="max-w-md">
|
|
4687
|
+
<div className="flex items-center gap-3">
|
|
4688
|
+
{settings?.showLogo !== false && (
|
|
4689
|
+
<span className="flex h-9 w-9 items-center justify-center rounded-lg border border-[var(--local-border)] text-sm font-semibold">
|
|
4690
|
+
O
|
|
4691
|
+
</span>
|
|
4692
|
+
)}
|
|
4693
|
+
<p data-jp-field="brandText" className="text-lg font-semibold tracking-tight">
|
|
4694
|
+
{data.brandText}
|
|
4695
|
+
</p>
|
|
4696
|
+
</div>
|
|
4697
|
+
{data.description && (
|
|
4698
|
+
<p data-jp-field="description" className="mt-3 text-sm leading-6 text-[var(--local-muted)]">
|
|
4699
|
+
{data.description}
|
|
4700
|
+
</p>
|
|
4701
|
+
)}
|
|
4702
|
+
<p data-jp-field="copyright" className="mt-4 text-xs text-[var(--local-muted)]">
|
|
4703
|
+
{data.copyright}
|
|
4704
|
+
</p>
|
|
4705
|
+
</div>
|
|
4706
|
+
|
|
4707
|
+
<nav data-jp-field="links" className="flex flex-wrap gap-4 text-sm">
|
|
4708
|
+
{links.map((link) => (
|
|
4709
|
+
<a
|
|
4710
|
+
key={link.id}
|
|
4711
|
+
data-jp-item-id={link.id}
|
|
4712
|
+
data-jp-item-field="links"
|
|
4713
|
+
href={link.href}
|
|
4714
|
+
target={link.external ? '_blank' : undefined}
|
|
4715
|
+
rel={link.external ? 'noreferrer' : undefined}
|
|
4716
|
+
className="text-[var(--local-muted)] transition-colors hover:text-[var(--local-text)]"
|
|
4717
|
+
>
|
|
4718
|
+
{link.label}
|
|
4719
|
+
</a>
|
|
4720
|
+
))}
|
|
4721
|
+
{data.designSystemHref && (
|
|
4722
|
+
<a
|
|
4723
|
+
data-jp-field="designSystemHref"
|
|
4724
|
+
href={data.designSystemHref}
|
|
4725
|
+
className="text-[var(--local-muted)] transition-colors hover:text-[var(--local-text)]"
|
|
4726
|
+
>
|
|
4727
|
+
Design system
|
|
4728
|
+
</a>
|
|
4729
|
+
)}
|
|
4730
|
+
</nav>
|
|
4731
|
+
</div>
|
|
4732
|
+
</footer>
|
|
4733
|
+
);
|
|
4734
|
+
}
|
|
4735
|
+
|
|
4736
|
+
END_OF_FILE_CONTENT
|
|
4737
|
+
echo "Creating src/components/footer/index.ts..."
|
|
4738
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/footer/index.ts"
|
|
4739
|
+
export * from './View';
|
|
4740
|
+
export * from './schema';
|
|
4741
|
+
export * from './types';
|
|
4742
|
+
|
|
4743
|
+
END_OF_FILE_CONTENT
|
|
4744
|
+
echo "Creating src/components/footer/schema.ts..."
|
|
4745
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/footer/schema.ts"
|
|
4746
|
+
import { z } from 'zod';
|
|
4747
|
+
import { BaseArrayItem, BaseSectionData } from '@olonjs/core';
|
|
4748
|
+
|
|
4749
|
+
export const FooterLinkSchema = BaseArrayItem.extend({
|
|
4750
|
+
id: z.string(),
|
|
4751
|
+
label: z.string().describe('ui:text'),
|
|
4752
|
+
href: z.string().describe('ui:text'),
|
|
4753
|
+
external: z.boolean().optional(),
|
|
4754
|
+
});
|
|
4755
|
+
|
|
4756
|
+
export const FooterSchema = BaseSectionData.extend({
|
|
4757
|
+
brandText: z.string().describe('ui:text'),
|
|
4758
|
+
description: z.string().optional().describe('ui:textarea'),
|
|
4759
|
+
copyright: z.string().describe('ui:text'),
|
|
4760
|
+
links: z.array(FooterLinkSchema).default([]).describe('ui:list'),
|
|
4761
|
+
designSystemHref: z.string().optional().describe('ui:text'),
|
|
4762
|
+
});
|
|
4763
|
+
|
|
4764
|
+
export const FooterSettingsSchema = z.object({
|
|
4765
|
+
showLogo: z.boolean().default(true),
|
|
4766
|
+
});
|
|
4767
|
+
|
|
4768
|
+
END_OF_FILE_CONTENT
|
|
4769
|
+
echo "Creating src/components/footer/types.ts..."
|
|
4770
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/footer/types.ts"
|
|
4771
|
+
import { z } from 'zod';
|
|
4772
|
+
import { FooterSchema, FooterSettingsSchema } from './schema';
|
|
4773
|
+
|
|
4774
|
+
export type FooterData = z.infer<typeof FooterSchema>;
|
|
4775
|
+
export type FooterSettings = z.infer<typeof FooterSettingsSchema>;
|
|
4776
|
+
|
|
4158
4777
|
END_OF_FILE_CONTENT
|
|
4159
4778
|
mkdir -p "src/components/form-demo"
|
|
4160
4779
|
echo "Creating src/components/form-demo/Untitled..."
|
|
@@ -8567,6 +9186,436 @@ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
|
|
8567
9186
|
END_OF_FILE_CONTENT
|
|
8568
9187
|
# SKIP: src/components/ui/tooltip.tsx:Zone.Identifier is binary and cannot be embedded as text.
|
|
8569
9188
|
mkdir -p "src/data"
|
|
9189
|
+
mkdir -p "src/data/collections"
|
|
9190
|
+
mkdir -p "src/data/collections/autori"
|
|
9191
|
+
echo "Creating src/data/collections/autori/autori.json..."
|
|
9192
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/collections/autori/autori.json"
|
|
9193
|
+
{
|
|
9194
|
+
"george-orwell": {
|
|
9195
|
+
"id": "george-orwell",
|
|
9196
|
+
"name": "George Orwell"
|
|
9197
|
+
},
|
|
9198
|
+
"umberto-eco": {
|
|
9199
|
+
"id": "umberto-eco",
|
|
9200
|
+
"name": "Umberto Eco"
|
|
9201
|
+
},
|
|
9202
|
+
"primo-levi": {
|
|
9203
|
+
"id": "primo-levi",
|
|
9204
|
+
"name": "Primo Levi"
|
|
9205
|
+
},
|
|
9206
|
+
"italo-calvino": {
|
|
9207
|
+
"id": "italo-calvino",
|
|
9208
|
+
"name": "Italo Calvino"
|
|
9209
|
+
},
|
|
9210
|
+
"giuseppe-tomasi-di-lampedusa": {
|
|
9211
|
+
"id": "giuseppe-tomasi-di-lampedusa",
|
|
9212
|
+
"name": "Giuseppe Tomasi di Lampedusa"
|
|
9213
|
+
},
|
|
9214
|
+
"italo-svevo": {
|
|
9215
|
+
"id": "italo-svevo",
|
|
9216
|
+
"name": "Italo Svevo"
|
|
9217
|
+
},
|
|
9218
|
+
"alessandro-manzoni": {
|
|
9219
|
+
"id": "alessandro-manzoni",
|
|
9220
|
+
"name": "Alessandro Manzoni"
|
|
9221
|
+
},
|
|
9222
|
+
"roberto-saviano": {
|
|
9223
|
+
"id": "roberto-saviano",
|
|
9224
|
+
"name": "Roberto Saviano"
|
|
9225
|
+
},
|
|
9226
|
+
"alessandro-baricco": {
|
|
9227
|
+
"id": "alessandro-baricco",
|
|
9228
|
+
"name": "Alessandro Baricco"
|
|
9229
|
+
},
|
|
9230
|
+
"natalia-ginzburg": {
|
|
9231
|
+
"id": "natalia-ginzburg",
|
|
9232
|
+
"name": "Natalia Ginzburg"
|
|
9233
|
+
},
|
|
9234
|
+
"gabriel-garcia-marquez": {
|
|
9235
|
+
"id": "gabriel-garcia-marquez",
|
|
9236
|
+
"name": "Gabriel Garcia Marquez"
|
|
9237
|
+
},
|
|
9238
|
+
"ray-bradbury": {
|
|
9239
|
+
"id": "ray-bradbury",
|
|
9240
|
+
"name": "Ray Bradbury"
|
|
9241
|
+
},
|
|
9242
|
+
"frank-herbert": {
|
|
9243
|
+
"id": "frank-herbert",
|
|
9244
|
+
"name": "Frank Herbert"
|
|
9245
|
+
},
|
|
9246
|
+
"william-gibson": {
|
|
9247
|
+
"id": "william-gibson",
|
|
9248
|
+
"name": "William Gibson"
|
|
9249
|
+
},
|
|
9250
|
+
"j-r-r-tolkien": {
|
|
9251
|
+
"id": "j-r-r-tolkien",
|
|
9252
|
+
"name": "J. R. R. Tolkien"
|
|
9253
|
+
},
|
|
9254
|
+
"j-k-rowling": {
|
|
9255
|
+
"id": "j-k-rowling",
|
|
9256
|
+
"name": "J. K. Rowling"
|
|
9257
|
+
},
|
|
9258
|
+
"jane-austen": {
|
|
9259
|
+
"id": "jane-austen",
|
|
9260
|
+
"name": "Jane Austen"
|
|
9261
|
+
},
|
|
9262
|
+
"herman-melville": {
|
|
9263
|
+
"id": "herman-melville",
|
|
9264
|
+
"name": "Herman Melville"
|
|
9265
|
+
},
|
|
9266
|
+
"fedor-dostoevskij": {
|
|
9267
|
+
"id": "fedor-dostoevskij",
|
|
9268
|
+
"name": "Fedor Dostoevskij"
|
|
9269
|
+
},
|
|
9270
|
+
"lev-tolstoj": {
|
|
9271
|
+
"id": "lev-tolstoj",
|
|
9272
|
+
"name": "Lev Tolstoj"
|
|
9273
|
+
},
|
|
9274
|
+
"michail-bulgakov": {
|
|
9275
|
+
"id": "michail-bulgakov",
|
|
9276
|
+
"name": "Michail Bulgakov"
|
|
9277
|
+
},
|
|
9278
|
+
"cormac-mccarthy": {
|
|
9279
|
+
"id": "cormac-mccarthy",
|
|
9280
|
+
"name": "Cormac McCarthy"
|
|
9281
|
+
},
|
|
9282
|
+
"david-mitchell": {
|
|
9283
|
+
"id": "david-mitchell",
|
|
9284
|
+
"name": "David Mitchell"
|
|
9285
|
+
},
|
|
9286
|
+
"haruki-murakami": {
|
|
9287
|
+
"id": "haruki-murakami",
|
|
9288
|
+
"name": "Haruki Murakami"
|
|
9289
|
+
},
|
|
9290
|
+
"chimamanda-ngozi-adichie": {
|
|
9291
|
+
"id": "chimamanda-ngozi-adichie",
|
|
9292
|
+
"name": "Chimamanda Ngozi Adichie"
|
|
9293
|
+
},
|
|
9294
|
+
"isabel-allende": {
|
|
9295
|
+
"id": "isabel-allende",
|
|
9296
|
+
"name": "Isabel Allende"
|
|
9297
|
+
},
|
|
9298
|
+
"toni-morrison": {
|
|
9299
|
+
"id": "toni-morrison",
|
|
9300
|
+
"name": "Toni Morrison"
|
|
9301
|
+
},
|
|
9302
|
+
"junot-diaz": {
|
|
9303
|
+
"id": "junot-diaz",
|
|
9304
|
+
"name": "Junot Diaz"
|
|
9305
|
+
},
|
|
9306
|
+
"jonathan-franzen": {
|
|
9307
|
+
"id": "jonathan-franzen",
|
|
9308
|
+
"name": "Jonathan Franzen"
|
|
9309
|
+
}
|
|
9310
|
+
}
|
|
9311
|
+
|
|
9312
|
+
END_OF_FILE_CONTENT
|
|
9313
|
+
mkdir -p "src/data/collections/libri"
|
|
9314
|
+
echo "Creating src/data/collections/libri/libri.json..."
|
|
9315
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/collections/libri/libri.json"
|
|
9316
|
+
{
|
|
9317
|
+
"1984": {
|
|
9318
|
+
"id": "1984",
|
|
9319
|
+
"title": "1984",
|
|
9320
|
+
"author": {
|
|
9321
|
+
"$ref": "../autori/autori.json#/george-orwell"
|
|
9322
|
+
},
|
|
9323
|
+
"year": 1949,
|
|
9324
|
+
"genre": "Distopia",
|
|
9325
|
+
"summary": "Un regime totalitario controlla linguaggio, memoria e pensiero."
|
|
9326
|
+
},
|
|
9327
|
+
"il-nome-della-rosa": {
|
|
9328
|
+
"id": "il-nome-della-rosa",
|
|
9329
|
+
"title": "Il nome della rosa",
|
|
9330
|
+
"author": {
|
|
9331
|
+
"$ref": "../autori/autori.json#/umberto-eco"
|
|
9332
|
+
},
|
|
9333
|
+
"year": 1980,
|
|
9334
|
+
"genre": "Romanzo storico",
|
|
9335
|
+
"summary": "Un'indagine in un'abbazia medievale diventa una riflessione su conoscenza, potere e interpretazione."
|
|
9336
|
+
},
|
|
9337
|
+
"se-questo-e-un-uomo": {
|
|
9338
|
+
"id": "se-questo-e-un-uomo",
|
|
9339
|
+
"title": "Se questo e un uomo",
|
|
9340
|
+
"author": {
|
|
9341
|
+
"$ref": "../autori/autori.json#/primo-levi"
|
|
9342
|
+
},
|
|
9343
|
+
"year": 1947,
|
|
9344
|
+
"genre": "Memoria",
|
|
9345
|
+
"summary": "La testimonianza essenziale di Levi sull'esperienza del lager e sulla dignita umana."
|
|
9346
|
+
},
|
|
9347
|
+
"le-citta-invisibili": {
|
|
9348
|
+
"id": "le-citta-invisibili",
|
|
9349
|
+
"title": "Le citta invisibili",
|
|
9350
|
+
"author": {
|
|
9351
|
+
"$ref": "../autori/autori.json#/italo-calvino"
|
|
9352
|
+
},
|
|
9353
|
+
"year": 1972,
|
|
9354
|
+
"genre": "Letteratura fantastica",
|
|
9355
|
+
"summary": "Marco Polo racconta a Kublai Khan citta immaginarie che parlano di memoria, desiderio e linguaggio."
|
|
9356
|
+
},
|
|
9357
|
+
"il-gattopardo": {
|
|
9358
|
+
"id": "il-gattopardo",
|
|
9359
|
+
"title": "Il Gattopardo",
|
|
9360
|
+
"author": {
|
|
9361
|
+
"$ref": "../autori/autori.json#/giuseppe-tomasi-di-lampedusa"
|
|
9362
|
+
},
|
|
9363
|
+
"year": 1958,
|
|
9364
|
+
"genre": "Romanzo storico",
|
|
9365
|
+
"summary": "Il tramonto dell'aristocrazia siciliana durante l'unificazione italiana."
|
|
9366
|
+
},
|
|
9367
|
+
"la-coscienza-di-zeno": {
|
|
9368
|
+
"id": "la-coscienza-di-zeno",
|
|
9369
|
+
"title": "La coscienza di Zeno",
|
|
9370
|
+
"author": {
|
|
9371
|
+
"$ref": "../autori/autori.json#/italo-svevo"
|
|
9372
|
+
},
|
|
9373
|
+
"year": 1923,
|
|
9374
|
+
"genre": "Romanzo psicologico",
|
|
9375
|
+
"summary": "Un diario ironico e nevrotico attraversa memoria, terapia e autoinganno."
|
|
9376
|
+
},
|
|
9377
|
+
"i-promessi-sposi": {
|
|
9378
|
+
"id": "i-promessi-sposi",
|
|
9379
|
+
"title": "I promessi sposi",
|
|
9380
|
+
"author": {
|
|
9381
|
+
"$ref": "../autori/autori.json#/alessandro-manzoni"
|
|
9382
|
+
},
|
|
9383
|
+
"year": 1842,
|
|
9384
|
+
"genre": "Classico",
|
|
9385
|
+
"summary": "La vicenda di Renzo e Lucia dentro carestia, guerra, peste e provvidenza."
|
|
9386
|
+
},
|
|
9387
|
+
"il-barone-rampante": {
|
|
9388
|
+
"id": "il-barone-rampante",
|
|
9389
|
+
"title": "Il barone rampante",
|
|
9390
|
+
"author": {
|
|
9391
|
+
"$ref": "../autori/autori.json#/italo-calvino"
|
|
9392
|
+
},
|
|
9393
|
+
"year": 1957,
|
|
9394
|
+
"genre": "Romanzo filosofico",
|
|
9395
|
+
"summary": "Cosimo sceglie di vivere sugli alberi e trasforma la distanza in una forma di liberta."
|
|
9396
|
+
},
|
|
9397
|
+
"gomorra": {
|
|
9398
|
+
"id": "gomorra",
|
|
9399
|
+
"title": "Gomorra",
|
|
9400
|
+
"author": {
|
|
9401
|
+
"$ref": "../autori/autori.json#/roberto-saviano"
|
|
9402
|
+
},
|
|
9403
|
+
"year": 2006,
|
|
9404
|
+
"genre": "Inchiesta",
|
|
9405
|
+
"summary": "Un reportage narrativo sulle economie e le violenze del sistema camorristico."
|
|
9406
|
+
},
|
|
9407
|
+
"oceano-mare": {
|
|
9408
|
+
"id": "oceano-mare",
|
|
9409
|
+
"title": "Oceano mare",
|
|
9410
|
+
"author": {
|
|
9411
|
+
"$ref": "../autori/autori.json#/alessandro-baricco"
|
|
9412
|
+
},
|
|
9413
|
+
"year": 1993,
|
|
9414
|
+
"genre": "Romanzo letterario",
|
|
9415
|
+
"summary": "Storie diverse si incontrano in una locanda sul mare, tra cura, naufragio e mistero."
|
|
9416
|
+
},
|
|
9417
|
+
"lessico-famigliare": {
|
|
9418
|
+
"id": "lessico-famigliare",
|
|
9419
|
+
"title": "Lessico famigliare",
|
|
9420
|
+
"author": {
|
|
9421
|
+
"$ref": "../autori/autori.json#/natalia-ginzburg"
|
|
9422
|
+
},
|
|
9423
|
+
"year": 1963,
|
|
9424
|
+
"genre": "Memoria narrativa",
|
|
9425
|
+
"summary": "Una famiglia prende forma attraverso parole, tic linguistici e memoria civile."
|
|
9426
|
+
},
|
|
9427
|
+
"cent-anni-di-solitudine": {
|
|
9428
|
+
"id": "cent-anni-di-solitudine",
|
|
9429
|
+
"title": "Cent'anni di solitudine",
|
|
9430
|
+
"author": {
|
|
9431
|
+
"$ref": "../autori/autori.json#/gabriel-garcia-marquez"
|
|
9432
|
+
},
|
|
9433
|
+
"year": 1967,
|
|
9434
|
+
"genre": "Realismo magico",
|
|
9435
|
+
"summary": "La saga dei Buendia e di Macondo intreccia mito, storia e destino."
|
|
9436
|
+
},
|
|
9437
|
+
"fahrenheit-451": {
|
|
9438
|
+
"id": "fahrenheit-451",
|
|
9439
|
+
"title": "Fahrenheit 451",
|
|
9440
|
+
"author": {
|
|
9441
|
+
"$ref": "../autori/autori.json#/ray-bradbury"
|
|
9442
|
+
},
|
|
9443
|
+
"year": 1953,
|
|
9444
|
+
"genre": "Distopia",
|
|
9445
|
+
"summary": "In un futuro dove i libri bruciano, leggere diventa un atto di resistenza."
|
|
9446
|
+
},
|
|
9447
|
+
"dune": {
|
|
9448
|
+
"id": "dune",
|
|
9449
|
+
"title": "Dune",
|
|
9450
|
+
"author": {
|
|
9451
|
+
"$ref": "../autori/autori.json#/frank-herbert"
|
|
9452
|
+
},
|
|
9453
|
+
"year": 1965,
|
|
9454
|
+
"genre": "Fantascienza",
|
|
9455
|
+
"summary": "Politica, ecologia e messianismo si scontrano sul pianeta desertico Arrakis."
|
|
9456
|
+
},
|
|
9457
|
+
"neuromancer": {
|
|
9458
|
+
"id": "neuromancer",
|
|
9459
|
+
"title": "Neuromancer",
|
|
9460
|
+
"author": {
|
|
9461
|
+
"$ref": "../autori/autori.json#/william-gibson"
|
|
9462
|
+
},
|
|
9463
|
+
"year": 1984,
|
|
9464
|
+
"genre": "Cyberpunk",
|
|
9465
|
+
"summary": "Un hacker decaduto viene trascinato in un colpo che attraversa cyberspazio e intelligenze artificiali."
|
|
9466
|
+
},
|
|
9467
|
+
"il-signore-degli-anelli": {
|
|
9468
|
+
"id": "il-signore-degli-anelli",
|
|
9469
|
+
"title": "Il Signore degli Anelli",
|
|
9470
|
+
"author": {
|
|
9471
|
+
"$ref": "../autori/autori.json#/j-r-r-tolkien"
|
|
9472
|
+
},
|
|
9473
|
+
"year": 1954,
|
|
9474
|
+
"genre": "Fantasy",
|
|
9475
|
+
"summary": "La Compagnia affronta il potere dell'Anello in una delle grandi epopee moderne."
|
|
9476
|
+
},
|
|
9477
|
+
"harry-potter-e-la-pietra-filosofale": {
|
|
9478
|
+
"id": "harry-potter-e-la-pietra-filosofale",
|
|
9479
|
+
"title": "Harry Potter e la pietra filosofale",
|
|
9480
|
+
"author": {
|
|
9481
|
+
"$ref": "../autori/autori.json#/j-k-rowling"
|
|
9482
|
+
},
|
|
9483
|
+
"year": 1997,
|
|
9484
|
+
"genre": "Fantasy",
|
|
9485
|
+
"summary": "Un ragazzo scopre il mondo magico e il proprio posto in una storia piu grande."
|
|
9486
|
+
},
|
|
9487
|
+
"orgoglio-e-pregiudizio": {
|
|
9488
|
+
"id": "orgoglio-e-pregiudizio",
|
|
9489
|
+
"title": "Orgoglio e pregiudizio",
|
|
9490
|
+
"author": {
|
|
9491
|
+
"$ref": "../autori/autori.json#/jane-austen"
|
|
9492
|
+
},
|
|
9493
|
+
"year": 1813,
|
|
9494
|
+
"genre": "Classico",
|
|
9495
|
+
"summary": "Elizabeth Bennet e Mr. Darcy si misurano con classe, carattere e giudizio sociale."
|
|
9496
|
+
},
|
|
9497
|
+
"moby-dick": {
|
|
9498
|
+
"id": "moby-dick",
|
|
9499
|
+
"title": "Moby Dick",
|
|
9500
|
+
"author": {
|
|
9501
|
+
"$ref": "../autori/autori.json#/herman-melville"
|
|
9502
|
+
},
|
|
9503
|
+
"year": 1851,
|
|
9504
|
+
"genre": "Avventura",
|
|
9505
|
+
"summary": "La caccia alla balena bianca diventa ossessione metafisica e viaggio nell'abisso."
|
|
9506
|
+
},
|
|
9507
|
+
"delitto-e-castigo": {
|
|
9508
|
+
"id": "delitto-e-castigo",
|
|
9509
|
+
"title": "Delitto e castigo",
|
|
9510
|
+
"author": {
|
|
9511
|
+
"$ref": "../autori/autori.json#/fedor-dostoevskij"
|
|
9512
|
+
},
|
|
9513
|
+
"year": 1866,
|
|
9514
|
+
"genre": "Romanzo psicologico",
|
|
9515
|
+
"summary": "Raskolnikov attraversa colpa, febbre morale e possibilita di redenzione."
|
|
9516
|
+
},
|
|
9517
|
+
"anna-karenina": {
|
|
9518
|
+
"id": "anna-karenina",
|
|
9519
|
+
"title": "Anna Karenina",
|
|
9520
|
+
"author": {
|
|
9521
|
+
"$ref": "../autori/autori.json#/lev-tolstoj"
|
|
9522
|
+
},
|
|
9523
|
+
"year": 1877,
|
|
9524
|
+
"genre": "Classico",
|
|
9525
|
+
"summary": "Una storia d'amore e rovina dentro la societa russa dell'Ottocento."
|
|
9526
|
+
},
|
|
9527
|
+
"il-maestro-e-margherita": {
|
|
9528
|
+
"id": "il-maestro-e-margherita",
|
|
9529
|
+
"title": "Il maestro e Margherita",
|
|
9530
|
+
"author": {
|
|
9531
|
+
"$ref": "../autori/autori.json#/michail-bulgakov"
|
|
9532
|
+
},
|
|
9533
|
+
"year": 1967,
|
|
9534
|
+
"genre": "Satira fantastica",
|
|
9535
|
+
"summary": "Il diavolo visita Mosca in un romanzo visionario su arte, censura e amore."
|
|
9536
|
+
},
|
|
9537
|
+
"la-strada": {
|
|
9538
|
+
"id": "la-strada",
|
|
9539
|
+
"title": "La strada",
|
|
9540
|
+
"author": {
|
|
9541
|
+
"$ref": "../autori/autori.json#/cormac-mccarthy"
|
|
9542
|
+
},
|
|
9543
|
+
"year": 2006,
|
|
9544
|
+
"genre": "Post-apocalittico",
|
|
9545
|
+
"summary": "Padre e figlio attraversano un mondo bruciato portando con se una fragile idea di bene."
|
|
9546
|
+
},
|
|
9547
|
+
"cloud-atlas": {
|
|
9548
|
+
"id": "cloud-atlas",
|
|
9549
|
+
"title": "Cloud Atlas",
|
|
9550
|
+
"author": {
|
|
9551
|
+
"$ref": "../autori/autori.json#/david-mitchell"
|
|
9552
|
+
},
|
|
9553
|
+
"year": 2004,
|
|
9554
|
+
"genre": "Romanzo corale",
|
|
9555
|
+
"summary": "Sei storie in epoche diverse compongono una meditazione su potere, memoria e reincorrenza."
|
|
9556
|
+
},
|
|
9557
|
+
"kafka-sulla-spiaggia": {
|
|
9558
|
+
"id": "kafka-sulla-spiaggia",
|
|
9559
|
+
"title": "Kafka sulla spiaggia",
|
|
9560
|
+
"author": {
|
|
9561
|
+
"$ref": "../autori/autori.json#/haruki-murakami"
|
|
9562
|
+
},
|
|
9563
|
+
"year": 2002,
|
|
9564
|
+
"genre": "Surrealismo",
|
|
9565
|
+
"summary": "Fuga, destino e sogno si intrecciano in un romanzo sospeso tra reale e mitico."
|
|
9566
|
+
},
|
|
9567
|
+
"americanah": {
|
|
9568
|
+
"id": "americanah",
|
|
9569
|
+
"title": "Americanah",
|
|
9570
|
+
"author": {
|
|
9571
|
+
"$ref": "../autori/autori.json#/chimamanda-ngozi-adichie"
|
|
9572
|
+
},
|
|
9573
|
+
"year": 2013,
|
|
9574
|
+
"genre": "Romanzo contemporaneo",
|
|
9575
|
+
"summary": "Migrazione, razza e identita raccontate attraverso una storia d'amore tra Nigeria e Stati Uniti."
|
|
9576
|
+
},
|
|
9577
|
+
"la-casa-degli-spiriti": {
|
|
9578
|
+
"id": "la-casa-degli-spiriti",
|
|
9579
|
+
"title": "La casa degli spiriti",
|
|
9580
|
+
"author": {
|
|
9581
|
+
"$ref": "../autori/autori.json#/isabel-allende"
|
|
9582
|
+
},
|
|
9583
|
+
"year": 1982,
|
|
9584
|
+
"genre": "Saga familiare",
|
|
9585
|
+
"summary": "La storia della famiglia Trueba intreccia politica, memoria e realismo magico."
|
|
9586
|
+
},
|
|
9587
|
+
"beloved": {
|
|
9588
|
+
"id": "beloved",
|
|
9589
|
+
"title": "Beloved",
|
|
9590
|
+
"author": {
|
|
9591
|
+
"$ref": "../autori/autori.json#/toni-morrison"
|
|
9592
|
+
},
|
|
9593
|
+
"year": 1987,
|
|
9594
|
+
"genre": "Romanzo storico",
|
|
9595
|
+
"summary": "Il trauma della schiavitu ritorna come presenza viva nella casa di Sethe."
|
|
9596
|
+
},
|
|
9597
|
+
"la-breve-favolosa-vita-di-oscar-wao": {
|
|
9598
|
+
"id": "la-breve-favolosa-vita-di-oscar-wao",
|
|
9599
|
+
"title": "La breve favolosa vita di Oscar Wao",
|
|
9600
|
+
"author": {
|
|
9601
|
+
"$ref": "../autori/autori.json#/junot-diaz"
|
|
9602
|
+
},
|
|
9603
|
+
"year": 2007,
|
|
9604
|
+
"genre": "Romanzo contemporaneo",
|
|
9605
|
+
"summary": "Famiglia, diaspora dominicana e cultura pop si intrecciano nella storia di Oscar."
|
|
9606
|
+
},
|
|
9607
|
+
"le-correzioni": {
|
|
9608
|
+
"id": "le-correzioni",
|
|
9609
|
+
"title": "Le correzioni",
|
|
9610
|
+
"author": {
|
|
9611
|
+
"$ref": "../autori/autori.json#/jonathan-franzen"
|
|
9612
|
+
},
|
|
9613
|
+
"year": 2001,
|
|
9614
|
+
"genre": "Romanzo familiare",
|
|
9615
|
+
"summary": "Una famiglia americana tenta di ritrovarsi mentre ciascuno affronta le proprie fratture."
|
|
9616
|
+
}
|
|
9617
|
+
}
|
|
9618
|
+
END_OF_FILE_CONTENT
|
|
8570
9619
|
mkdir -p "src/data/config"
|
|
8571
9620
|
echo "Creating src/data/config/menu.json..."
|
|
8572
9621
|
cat << 'END_OF_FILE_CONTENT' > "src/data/config/menu.json"
|
|
@@ -8592,6 +9641,24 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/menu.json"
|
|
|
8592
9641
|
"label": "GitHub",
|
|
8593
9642
|
"href": "https://github.com/olonjs/core"
|
|
8594
9643
|
}
|
|
9644
|
+
],
|
|
9645
|
+
"footer": [
|
|
9646
|
+
{
|
|
9647
|
+
"id": "footer-home",
|
|
9648
|
+
"label": "Home",
|
|
9649
|
+
"href": "/"
|
|
9650
|
+
},
|
|
9651
|
+
{
|
|
9652
|
+
"id": "footer-libri",
|
|
9653
|
+
"label": "Libri",
|
|
9654
|
+
"href": "/libri"
|
|
9655
|
+
},
|
|
9656
|
+
{
|
|
9657
|
+
"id": "footer-github",
|
|
9658
|
+
"label": "GitHub",
|
|
9659
|
+
"href": "https://github.com/olonjs/core",
|
|
9660
|
+
"external": true
|
|
9661
|
+
}
|
|
8595
9662
|
]
|
|
8596
9663
|
}
|
|
8597
9664
|
END_OF_FILE_CONTENT
|
|
@@ -8647,7 +9714,6 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
|
|
|
8647
9714
|
"title": "OlonJS",
|
|
8648
9715
|
"logoUrl": "/brand/mark/olon-mark-dark.svg"
|
|
8649
9716
|
},
|
|
8650
|
-
|
|
8651
9717
|
"header": {
|
|
8652
9718
|
"id": "global-header",
|
|
8653
9719
|
"type": "header",
|
|
@@ -8686,13 +9752,11 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
|
|
|
8686
9752
|
"type": "footer",
|
|
8687
9753
|
"data": {
|
|
8688
9754
|
"brandText": "OlonJS",
|
|
9755
|
+
"description": "AI-native content infrastructure for deterministic, sovereign, git-backed sites.",
|
|
8689
9756
|
"copyright": "© 2026 OlonJS · v1.5 · Guido Serio",
|
|
8690
|
-
"links":
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
"href": "https://github.com/olonjs/core"
|
|
8694
|
-
}
|
|
8695
|
-
],
|
|
9757
|
+
"links": {
|
|
9758
|
+
"$ref": "../config/menu.json#/footer"
|
|
9759
|
+
},
|
|
8696
9760
|
"designSystemHref": ""
|
|
8697
9761
|
},
|
|
8698
9762
|
"settings": {
|
|
@@ -8869,11 +9933,70 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/theme.json"
|
|
|
8869
9933
|
}
|
|
8870
9934
|
END_OF_FILE_CONTENT
|
|
8871
9935
|
mkdir -p "src/data/pages"
|
|
8872
|
-
|
|
8873
|
-
|
|
9936
|
+
mkdir -p "src/data/pages/authors"
|
|
9937
|
+
echo "Creating src/data/pages/authors.json..."
|
|
9938
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/authors.json"
|
|
9939
|
+
{
|
|
9940
|
+
"id": "authors-page",
|
|
9941
|
+
"slug": "authors",
|
|
9942
|
+
"meta": {
|
|
9943
|
+
"title": "Authors",
|
|
9944
|
+
"description": "Author directory powered by the autori collection."
|
|
9945
|
+
},
|
|
9946
|
+
"sections": [
|
|
9947
|
+
{
|
|
9948
|
+
"id": "authors-list-1",
|
|
9949
|
+
"type": "authors-list",
|
|
9950
|
+
"data": {
|
|
9951
|
+
"anchorId": "authors",
|
|
9952
|
+
"eyebrow": "Collection demo",
|
|
9953
|
+
"title": "Authors",
|
|
9954
|
+
"description": "A collection-backed directory of authors referenced by books.",
|
|
9955
|
+
"items": {
|
|
9956
|
+
"$ref": "../collections/autori/autori.json"
|
|
9957
|
+
}
|
|
9958
|
+
}
|
|
9959
|
+
}
|
|
9960
|
+
],
|
|
9961
|
+
"global-header": false
|
|
9962
|
+
}
|
|
9963
|
+
|
|
9964
|
+
END_OF_FILE_CONTENT
|
|
9965
|
+
mkdir -p "src/data/pages/authors/[authorId]"
|
|
9966
|
+
echo "Creating src/data/pages/authors/[authorId]/libri.json..."
|
|
9967
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/authors/[authorId]/libri.json"
|
|
9968
|
+
{
|
|
9969
|
+
"id": "author-books-page",
|
|
9970
|
+
"slug": "authors/[authorId]/libri",
|
|
9971
|
+
"meta": {
|
|
9972
|
+
"title": "Libri per autore",
|
|
9973
|
+
"description": "Catalogo libri filtrato per autore."
|
|
9974
|
+
},
|
|
9975
|
+
"sections": [
|
|
9976
|
+
{
|
|
9977
|
+
"id": "books-list-author-filter",
|
|
9978
|
+
"type": "books-list",
|
|
9979
|
+
"data": {
|
|
9980
|
+
"anchorId": "libri-autore",
|
|
9981
|
+
"eyebrow": "Author books",
|
|
9982
|
+
"title": "Libri",
|
|
9983
|
+
"description": "Libri filtrati per autore dalla collection autori.",
|
|
9984
|
+
"items": {
|
|
9985
|
+
"$ref": "../../collections/libri/libri.json"
|
|
9986
|
+
},
|
|
9987
|
+
"pageSize": 10
|
|
9988
|
+
}
|
|
9989
|
+
}
|
|
9990
|
+
],
|
|
9991
|
+
"global-header": false
|
|
9992
|
+
}
|
|
9993
|
+
|
|
9994
|
+
END_OF_FILE_CONTENT
|
|
9995
|
+
echo "Creating src/data/pages/form.json..."
|
|
9996
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/form.json"
|
|
8874
9997
|
{
|
|
8875
|
-
"id": "
|
|
8876
|
-
"slug": "
|
|
9998
|
+
"id": "form-page",
|
|
9999
|
+
"slug": "form",
|
|
8877
10000
|
"meta": {
|
|
8878
10001
|
"title": "Home",
|
|
8879
10002
|
"description": "OlonJS tenant alpha — form smoke test"
|
|
@@ -8896,6 +10019,93 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
8896
10019
|
"global-header": false
|
|
8897
10020
|
}
|
|
8898
10021
|
END_OF_FILE_CONTENT
|
|
10022
|
+
echo "Creating src/data/pages/home.json..."
|
|
10023
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
10024
|
+
{
|
|
10025
|
+
"id": "libri-page",
|
|
10026
|
+
"slug": "home",
|
|
10027
|
+
"meta": {
|
|
10028
|
+
"title": "Libri",
|
|
10029
|
+
"description": "Catalogo libri dimostrativo alimentato da COP collections."
|
|
10030
|
+
},
|
|
10031
|
+
"sections": [
|
|
10032
|
+
{
|
|
10033
|
+
"id": "books-list-1",
|
|
10034
|
+
"type": "books-list",
|
|
10035
|
+
"data": {
|
|
10036
|
+
"anchorId": "catalogo-libri",
|
|
10037
|
+
"eyebrow": "Collection demo",
|
|
10038
|
+
"title": "Libri",
|
|
10039
|
+
"description": "Una pagina collection con 30 titoli, paginazione lato componente e link alle schede dinamiche.",
|
|
10040
|
+
"items": {
|
|
10041
|
+
"$ref": "../collections/libri/libri.json"
|
|
10042
|
+
},
|
|
10043
|
+
"pageSize": 10
|
|
10044
|
+
}
|
|
10045
|
+
}
|
|
10046
|
+
],
|
|
10047
|
+
"global-header": false
|
|
10048
|
+
}
|
|
10049
|
+
END_OF_FILE_CONTENT
|
|
10050
|
+
mkdir -p "src/data/pages/libri"
|
|
10051
|
+
echo "Creating src/data/pages/libri.json..."
|
|
10052
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/libri.json"
|
|
10053
|
+
{
|
|
10054
|
+
"id": "libri-page",
|
|
10055
|
+
"slug": "libri",
|
|
10056
|
+
"meta": {
|
|
10057
|
+
"title": "Libri",
|
|
10058
|
+
"description": "Catalogo libri dimostrativo alimentato da COP collections."
|
|
10059
|
+
},
|
|
10060
|
+
"sections": [
|
|
10061
|
+
{
|
|
10062
|
+
"id": "books-list-1",
|
|
10063
|
+
"type": "books-list",
|
|
10064
|
+
"data": {
|
|
10065
|
+
"anchorId": "catalogo-libri",
|
|
10066
|
+
"eyebrow": "Collection demo",
|
|
10067
|
+
"title": "Libri",
|
|
10068
|
+
"description": "Una pagina collection con titoli, paginazione lato componente e filtro autore.",
|
|
10069
|
+
"items": {
|
|
10070
|
+
"$ref": "../collections/libri/libri.json"
|
|
10071
|
+
},
|
|
10072
|
+
"pageSize": 10
|
|
10073
|
+
}
|
|
10074
|
+
}
|
|
10075
|
+
],
|
|
10076
|
+
"global-header": false
|
|
10077
|
+
}
|
|
10078
|
+
|
|
10079
|
+
END_OF_FILE_CONTENT
|
|
10080
|
+
echo "Creating src/data/pages/libri/[slug].json..."
|
|
10081
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/libri/[slug].json"
|
|
10082
|
+
{
|
|
10083
|
+
"id": "libro-detail-page",
|
|
10084
|
+
"slug": "libri/[slug]",
|
|
10085
|
+
"meta": {
|
|
10086
|
+
"title": "Dettaglio libro",
|
|
10087
|
+
"description": "Pagina dinamica dettaglio libro alimentata dalla collection libri."
|
|
10088
|
+
},
|
|
10089
|
+
"sections": [
|
|
10090
|
+
{
|
|
10091
|
+
"id": "book-detail-1",
|
|
10092
|
+
"type": "book-detail",
|
|
10093
|
+
"data": {
|
|
10094
|
+
"anchorId": "dettaglio-libro",
|
|
10095
|
+
"item": {
|
|
10096
|
+
"$ref": "collection:current"
|
|
10097
|
+
},
|
|
10098
|
+
"backLabel": "← Torna ai libri"
|
|
10099
|
+
}
|
|
10100
|
+
}
|
|
10101
|
+
],
|
|
10102
|
+
"collection": {
|
|
10103
|
+
"source": "libri",
|
|
10104
|
+
"paramKey": "slug"
|
|
10105
|
+
},
|
|
10106
|
+
"global-header": false
|
|
10107
|
+
}
|
|
10108
|
+
END_OF_FILE_CONTENT
|
|
8899
10109
|
mkdir -p "src/emails"
|
|
8900
10110
|
echo "Creating src/emails/LeadNotificationEmail.tsx..."
|
|
8901
10111
|
cat << 'END_OF_FILE_CONTENT' > "src/emails/LeadNotificationEmail.tsx"
|
|
@@ -9213,12 +10423,19 @@ echo "Creating src/entry-ssg.tsx..."
|
|
|
9213
10423
|
cat << 'END_OF_FILE_CONTENT' > "src/entry-ssg.tsx"
|
|
9214
10424
|
import { renderToString } from 'react-dom/server';
|
|
9215
10425
|
import { StaticRouter } from 'react-router-dom/server';
|
|
9216
|
-
import {
|
|
10426
|
+
import {
|
|
10427
|
+
ConfigProvider,
|
|
10428
|
+
PageRenderer,
|
|
10429
|
+
StudioProvider,
|
|
10430
|
+
contract,
|
|
10431
|
+
resolvePageMatchFromRegistry,
|
|
10432
|
+
resolveRuntimeConfig,
|
|
10433
|
+
} from '@olonjs/core';
|
|
9217
10434
|
import type { JsonPagesConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
9218
10435
|
import { ThemeProvider } from '@/components/ThemeProvider';
|
|
9219
10436
|
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
9220
10437
|
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
9221
|
-
import { menuConfig, pages, refDocuments, siteConfig, themeConfig } from '@/runtime';
|
|
10438
|
+
import { collectionSchemas, collections, menuConfig, pages, refDocuments, siteConfig, themeConfig } from '@/runtime';
|
|
9222
10439
|
import tenantCss from '@/index.css?inline';
|
|
9223
10440
|
|
|
9224
10441
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -9233,10 +10450,16 @@ function getSortedSlugs(): string[] {
|
|
|
9233
10450
|
return Object.keys(pages).sort((a, b) => a.localeCompare(b));
|
|
9234
10451
|
}
|
|
9235
10452
|
|
|
9236
|
-
function resolvePage(slug: string): { slug: string; page: PageConfig } {
|
|
10453
|
+
function resolvePage(slug: string): { slug: string; registrySlug: string; page: PageConfig; params: Record<string, string> } {
|
|
9237
10454
|
const normalized = normalizeSlug(slug);
|
|
9238
|
-
|
|
9239
|
-
|
|
10455
|
+
const pageMatch = resolvePageMatchFromRegistry(pages, normalized);
|
|
10456
|
+
if (pageMatch) {
|
|
10457
|
+
return {
|
|
10458
|
+
slug: normalized || pageMatch.registrySlug,
|
|
10459
|
+
registrySlug: pageMatch.registrySlug,
|
|
10460
|
+
page: pageMatch.page,
|
|
10461
|
+
params: pageMatch.params,
|
|
10462
|
+
};
|
|
9240
10463
|
}
|
|
9241
10464
|
|
|
9242
10465
|
const slugs = getSortedSlugs();
|
|
@@ -9246,7 +10469,7 @@ function resolvePage(slug: string): { slug: string; page: PageConfig } {
|
|
|
9246
10469
|
|
|
9247
10470
|
const home = slugs.find((item) => item === 'home');
|
|
9248
10471
|
const fallbackSlug = home ?? slugs[0];
|
|
9249
|
-
return { slug: fallbackSlug, page: pages[fallbackSlug] };
|
|
10472
|
+
return { slug: fallbackSlug, registrySlug: fallbackSlug, page: pages[fallbackSlug], params: {} };
|
|
9250
10473
|
}
|
|
9251
10474
|
|
|
9252
10475
|
function flattenThemeTokens(
|
|
@@ -9334,14 +10557,18 @@ function resolveTenantId(): string {
|
|
|
9334
10557
|
export function render(slug: string): string {
|
|
9335
10558
|
const resolved = resolvePage(slug);
|
|
9336
10559
|
const location = resolved.slug === 'home' ? '/' : `/${resolved.slug}`;
|
|
10560
|
+
const collectionContext = contract.resolveCollectionContext(resolved.page, resolved.params, collections);
|
|
9337
10561
|
const resolvedRuntime = resolveRuntimeConfig({
|
|
9338
|
-
pages,
|
|
10562
|
+
pages: { [resolved.registrySlug]: resolved.page },
|
|
9339
10563
|
siteConfig,
|
|
9340
10564
|
themeConfig,
|
|
9341
10565
|
menuConfig,
|
|
10566
|
+
collections,
|
|
10567
|
+
collectionSchemas,
|
|
10568
|
+
collectionContext,
|
|
9342
10569
|
refDocuments,
|
|
9343
10570
|
});
|
|
9344
|
-
const resolvedPage = resolvedRuntime.pages[resolved.
|
|
10571
|
+
const resolvedPage = resolvedRuntime.pages[resolved.registrySlug] ?? resolved.page;
|
|
9345
10572
|
|
|
9346
10573
|
return renderToString(
|
|
9347
10574
|
<StaticRouter location={location}>
|
|
@@ -9391,12 +10618,22 @@ export function getPageMeta(slug: string): { title: string; description: string
|
|
|
9391
10618
|
export function getWebMcpBuildState(): {
|
|
9392
10619
|
pages: Record<string, PageConfig>;
|
|
9393
10620
|
schemas: JsonPagesConfig['schemas'];
|
|
10621
|
+
collections: JsonPagesConfig['collections'];
|
|
10622
|
+
collectionSchemas: JsonPagesConfig['collectionSchemas'];
|
|
9394
10623
|
siteConfig: SiteConfig;
|
|
10624
|
+
themeConfig: ThemeConfig;
|
|
10625
|
+
menuConfig: JsonPagesConfig['menuConfig'];
|
|
10626
|
+
refDocuments: JsonPagesConfig['refDocuments'];
|
|
9395
10627
|
} {
|
|
9396
10628
|
return {
|
|
9397
10629
|
pages,
|
|
9398
10630
|
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
10631
|
+
collections,
|
|
10632
|
+
collectionSchemas,
|
|
9399
10633
|
siteConfig,
|
|
10634
|
+
themeConfig,
|
|
10635
|
+
menuConfig,
|
|
10636
|
+
refDocuments,
|
|
9400
10637
|
};
|
|
9401
10638
|
}
|
|
9402
10639
|
|
|
@@ -10344,17 +11581,38 @@ cat << 'END_OF_FILE_CONTENT' > "src/index.css"
|
|
|
10344
11581
|
|
|
10345
11582
|
END_OF_FILE_CONTENT
|
|
10346
11583
|
mkdir -p "src/lib"
|
|
11584
|
+
echo "Creating src/lib/CollectionRegistry.ts..."
|
|
11585
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/CollectionRegistry.ts"
|
|
11586
|
+
import { AutoriCollectionSchema } from '@/collections/autori';
|
|
11587
|
+
import { LibriCollectionSchema } from '@/collections/libri';
|
|
11588
|
+
|
|
11589
|
+
export const CollectionRegistry = {
|
|
11590
|
+
autori: AutoriCollectionSchema,
|
|
11591
|
+
libri: LibriCollectionSchema,
|
|
11592
|
+
} as const;
|
|
11593
|
+
|
|
11594
|
+
export type CollectionType = keyof typeof CollectionRegistry;
|
|
11595
|
+
|
|
11596
|
+
END_OF_FILE_CONTENT
|
|
10347
11597
|
echo "Creating src/lib/ComponentRegistry.tsx..."
|
|
10348
11598
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/ComponentRegistry.tsx"
|
|
10349
11599
|
import type { SectionType } from '@/types';
|
|
10350
11600
|
import type { SectionComponentPropsMap } from '@/types';
|
|
11601
|
+
import { AuthorsListView } from '@/components/authors-list';
|
|
11602
|
+
import { BookDetailView } from '@/components/book-detail';
|
|
11603
|
+
import { BooksListView } from '@/components/books-list';
|
|
10351
11604
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
11605
|
+
import { FooterView } from '@/components/footer';
|
|
10352
11606
|
import { FormDemoView } from '@/components/form-demo';
|
|
10353
11607
|
|
|
10354
11608
|
export const ComponentRegistry: {
|
|
10355
11609
|
[K in SectionType]: React.FC<SectionComponentPropsMap[K]>;
|
|
10356
11610
|
} = {
|
|
11611
|
+
'authors-list': AuthorsListView as React.FC<SectionComponentPropsMap['authors-list']>,
|
|
11612
|
+
'book-detail': BookDetailView as React.FC<SectionComponentPropsMap['book-detail']>,
|
|
11613
|
+
'books-list': BooksListView as React.FC<SectionComponentPropsMap['books-list']>,
|
|
10357
11614
|
'empty-tenant': EmptyTenantView as React.FC<SectionComponentPropsMap['empty-tenant']>,
|
|
11615
|
+
footer: FooterView as React.FC<SectionComponentPropsMap['footer']>,
|
|
10358
11616
|
'form-demo': FormDemoView as React.FC<SectionComponentPropsMap['form-demo']>,
|
|
10359
11617
|
};
|
|
10360
11618
|
|
|
@@ -10423,20 +11681,52 @@ echo "Creating src/lib/addSectionConfig.ts..."
|
|
|
10423
11681
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/addSectionConfig.ts"
|
|
10424
11682
|
import type { AddSectionConfig } from '@olonjs/core';
|
|
10425
11683
|
|
|
10426
|
-
const addableSectionTypes = ['empty-tenant', 'form-demo'] as const;
|
|
11684
|
+
const addableSectionTypes = ['authors-list', 'book-detail', 'books-list', 'empty-tenant', 'footer', 'form-demo'] as const;
|
|
10427
11685
|
|
|
10428
11686
|
const sectionTypeLabels: Record<string, string> = {
|
|
11687
|
+
'authors-list': 'Authors List',
|
|
11688
|
+
'book-detail': 'Book Detail',
|
|
11689
|
+
'books-list': 'Books List',
|
|
10429
11690
|
'empty-tenant': 'Empty Tenant',
|
|
11691
|
+
footer: 'Footer',
|
|
10430
11692
|
'form-demo': 'Form Demo',
|
|
10431
11693
|
};
|
|
10432
11694
|
|
|
10433
11695
|
function getDefaultSectionData(type: string): Record<string, unknown> {
|
|
10434
11696
|
switch (type) {
|
|
11697
|
+
case 'authors-list':
|
|
11698
|
+
return {
|
|
11699
|
+
eyebrow: 'Collection demo',
|
|
11700
|
+
title: 'Authors',
|
|
11701
|
+
description: 'Authors loaded from the autori collection.',
|
|
11702
|
+
items: { $ref: '../collections/autori/autori.json' },
|
|
11703
|
+
};
|
|
11704
|
+
case 'book-detail':
|
|
11705
|
+
return {
|
|
11706
|
+
item: { $ref: 'collection:current' },
|
|
11707
|
+
backLabel: 'Torna ai libri',
|
|
11708
|
+
};
|
|
11709
|
+
case 'books-list':
|
|
11710
|
+
return {
|
|
11711
|
+
eyebrow: 'Collection demo',
|
|
11712
|
+
title: 'Libri',
|
|
11713
|
+
description: 'Catalogo dimostrativo alimentato dalla collection libri.',
|
|
11714
|
+
items: { $ref: '../collections/libri/libri.json' },
|
|
11715
|
+
pageSize: 10,
|
|
11716
|
+
};
|
|
10435
11717
|
case 'empty-tenant':
|
|
10436
11718
|
return {
|
|
10437
11719
|
title: 'Your tenant is empty.',
|
|
10438
11720
|
description: 'Create your first page to start building your site.',
|
|
10439
11721
|
};
|
|
11722
|
+
case 'footer':
|
|
11723
|
+
return {
|
|
11724
|
+
brandText: 'OlonJS',
|
|
11725
|
+
description: 'AI-native content infrastructure for deterministic, git-backed sites.',
|
|
11726
|
+
copyright: '© 2026 OlonJS',
|
|
11727
|
+
links: [],
|
|
11728
|
+
designSystemHref: '',
|
|
11729
|
+
};
|
|
10440
11730
|
case 'form-demo':
|
|
10441
11731
|
return {
|
|
10442
11732
|
title: 'Contattaci',
|
|
@@ -10484,6 +11774,49 @@ export function getHydratedData(
|
|
|
10484
11774
|
};
|
|
10485
11775
|
}
|
|
10486
11776
|
|
|
11777
|
+
END_OF_FILE_CONTENT
|
|
11778
|
+
echo "Creating src/lib/getFileCollections.ts..."
|
|
11779
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/getFileCollections.ts"
|
|
11780
|
+
import type { JsonPagesConfig } from '@/types';
|
|
11781
|
+
|
|
11782
|
+
type CollectionDocuments = NonNullable<JsonPagesConfig['collections']>;
|
|
11783
|
+
|
|
11784
|
+
function collectionSourceFromPath(filePath: string): string | null {
|
|
11785
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
11786
|
+
const match = normalizedPath.match(/\/data\/collections\/([^/]+)\/[^/]+\.json$/i);
|
|
11787
|
+
return match?.[1]?.trim() || null;
|
|
11788
|
+
}
|
|
11789
|
+
|
|
11790
|
+
export function getFileCollections(): CollectionDocuments {
|
|
11791
|
+
const glob = import.meta.glob<{ default: unknown }>('@/data/collections/**/*.json', { eager: true });
|
|
11792
|
+
const bySource = new Map<string, Record<string, unknown>>();
|
|
11793
|
+
const entries = Object.entries(glob).sort(([a], [b]) => a.localeCompare(b));
|
|
11794
|
+
|
|
11795
|
+
for (const [path, mod] of entries) {
|
|
11796
|
+
const source = collectionSourceFromPath(path);
|
|
11797
|
+
const raw = mod?.default;
|
|
11798
|
+
if (!source) {
|
|
11799
|
+
console.warn(`[tenant-alpha:getFileCollections] Ignoring collection module with invalid path "${path}".`);
|
|
11800
|
+
continue;
|
|
11801
|
+
}
|
|
11802
|
+
if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
11803
|
+
console.warn(`[tenant-alpha:getFileCollections] Ignoring invalid collection module at "${path}".`);
|
|
11804
|
+
continue;
|
|
11805
|
+
}
|
|
11806
|
+
if (bySource.has(source)) {
|
|
11807
|
+
console.warn(`[tenant-alpha:getFileCollections] Duplicate collection source "${source}" at "${path}". Keeping latest match.`);
|
|
11808
|
+
}
|
|
11809
|
+
bySource.set(source, raw as Record<string, unknown>);
|
|
11810
|
+
}
|
|
11811
|
+
|
|
11812
|
+
const collections: CollectionDocuments = {};
|
|
11813
|
+
for (const source of Array.from(bySource.keys()).sort((a, b) => a.localeCompare(b))) {
|
|
11814
|
+
const collection = bySource.get(source);
|
|
11815
|
+
if (collection) collections[source] = collection;
|
|
11816
|
+
}
|
|
11817
|
+
return collections;
|
|
11818
|
+
}
|
|
11819
|
+
|
|
10487
11820
|
END_OF_FILE_CONTENT
|
|
10488
11821
|
echo "Creating src/lib/getFilePages.ts..."
|
|
10489
11822
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/getFilePages.ts"
|
|
@@ -10535,11 +11868,19 @@ export function getFilePages(): Record<string, PageConfig> {
|
|
|
10535
11868
|
END_OF_FILE_CONTENT
|
|
10536
11869
|
echo "Creating src/lib/schemas.ts..."
|
|
10537
11870
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
|
|
11871
|
+
import { AuthorsListSchema } from '@/components/authors-list';
|
|
11872
|
+
import { BookDetailSchema } from '@/components/book-detail';
|
|
11873
|
+
import { BooksListSchema } from '@/components/books-list';
|
|
10538
11874
|
import { EmptyTenantSchema } from '@/components/empty-tenant';
|
|
11875
|
+
import { FooterSchema } from '@/components/footer';
|
|
10539
11876
|
import { FormDemoSchema, FormDemoSubmissionSchema } from '@/components/form-demo';
|
|
10540
11877
|
|
|
10541
11878
|
export const SECTION_SCHEMAS = {
|
|
11879
|
+
'authors-list': AuthorsListSchema,
|
|
11880
|
+
'book-detail': BookDetailSchema,
|
|
11881
|
+
'books-list': BooksListSchema,
|
|
10542
11882
|
'empty-tenant': EmptyTenantSchema,
|
|
11883
|
+
footer: FooterSchema,
|
|
10543
11884
|
'form-demo': FormDemoSchema,
|
|
10544
11885
|
} as const;
|
|
10545
11886
|
|
|
@@ -10559,6 +11900,7 @@ export type SectionType = keyof typeof SECTION_SCHEMAS;
|
|
|
10559
11900
|
export {
|
|
10560
11901
|
BaseSectionData,
|
|
10561
11902
|
BaseArrayItem,
|
|
11903
|
+
BaseCollectionItem,
|
|
10562
11904
|
BaseSectionSettingsSchema,
|
|
10563
11905
|
CtaSchema,
|
|
10564
11906
|
ImageSelectionSchema,
|
|
@@ -10817,7 +12159,9 @@ END_OF_FILE_CONTENT
|
|
|
10817
12159
|
echo "Creating src/runtime.ts..."
|
|
10818
12160
|
cat << 'END_OF_FILE_CONTENT' > "src/runtime.ts"
|
|
10819
12161
|
import type { JsonPagesConfig, MenuConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
12162
|
+
import { CollectionRegistry } from '@/lib/CollectionRegistry';
|
|
10820
12163
|
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
12164
|
+
import { getFileCollections } from '@/lib/getFileCollections';
|
|
10821
12165
|
import { getFilePages } from '@/lib/getFilePages';
|
|
10822
12166
|
import siteData from '@/data/config/site.json';
|
|
10823
12167
|
import menuData from '@/data/config/menu.json';
|
|
@@ -10827,6 +12171,8 @@ export const siteConfig = siteData as unknown as SiteConfig;
|
|
|
10827
12171
|
export const themeConfig = themeData as unknown as ThemeConfig;
|
|
10828
12172
|
export const menuConfig = menuData as unknown as MenuConfig;
|
|
10829
12173
|
export const pages = getFilePages();
|
|
12174
|
+
export const collections = getFileCollections();
|
|
12175
|
+
export const collectionSchemas = CollectionRegistry as unknown as JsonPagesConfig['collectionSchemas'];
|
|
10830
12176
|
export const refDocuments = {
|
|
10831
12177
|
'menu.json': menuConfig,
|
|
10832
12178
|
'config/menu.json': menuConfig,
|
|
@@ -10836,12 +12182,22 @@ export const refDocuments = {
|
|
|
10836
12182
|
export function getWebMcpBuildState(): {
|
|
10837
12183
|
pages: Record<string, PageConfig>;
|
|
10838
12184
|
schemas: JsonPagesConfig['schemas'];
|
|
12185
|
+
collectionSchemas: JsonPagesConfig['collectionSchemas'];
|
|
12186
|
+
collections: JsonPagesConfig['collections'];
|
|
10839
12187
|
siteConfig: SiteConfig;
|
|
12188
|
+
themeConfig: ThemeConfig;
|
|
12189
|
+
menuConfig: MenuConfig;
|
|
12190
|
+
refDocuments: JsonPagesConfig['refDocuments'];
|
|
10840
12191
|
} {
|
|
10841
12192
|
return {
|
|
10842
12193
|
pages,
|
|
10843
12194
|
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
12195
|
+
collectionSchemas,
|
|
12196
|
+
collections,
|
|
10844
12197
|
siteConfig,
|
|
12198
|
+
themeConfig,
|
|
12199
|
+
menuConfig,
|
|
12200
|
+
refDocuments,
|
|
10845
12201
|
};
|
|
10846
12202
|
}
|
|
10847
12203
|
|
|
@@ -10849,23 +12205,45 @@ END_OF_FILE_CONTENT
|
|
|
10849
12205
|
# SKIP: src/runtime.ts:Zone.Identifier is binary and cannot be embedded as text.
|
|
10850
12206
|
echo "Creating src/types.ts..."
|
|
10851
12207
|
cat << 'END_OF_FILE_CONTENT' > "src/types.ts"
|
|
12208
|
+
import type { AuthorsListData, AuthorsListSettings } from '@/components/authors-list';
|
|
12209
|
+
import type { BookDetailData, BookDetailSettings } from '@/components/book-detail';
|
|
12210
|
+
import type { BooksListData, BooksListSettings } from '@/components/books-list';
|
|
10852
12211
|
import type { EmptyTenantData, EmptyTenantSettings } from '@/components/empty-tenant';
|
|
12212
|
+
import type { FooterData, FooterSettings } from '@/components/footer';
|
|
10853
12213
|
import type { FormDemoData, FormDemoSettings } from '@/components/form-demo';
|
|
12214
|
+
import type { Autore } from '@/collections/autori';
|
|
12215
|
+
import type { Libro } from '@/collections/libri';
|
|
10854
12216
|
|
|
10855
12217
|
export type SectionComponentPropsMap = {
|
|
12218
|
+
'authors-list': { data: AuthorsListData; settings?: AuthorsListSettings };
|
|
12219
|
+
'book-detail': { data: BookDetailData; settings?: BookDetailSettings };
|
|
12220
|
+
'books-list': { data: BooksListData; settings?: BooksListSettings };
|
|
10856
12221
|
'empty-tenant': { data: EmptyTenantData; settings?: EmptyTenantSettings };
|
|
12222
|
+
footer: { data: FooterData; settings?: FooterSettings };
|
|
10857
12223
|
'form-demo': { data: FormDemoData; settings?: FormDemoSettings };
|
|
10858
12224
|
};
|
|
10859
12225
|
|
|
10860
12226
|
declare module '@olonjs/core' {
|
|
10861
12227
|
export interface SectionDataRegistry {
|
|
12228
|
+
'authors-list': AuthorsListData;
|
|
12229
|
+
'book-detail': BookDetailData;
|
|
12230
|
+
'books-list': BooksListData;
|
|
10862
12231
|
'empty-tenant': EmptyTenantData;
|
|
12232
|
+
footer: FooterData;
|
|
10863
12233
|
'form-demo': FormDemoData;
|
|
10864
12234
|
}
|
|
10865
12235
|
export interface SectionSettingsRegistry {
|
|
12236
|
+
'authors-list': AuthorsListSettings;
|
|
12237
|
+
'book-detail': BookDetailSettings;
|
|
12238
|
+
'books-list': BooksListSettings;
|
|
10866
12239
|
'empty-tenant': EmptyTenantSettings;
|
|
12240
|
+
footer: FooterSettings;
|
|
10867
12241
|
'form-demo': FormDemoSettings;
|
|
10868
12242
|
}
|
|
12243
|
+
export interface CollectionItemRegistry {
|
|
12244
|
+
autori: Autore;
|
|
12245
|
+
libri: Libro;
|
|
12246
|
+
}
|
|
10869
12247
|
}
|
|
10870
12248
|
|
|
10871
12249
|
export * from '@olonjs/core';
|
|
@@ -11027,6 +12405,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
11027
12405
|
const ASSETS_IMAGES_DIR = path.resolve(__dirname, 'public', 'assets', 'images');
|
|
11028
12406
|
const DATA_CONFIG_DIR = path.resolve(__dirname, 'src', 'data', 'config');
|
|
11029
12407
|
const DATA_PAGES_DIR = path.resolve(__dirname, 'src', 'data', 'pages');
|
|
12408
|
+
const DATA_COLLECTIONS_DIR = path.resolve(__dirname, 'src', 'data', 'collections');
|
|
11030
12409
|
const IMAGE_EXT = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif']);
|
|
11031
12410
|
const IMAGE_MIMES = new Set([
|
|
11032
12411
|
'image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml', 'image/avif',
|
|
@@ -11063,6 +12442,20 @@ function sendJsonFile(res, filePath) {
|
|
|
11063
12442
|
}
|
|
11064
12443
|
}
|
|
11065
12444
|
|
|
12445
|
+
function safeDataSlugPath(rootDir, rawSlug, fallback) {
|
|
12446
|
+
const slug = String(rawSlug || fallback)
|
|
12447
|
+
.replace(/\\/g, '/')
|
|
12448
|
+
.replace(/^\/+|\/+$/g, '');
|
|
12449
|
+
const segments = slug
|
|
12450
|
+
.split('/')
|
|
12451
|
+
.map((segment) => segment.replace(/[^a-zA-Z0-9_[\]-]/g, '_'))
|
|
12452
|
+
.filter(Boolean);
|
|
12453
|
+
const candidate = path.resolve(rootDir, `${segments.join(path.sep) || fallback}.json`);
|
|
12454
|
+
const isInsideRoot = candidate.startsWith(`${rootDir}${path.sep}`) || candidate === rootDir;
|
|
12455
|
+
if (!isInsideRoot) throw new Error('Invalid data path');
|
|
12456
|
+
return candidate;
|
|
12457
|
+
}
|
|
12458
|
+
|
|
11066
12459
|
function isTenantPageJsonRequest(req, pathname) {
|
|
11067
12460
|
if (req.method !== 'GET' || !pathname.endsWith('.json')) return false;
|
|
11068
12461
|
const viteOrStaticPrefixes = ['/api/', '/assets/', '/src/', '/node_modules/', '/public/', '/@'];
|
|
@@ -11120,15 +12513,23 @@ export default defineConfig({
|
|
|
11120
12513
|
const pageManifestMatch = pathname.match(/^\/mcp-manifests\/(.+)\.json$/i);
|
|
11121
12514
|
if (pageManifestMatch && req.method === 'GET') {
|
|
11122
12515
|
const slug = normalizeManifestSlug(pageManifestMatch[1]);
|
|
11123
|
-
const
|
|
11124
|
-
|
|
12516
|
+
const resolved = core.resolvePublicPageDocument({
|
|
12517
|
+
slug,
|
|
12518
|
+
pages: buildState.pages,
|
|
12519
|
+
siteConfig: buildState.siteConfig,
|
|
12520
|
+
themeConfig: buildState.themeConfig,
|
|
12521
|
+
menuConfig: buildState.menuConfig,
|
|
12522
|
+
collections: buildState.collections,
|
|
12523
|
+
refDocuments: buildState.refDocuments,
|
|
12524
|
+
});
|
|
12525
|
+
if (!resolved) {
|
|
11125
12526
|
sendJson(res, 404, { error: 'Page manifest not found' });
|
|
11126
12527
|
return true;
|
|
11127
12528
|
}
|
|
11128
12529
|
|
|
11129
12530
|
sendJson(res, 200, buildPageManifest({
|
|
11130
12531
|
slug,
|
|
11131
|
-
pageConfig,
|
|
12532
|
+
pageConfig: resolved.page,
|
|
11132
12533
|
schemas: buildState.schemas,
|
|
11133
12534
|
submissionSchemas: buildState.submissionSchemas,
|
|
11134
12535
|
siteConfig: buildState.siteConfig,
|
|
@@ -11139,15 +12540,23 @@ export default defineConfig({
|
|
|
11139
12540
|
const schemaMatch = pathname.match(/^\/schemas\/(.+)\.schema\.json$/i);
|
|
11140
12541
|
if (schemaMatch && req.method === 'GET') {
|
|
11141
12542
|
const slug = normalizeManifestSlug(schemaMatch[1]);
|
|
11142
|
-
const
|
|
11143
|
-
|
|
12543
|
+
const resolved = core.resolvePublicPageDocument({
|
|
12544
|
+
slug,
|
|
12545
|
+
pages: buildState.pages,
|
|
12546
|
+
siteConfig: buildState.siteConfig,
|
|
12547
|
+
themeConfig: buildState.themeConfig,
|
|
12548
|
+
menuConfig: buildState.menuConfig,
|
|
12549
|
+
collections: buildState.collections,
|
|
12550
|
+
refDocuments: buildState.refDocuments,
|
|
12551
|
+
});
|
|
12552
|
+
if (!resolved) {
|
|
11144
12553
|
sendJson(res, 404, { error: 'Schema contract not found' });
|
|
11145
12554
|
return true;
|
|
11146
12555
|
}
|
|
11147
12556
|
|
|
11148
12557
|
sendJson(res, 200, buildPageContract({
|
|
11149
12558
|
slug,
|
|
11150
|
-
pageConfig,
|
|
12559
|
+
pageConfig: resolved.page,
|
|
11151
12560
|
schemas: buildState.schemas,
|
|
11152
12561
|
submissionSchemas: buildState.submissionSchemas,
|
|
11153
12562
|
siteConfig: buildState.siteConfig,
|
|
@@ -11188,13 +12597,31 @@ export default defineConfig({
|
|
|
11188
12597
|
.replace(/^pages\//i, '')
|
|
11189
12598
|
.replace(/\.json$/i, '')
|
|
11190
12599
|
.replace(/^\/+|\/+$/g, '');
|
|
11191
|
-
|
|
11192
|
-
const isInsidePagesDir = candidate.startsWith(`${DATA_PAGES_DIR}${path.sep}`) || candidate === DATA_PAGES_DIR;
|
|
11193
|
-
if (!slug || !isInsidePagesDir || !fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
|
|
12600
|
+
if (!slug) {
|
|
11194
12601
|
sendJson(res, 404, { error: 'Page JSON not found' });
|
|
11195
12602
|
return;
|
|
11196
12603
|
}
|
|
11197
|
-
|
|
12604
|
+
void (async () => {
|
|
12605
|
+
const core = await loadWebMcpBuilders();
|
|
12606
|
+
const runtime = await server.ssrLoadModule('/src/runtime.ts');
|
|
12607
|
+
const buildState = runtime.getWebMcpBuildState();
|
|
12608
|
+
const resolved = core.resolvePublicPageDocument({
|
|
12609
|
+
slug,
|
|
12610
|
+
pages: buildState.pages,
|
|
12611
|
+
siteConfig: buildState.siteConfig,
|
|
12612
|
+
themeConfig: buildState.themeConfig,
|
|
12613
|
+
menuConfig: buildState.menuConfig,
|
|
12614
|
+
collections: buildState.collections,
|
|
12615
|
+
refDocuments: buildState.refDocuments,
|
|
12616
|
+
});
|
|
12617
|
+
if (!resolved) {
|
|
12618
|
+
sendJson(res, 404, { error: 'Page JSON not found' });
|
|
12619
|
+
return;
|
|
12620
|
+
}
|
|
12621
|
+
sendJson(res, 200, resolved.page);
|
|
12622
|
+
})().catch((error) => {
|
|
12623
|
+
sendJson(res, 500, { error: error?.message || 'Page JSON resolution failed' });
|
|
12624
|
+
});
|
|
11198
12625
|
return;
|
|
11199
12626
|
}
|
|
11200
12627
|
if (req.method === 'GET' && req.url === '/api/list-assets') {
|
|
@@ -11213,12 +12640,23 @@ export default defineConfig({
|
|
|
11213
12640
|
if (!projectState || typeof slug !== 'string') { sendJson(res, 400, { error: 'Missing projectState or slug' }); return; }
|
|
11214
12641
|
if (!fs.existsSync(DATA_CONFIG_DIR)) fs.mkdirSync(DATA_CONFIG_DIR, { recursive: true });
|
|
11215
12642
|
if (!fs.existsSync(DATA_PAGES_DIR)) fs.mkdirSync(DATA_PAGES_DIR, { recursive: true });
|
|
12643
|
+
if (!fs.existsSync(DATA_COLLECTIONS_DIR)) fs.mkdirSync(DATA_COLLECTIONS_DIR, { recursive: true });
|
|
11216
12644
|
if (projectState.site != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'site.json'), JSON.stringify(projectState.site, null, 2), 'utf8');
|
|
11217
12645
|
if (projectState.menu != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'menu.json'), JSON.stringify(projectState.menu, null, 2), 'utf8');
|
|
11218
12646
|
if (projectState.theme != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'theme.json'), JSON.stringify(projectState.theme, null, 2), 'utf8');
|
|
11219
12647
|
if (projectState.page != null) {
|
|
11220
|
-
const
|
|
11221
|
-
fs.
|
|
12648
|
+
const pagePath = safeDataSlugPath(DATA_PAGES_DIR, slug, 'page');
|
|
12649
|
+
fs.mkdirSync(path.dirname(pagePath), { recursive: true });
|
|
12650
|
+
fs.writeFileSync(pagePath, JSON.stringify(projectState.page, null, 2), 'utf8');
|
|
12651
|
+
}
|
|
12652
|
+
if (projectState.collections && typeof projectState.collections === 'object') {
|
|
12653
|
+
for (const [source, collection] of Object.entries(projectState.collections)) {
|
|
12654
|
+
const sourceSlug = String(source).replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
12655
|
+
if (!sourceSlug) continue;
|
|
12656
|
+
const collectionDir = path.join(DATA_COLLECTIONS_DIR, sourceSlug);
|
|
12657
|
+
fs.mkdirSync(collectionDir, { recursive: true });
|
|
12658
|
+
fs.writeFileSync(path.join(collectionDir, `${sourceSlug}.json`), JSON.stringify(collection, null, 2), 'utf8');
|
|
12659
|
+
}
|
|
11222
12660
|
}
|
|
11223
12661
|
sendJson(res, 200, { ok: true });
|
|
11224
12662
|
} catch (e) { sendJson(res, 500, { error: e?.message || 'Save to file failed' }); }
|