@fullgrainsoftware/portal 0.9.0
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/LICENSE +21 -0
- package/README.md +242 -0
- package/bin/fullgrain.mjs +3 -0
- package/dist/bookings.d.ts +169 -0
- package/dist/bookings.js +96 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +116 -0
- package/dist/cms-client.d.ts +9 -0
- package/dist/cms-client.js +38 -0
- package/dist/cms-types.d.ts +99 -0
- package/dist/cms-types.js +1 -0
- package/dist/commerce.d.ts +111 -0
- package/dist/commerce.js +91 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +8 -0
- package/dist/inquiries.d.ts +38 -0
- package/dist/inquiries.js +52 -0
- package/dist/manifest.d.ts +297 -0
- package/dist/manifest.js +102 -0
- package/dist/preview.d.ts +49 -0
- package/dist/preview.js +165 -0
- package/dist/reader.d.ts +23 -0
- package/dist/reader.js +82 -0
- package/dist/reviews.d.ts +2 -0
- package/dist/reviews.js +13 -0
- package/package.json +50 -0
package/dist/reader.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const toKey = (value) => value.trim().toLowerCase();
|
|
2
|
+
function findSection(page, title) {
|
|
3
|
+
return page?.sections?.find((s) => toKey(s.title) === toKey(title));
|
|
4
|
+
}
|
|
5
|
+
function groupValues(section, group) {
|
|
6
|
+
const out = {};
|
|
7
|
+
for (const [fieldKey, field] of Object.entries(section.fields)) {
|
|
8
|
+
const component = group.components?.find((c) => toKey(c.key) === toKey(field.label));
|
|
9
|
+
// A component that exists is authoritative — including when an editor has
|
|
10
|
+
// deliberately cleared it to "". Only fall back to the manifest default
|
|
11
|
+
// when there is no component at all (never synced), so a cleared field
|
|
12
|
+
// stays cleared instead of resurrecting its default on every render.
|
|
13
|
+
out[fieldKey] = component ? component.value?.trim() ?? "" : field.default ?? "";
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
function seedValues(section) {
|
|
18
|
+
return (section.seed ?? []).map((item) => {
|
|
19
|
+
const out = {};
|
|
20
|
+
for (const [fieldKey, field] of Object.entries(section.fields)) {
|
|
21
|
+
out[fieldKey] = item.values[fieldKey] ?? field.default ?? "";
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function defaultValues(section) {
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const [fieldKey, field] of Object.entries(section.fields)) {
|
|
29
|
+
out[fieldKey] = field.default ?? "";
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
const withMeta = (values, id) => Object.assign({}, values, { _cms: { id } });
|
|
34
|
+
function readSection(section, cmsPage) {
|
|
35
|
+
const cmsSection = findSection(cmsPage, section.title);
|
|
36
|
+
// The section exists in the CMS => it has been synced. An emptied synced
|
|
37
|
+
// section is genuinely empty; the seed must NOT reappear (that resurrects
|
|
38
|
+
// deleted content on the live site). Seed only a section that was never
|
|
39
|
+
// synced (no matching CMS section at all).
|
|
40
|
+
const synced = cmsSection !== undefined;
|
|
41
|
+
const groups = [...(cmsSection?.groups ?? [])].sort((a, b) => a.sort_order - b.sort_order ||
|
|
42
|
+
(a.created_at ?? "").localeCompare(b.created_at ?? ""));
|
|
43
|
+
if (section.kind === "repeatable") {
|
|
44
|
+
if (groups.length)
|
|
45
|
+
return groups.map((g) => withMeta(groupValues(section, g), g.id));
|
|
46
|
+
return synced ? [] : seedValues(section).map((v) => withMeta(v, ""));
|
|
47
|
+
}
|
|
48
|
+
if (groups.length)
|
|
49
|
+
return withMeta(groupValues(section, groups[0]), groups[0].id);
|
|
50
|
+
if (synced)
|
|
51
|
+
return withMeta(defaultValues(section), "");
|
|
52
|
+
const seeded = seedValues(section);
|
|
53
|
+
return withMeta(seeded[0] ?? defaultValues(section), "");
|
|
54
|
+
}
|
|
55
|
+
export function readSiteContent(manifest, website) {
|
|
56
|
+
const result = {};
|
|
57
|
+
const seenKeys = new Set();
|
|
58
|
+
for (const page of manifest.pages) {
|
|
59
|
+
const cmsPage = website?.pages?.find((p) => p.slug === page.slug);
|
|
60
|
+
for (const section of page.sections) {
|
|
61
|
+
// Section keys are the content object's property names; a duplicate would
|
|
62
|
+
// silently clobber one section. defineSite does no runtime validation and
|
|
63
|
+
// the Zod schema only runs in the sync CLI, so guard it here too.
|
|
64
|
+
if (seenKeys.has(section.key)) {
|
|
65
|
+
throw new Error(`Duplicate section key "${section.key}" in manifest — section keys must be unique across all pages.`);
|
|
66
|
+
}
|
|
67
|
+
seenKeys.add(section.key);
|
|
68
|
+
result[section.key] = readSection(section, cmsPage);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Spread onto an item's wrapper element to make it click-to-edit in the
|
|
75
|
+
* dashboard preview: `<div {...cmsItemProps(item)}>`. Returns {} for seed items
|
|
76
|
+
* (no CMS id yet), leaving them non-editable.
|
|
77
|
+
*/
|
|
78
|
+
export function cmsItemProps(item) {
|
|
79
|
+
return item._cms.id
|
|
80
|
+
? { "data-cms-editable": "", "data-cms-item": item._cms.id }
|
|
81
|
+
: {};
|
|
82
|
+
}
|
package/dist/reviews.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function isReview(value) {
|
|
2
|
+
if (!value || typeof value !== "object")
|
|
3
|
+
return false;
|
|
4
|
+
const r = value;
|
|
5
|
+
const ratingOk = r.rating === null || (typeof r.rating === "number" && Number.isInteger(r.rating) && r.rating >= 1 && r.rating <= 5);
|
|
6
|
+
return typeof r.id === "string" && typeof r.name === "string" && typeof r.text === "string" && typeof r.createdAt === "string" && ratingOk;
|
|
7
|
+
}
|
|
8
|
+
export function readReviews(website) {
|
|
9
|
+
const list = website?.reviews;
|
|
10
|
+
if (!Array.isArray(list))
|
|
11
|
+
return [];
|
|
12
|
+
return list.filter(isReview).map((r) => ({ id: r.id, name: r.name, rating: r.rating, text: r.text, createdAt: r.createdAt }));
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fullgrainsoftware/portal",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Fullgrain Portal site SDK: manifest, typed content reader, sync CLI, preview bridge, commerce, inquiries and bookings clients",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/fullgrainsoftware/fullgrain-portal-sdk.git"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"registry": "https://registry.npmjs.org",
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"fullgrain": "bin/fullgrain.mjs"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"bin",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.build.json",
|
|
37
|
+
"check": "tsc --noEmit",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"prepare": "npm run build"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"jiti": "^2.4.2",
|
|
43
|
+
"zod": "^3.25.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22",
|
|
47
|
+
"typescript": "^5.8.0",
|
|
48
|
+
"vitest": "^4.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|