@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/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
+ }
@@ -0,0 +1,2 @@
1
+ import type { CMSReview, CMSWebsite } from "./cms-types.js";
2
+ export declare function readReviews(website: CMSWebsite | null | undefined): CMSReview[];
@@ -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
+ }