domma-cms 0.1.0 → 0.2.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.
Files changed (87) hide show
  1. package/admin/css/admin.css +78 -1
  2. package/admin/js/api.js +32 -0
  3. package/admin/js/app.js +24 -7
  4. package/admin/js/config/sidebar-config.js +8 -0
  5. package/admin/js/templates/collection-editor.html +80 -0
  6. package/admin/js/templates/collection-entries.html +36 -0
  7. package/admin/js/templates/collections.html +12 -0
  8. package/admin/js/templates/documentation.html +136 -0
  9. package/admin/js/templates/navigation.html +26 -4
  10. package/admin/js/templates/page-editor.html +91 -85
  11. package/admin/js/templates/settings.html +433 -172
  12. package/admin/js/views/collection-editor.js +487 -0
  13. package/admin/js/views/collection-entries.js +484 -0
  14. package/admin/js/views/collections.js +153 -0
  15. package/admin/js/views/dashboard.js +14 -6
  16. package/admin/js/views/index.js +9 -3
  17. package/admin/js/views/login.js +3 -2
  18. package/admin/js/views/navigation.js +77 -11
  19. package/admin/js/views/page-editor.js +207 -25
  20. package/admin/js/views/pages.js +14 -6
  21. package/admin/js/views/settings.js +137 -2
  22. package/admin/js/views/users.js +10 -7
  23. package/bin/cli.js +37 -10
  24. package/config/auth.json +2 -1
  25. package/config/content.json +1 -0
  26. package/config/navigation.json +14 -4
  27. package/config/plugins.json +0 -18
  28. package/config/presets.json +4 -8
  29. package/config/site.json +44 -3
  30. package/package.json +6 -2
  31. package/plugins/domma-effects/admin/templates/domma-effects.html +92 -3
  32. package/plugins/domma-effects/plugin.js +125 -0
  33. package/plugins/domma-effects/public/inject-body.html +19 -0
  34. package/plugins/example-analytics/admin/views/analytics.js +2 -2
  35. package/plugins/example-analytics/plugin.json +8 -0
  36. package/plugins/example-analytics/stats.json +15 -1
  37. package/plugins/form-builder/admin/templates/form-editor.html +19 -6
  38. package/plugins/form-builder/admin/views/form-editor.js +634 -9
  39. package/plugins/form-builder/admin/views/form-submissions.js +4 -4
  40. package/plugins/form-builder/admin/views/forms-list.js +5 -5
  41. package/plugins/form-builder/data/forms/consent.json +104 -0
  42. package/plugins/form-builder/data/forms/contacts.json +66 -0
  43. package/plugins/form-builder/data/submissions/consent.json +13 -0
  44. package/plugins/form-builder/data/submissions/contacts.json +26 -0
  45. package/plugins/form-builder/plugin.js +62 -11
  46. package/plugins/form-builder/plugin.json +12 -16
  47. package/plugins/form-builder/public/form-logic-engine.js +568 -0
  48. package/plugins/form-builder/public/inject-body.html +88 -6
  49. package/plugins/form-builder/public/inject-head.html +16 -0
  50. package/plugins/form-builder/public/package.json +1 -0
  51. package/public/css/site.css +113 -0
  52. package/public/js/btt.js +90 -0
  53. package/public/js/cookie-consent.js +61 -0
  54. package/public/js/site.js +129 -34
  55. package/scripts/build.js +129 -0
  56. package/scripts/seed.js +517 -7
  57. package/server/routes/api/collections.js +301 -0
  58. package/server/routes/api/settings.js +66 -2
  59. package/server/server.js +19 -15
  60. package/server/services/collections.js +430 -0
  61. package/server/services/content.js +11 -2
  62. package/server/services/hooks.js +109 -0
  63. package/server/services/markdown.js +500 -149
  64. package/server/services/plugins.js +6 -1
  65. package/server/services/renderer.js +73 -7
  66. package/server/templates/page.html +38 -3
  67. package/plugins/back-to-top/admin/templates/back-to-top-settings.html +0 -55
  68. package/plugins/back-to-top/admin/views/back-to-top-settings.js +0 -44
  69. package/plugins/back-to-top/config.js +0 -10
  70. package/plugins/back-to-top/plugin.js +0 -24
  71. package/plugins/back-to-top/plugin.json +0 -36
  72. package/plugins/back-to-top/public/inject-body.html +0 -105
  73. package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +0 -113
  74. package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +0 -73
  75. package/plugins/cookie-consent/config.js +0 -30
  76. package/plugins/cookie-consent/plugin.js +0 -24
  77. package/plugins/cookie-consent/plugin.json +0 -36
  78. package/plugins/cookie-consent/public/inject-body.html +0 -69
  79. package/plugins/custom-css/admin/templates/custom-css.html +0 -17
  80. package/plugins/custom-css/admin/views/custom-css.js +0 -35
  81. package/plugins/custom-css/config.js +0 -1
  82. package/plugins/custom-css/data/custom.css +0 -0
  83. package/plugins/custom-css/plugin.js +0 -63
  84. package/plugins/custom-css/plugin.json +0 -32
  85. package/plugins/custom-css/public/inject-head.html +0 -1
  86. package/plugins/form-builder/data/forms/contact.json +0 -52
  87. package/plugins/form-builder/data/submissions/contact.json +0 -14
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Build script — creates a publishable staging copy in _publish/
3
+ * Browser-facing JS/CSS is minified; server-side code is copied as-is.
4
+ * Run: node scripts/build.js
5
+ */
6
+
7
+ import {cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync} from 'fs';
8
+ import {dirname, join} from 'path';
9
+ import {glob} from 'fs/promises';
10
+ import * as esbuild from 'esbuild';
11
+
12
+ const ROOT = new URL('..', import.meta.url).pathname.replace(/\/$/, '');
13
+ const OUT = join(ROOT, '_publish');
14
+
15
+ // Files/dirs copied verbatim (server-side, config, docs)
16
+ const COPY_AS_IS = [
17
+ 'server',
18
+ 'bin',
19
+ 'config',
20
+ 'scripts',
21
+ 'package.json',
22
+ 'README.md',
23
+ 'LICENSE',
24
+ '.npmignore',
25
+ ];
26
+
27
+ // Glob patterns for browser-facing assets to minify
28
+ const MINIFY_PATTERNS = [
29
+ 'admin/js/**/*.js',
30
+ 'admin/css/**/*.css',
31
+ 'public/js/**/*.js',
32
+ 'public/css/**/*.css',
33
+ 'plugins/*/public/**/*.js',
34
+ 'plugins/*/public/**/*.css',
35
+ ];
36
+
37
+ // Files within plugins that are copied as-is (server-side plugin code)
38
+ const PLUGIN_AS_IS_PATTERNS = [
39
+ 'plugins/**/plugin.js',
40
+ 'plugins/**/config.js',
41
+ 'plugins/**/routes/**/*.js',
42
+ 'plugins/**/services/**/*.js',
43
+ 'plugins/**/views/**/*.js',
44
+ 'plugins/**/*.json',
45
+ 'plugins/**/*.html',
46
+ ];
47
+
48
+ // HTML templates in admin (copied as-is)
49
+ const ADMIN_HTML_PATTERNS = [
50
+ 'admin/**/*.html',
51
+ ];
52
+
53
+ async function collectFiles(pattern) {
54
+ const files = [];
55
+ for await (const file of glob(pattern, {cwd: ROOT})) {
56
+ files.push(file);
57
+ }
58
+ return files;
59
+ }
60
+
61
+ async function minifyFile(relPath, loader) {
62
+ const src = readFileSync(join(ROOT, relPath), 'utf8');
63
+ const result = await esbuild.transform(src, {
64
+ loader,
65
+ minify: true,
66
+ target: 'es2020',
67
+ });
68
+ const outPath = join(OUT, relPath);
69
+ mkdirSync(dirname(outPath), {recursive: true});
70
+ writeFileSync(outPath, result.code);
71
+ }
72
+
73
+ async function build() {
74
+ // Clean and recreate _publish/
75
+ if (existsSync(OUT)) rmSync(OUT, {recursive: true, force: true});
76
+ mkdirSync(OUT, {recursive: true});
77
+
78
+ // Copy server-side dirs/files verbatim
79
+ for (const item of COPY_AS_IS) {
80
+ const src = join(ROOT, item);
81
+ if (existsSync(src)) {
82
+ cpSync(src, join(OUT, item), {recursive: true});
83
+ }
84
+ }
85
+
86
+ // Copy plugin server-side code verbatim
87
+ const pluginAsIs = await collectFiles('{' + PLUGIN_AS_IS_PATTERNS.join(',') + '}');
88
+ for (const rel of pluginAsIs) {
89
+ const outPath = join(OUT, rel);
90
+ mkdirSync(dirname(outPath), {recursive: true});
91
+ cpSync(join(ROOT, rel), outPath);
92
+ }
93
+
94
+ // Copy admin HTML templates verbatim
95
+ const adminHtml = await collectFiles(ADMIN_HTML_PATTERNS[0]);
96
+ for (const rel of adminHtml) {
97
+ const outPath = join(OUT, rel);
98
+ mkdirSync(dirname(outPath), {recursive: true});
99
+ cpSync(join(ROOT, rel), outPath);
100
+ }
101
+
102
+ // Minify browser-facing JS and CSS
103
+ let jsCount = 0;
104
+ let cssCount = 0;
105
+
106
+ // Collect all minify targets, deduplicate against plugin as-is
107
+ const pluginAsIsSet = new Set(pluginAsIs);
108
+
109
+ for (const pattern of MINIFY_PATTERNS) {
110
+ const files = await collectFiles(pattern);
111
+ for (const rel of files) {
112
+ if (pluginAsIsSet.has(rel)) continue; // skip plugin server-side JS
113
+ const loader = rel.endsWith('.css') ? 'css' : 'js';
114
+ await minifyFile(rel, loader);
115
+ if (loader === 'js') jsCount++;
116
+ else cssCount++;
117
+ }
118
+ }
119
+
120
+ const totalCopied = COPY_AS_IS.filter(i => existsSync(join(ROOT, i))).length
121
+ + pluginAsIs.length + adminHtml.length;
122
+
123
+ console.log(`Built into _publish/ — JS: ${jsCount} minified, CSS: ${cssCount} minified, ${totalCopied} entries copied`);
124
+ }
125
+
126
+ build().catch(err => {
127
+ console.error('Build failed:', err.message);
128
+ process.exit(1);
129
+ });
package/scripts/seed.js CHANGED
@@ -13,7 +13,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
13
  const ROOT = path.resolve(__dirname, '..');
14
14
 
15
15
  const PAGES_DIR = path.join(ROOT, 'content', 'pages');
16
- const NAV_CFG = path.join(ROOT, 'config', 'navigation.json');
16
+ const COLLECTIONS_DIR = path.join(ROOT, 'content', 'collections');
17
+ const FORMS_DIR = path.join(ROOT, 'plugins', 'form-builder', 'data', 'forms');
18
+ const SUBS_DIR = path.join(ROOT, 'plugins', 'form-builder', 'data', 'submissions');
19
+ const NAV_CFG = path.join(ROOT, 'config', 'navigation.json');
17
20
 
18
21
  const now = new Date().toISOString();
19
22
 
@@ -35,7 +38,9 @@ const SEED_NAV = {
35
38
  {text: 'Typography', url: '/resources/typography', icon: 'type'},
36
39
  {text: 'Grid System', url: '/resources/grid', icon: 'layout'},
37
40
  {text: 'Cards', url: '/resources/cards', icon: 'credit-card'},
38
- {text: 'Shortcode Reference', url: '/resources/shortcodes', icon: 'code'}
41
+ {text: 'Shortcode Reference', url: '/resources/shortcodes', icon: 'code'},
42
+ {text: 'Effects', url: '/resources/effects', icon: 'zap'},
43
+ {text: 'Interactive', url: '/resources/interactive', icon: 'mouse-pointer'}
39
44
  ]
40
45
  }
41
46
  ],
@@ -109,13 +114,41 @@ Add your services, mission statement, or team information here.
109
114
  },
110
115
  body: `# Contact
111
116
 
112
- Get in touch we'd love to hear from you.
117
+ Have a question, a project in mind, or just want to say hello? Fill in the form and we'll get back to you as soon as possible.
113
118
 
114
- ## How to reach us
119
+ [grid cols="2" gap="6"]
120
+ [col]
121
+
122
+ ## Send a message
123
+
124
+ <div data-form="contact"></div>
125
+
126
+ [/col]
127
+ [col]
128
+
129
+ ## Get in touch
130
+
131
+ [card title="Address"]
132
+ 123 Example Street
133
+ London, EC1A 1BB
134
+ United Kingdom
135
+ [/card]
115
136
 
116
- Replace this with your contact details, a form, or whatever works best for your site.
137
+ [card title="Email"]
138
+ hello@example.com
139
+ [/card]
117
140
 
118
- Edit this page from the [admin panel](/admin/).
141
+ [card title="Phone"]
142
+ +44 20 0000 0000
143
+ [/card]
144
+
145
+ [card title="Office hours"]
146
+ Monday – Friday: 9am – 5pm
147
+ Saturday – Sunday: Closed
148
+ [/card]
149
+
150
+ [/col]
151
+ [/grid]
119
152
  `
120
153
  },
121
154
  {
@@ -217,6 +250,22 @@ Full reference for every shortcode supported in the editor, including all attrib
217
250
  [Read more →](/resources/shortcodes)
218
251
  [/card]
219
252
  [/col]
253
+ [col]
254
+ [card title="Effects"]
255
+ Live demonstrations of every effect shortcode — scroll reveals, animated counters, typewriter text, ambient backgrounds,
256
+ and more.
257
+
258
+ [Read more →](/resources/effects)
259
+ [/card]
260
+ [/col]
261
+ [col]
262
+ [card title="Interactive"]
263
+ Live demos of \`[slideover]\` panels and \`[dconfig]\` declarative behaviour — toggle classes, show panels, and wire up
264
+ click handlers without writing JavaScript.
265
+
266
+ [Read more →](/resources/interactive)
267
+ [/card]
268
+ [/col]
220
269
  [/grid]
221
270
  `
222
271
  },
@@ -612,9 +661,444 @@ Replace \`your-form-slug\` with the slug from the Form Builder. Example:
612
661
 
613
662
  ---
614
663
 
664
+ ## [\`slideover\`]
665
+
666
+ Renders a trigger button that opens a slide-in panel with Markdown content.
667
+
668
+ | Attribute | Required | Description |
669
+ |-----------|----------|-------------|
670
+ | \`title\` | No | Panel header text |
671
+ | \`trigger\` | No | Button label (default: \`"Open"\`) |
672
+ | \`size\` | No | \`sm\`, \`md\` (default), \`lg\` |
673
+ | \`position\` | No | \`right\` (default) or \`left\` |
674
+
675
+ \`\`\`
676
+ [slideover title="More Info" trigger="Read more" size="md"]
677
+ Markdown here — cards, grids, and other shortcodes work inside.
678
+ [/slideover]
679
+ \`\`\`
680
+
681
+ ---
682
+
683
+ ## [\`dconfig\`]
684
+
685
+ Declarative behaviour — wires up click handlers and class toggles from JSON, no JavaScript needed.
686
+
687
+ \`\`\`
688
+ [dconfig]
689
+ {
690
+ "#my-btn": {
691
+ "events": {
692
+ "click": { "target": "#panel", "toggleClass": "hidden" }
693
+ }
694
+ }
695
+ }
696
+ [/dconfig]
697
+ \`\`\`
698
+
699
+ You can also set DConfig from the **DConfig section** in the page editor. Inline shortcodes win on selector conflict.
700
+ See [Interactive demos →](/resources/interactive)
701
+
702
+ ---
703
+
704
+ ← [Back to Resources](/resources)
705
+ `
706
+ },
707
+
708
+ // ----- Effects -----
709
+ {
710
+ file: path.join('resources', 'effects.md'),
711
+ fm: {
712
+ title: 'Effects', slug: 'effects',
713
+ description: 'Live demonstrations of every Domma Effects shortcode — reveals, counters, typewriter text, ambient backgrounds, and more.',
714
+ layout: 'default', status: 'published',
715
+ sortOrder: 15, showInNav: false, sidebar: false,
716
+ seo: {title: 'Effects — Resources', description: 'Live demonstrations of Domma Effects shortcodes'},
717
+ createdAt: now, updatedAt: now
718
+ },
719
+ body: `# Effects
720
+
721
+ Effect shortcodes bring content to life. All examples below are live (requires the Domma Effects plugin).
722
+
723
+ ---
724
+
725
+ ## \`[reveal]\` — Scroll-triggered entrance
726
+
727
+ [reveal animation="fade"]
728
+ [card title="Fade in"]
729
+ This card faded in as you scrolled to it.
730
+ [/card]
731
+ [/reveal]
732
+
733
+ \`\`\`
734
+ [reveal animation="fade"]
735
+ Your content here.
736
+ [/reveal]
737
+ \`\`\`
738
+
739
+ **Attributes:** \`animation\` (fade, slide-up, zoom, flip) · \`duration\` (ms) · \`delay\` (ms) · \`once\` (true/false)
740
+
741
+ ---
742
+
743
+ ## \`[counter]\` — Animated number
744
+
745
+ [reveal animation="slide-up"]
746
+ [grid cols="3" gap="4"]
747
+ [col]<div style="text-align:center;padding:1rem 0">
748
+
749
+ ## [counter to="247" separator="," /]
750
+
751
+ Pages published
752
+ </div>[/col]
753
+ [col]<div style="text-align:center;padding:1rem 0">
754
+
755
+ ## [counter to="98" suffix="%" /]
756
+
757
+ Uptime
758
+ </div>[/col]
759
+ [col]<div style="text-align:center;padding:1rem 0">
760
+
761
+ ## [counter to="12" /]
762
+
763
+ Plugins
764
+ </div>[/col]
765
+ [/grid]
766
+ [/reveal]
767
+
768
+ \`\`\`
769
+ [counter to="100" prefix="$" suffix="+" /]
770
+ \`\`\`
771
+
772
+ ---
773
+
774
+ ## \`[scribe]\` — Typewriter
775
+
776
+ [reveal animation="fade"]
777
+ [card]
778
+ [scribe speed="40" cursor="true"]
779
+ Domma CMS — fast, flexible, file-based.
780
+ [/scribe]
781
+ [/card]
782
+ [/reveal]
783
+
784
+ ---
785
+
786
+ ## \`[animate]\` — CSS-only animation
787
+
788
+ [grid cols="3" gap="4"]
789
+ [col]
790
+ [animate type="fade-in-up"]
791
+ [card title="fade-in-up"]
792
+ No plugin needed.
793
+ [/card]
794
+ [/animate]
795
+ [/col]
796
+ [col]
797
+ [animate type="zoom-in"]
798
+ [card title="zoom-in"]
799
+ Scales up from centre.
800
+ [/card]
801
+ [/animate]
802
+ [/col]
803
+ [col]
804
+ [animate type="fade-in-left"]
805
+ [card title="fade-in-left"]
806
+ Slides in from right.
807
+ [/card]
808
+ [/animate]
809
+ [/col]
810
+ [/grid]
811
+
812
+ ---
813
+
814
+ ← [Back to Resources](/resources)
815
+ `
816
+ },
817
+
818
+ // ----- Interactive -----
819
+ {
820
+ file: path.join('resources', 'interactive.md'),
821
+ fm: {
822
+ title: 'Interactive Shortcodes', slug: 'interactive',
823
+ description: 'Live demonstrations of the [slideover] and [dconfig] shortcodes — declarative interactivity without JavaScript.',
824
+ layout: 'default', status: 'published',
825
+ sortOrder: 16, showInNav: false, sidebar: false,
826
+ seo: {
827
+ title: 'Interactive Shortcodes — Resources',
828
+ description: 'Live demos of slideover and DConfig in Domma CMS'
829
+ },
830
+ createdAt: now, updatedAt: now
831
+ },
832
+ body: `# Interactive Shortcodes
833
+
834
+ Two shortcodes let you add interactivity to any page without writing JavaScript.
835
+
836
+ ---
837
+
838
+ ## Slideover
839
+
840
+ [slideover title="More Information" trigger="Read more →"]
841
+ ## About Domma CMS
842
+
843
+ Domma CMS is a **file-based** content management system built on Fastify. Pages are stored as Markdown files with YAML frontmatter.
844
+
845
+ No database required — content lives in \\\`content/pages/\\\` and is served server-side.
846
+ [/slideover]
847
+
848
+ [slideover title="Package Details" trigger="View specifications" size="lg"]
849
+ [card title="Technical Requirements"]
850
+ - **Node.js** 18 or later
851
+ - Any Linux/macOS/Windows system
852
+ [/card]
853
+
854
+ [card title="Included Plugins" collapsible="true"]
855
+ - Back to Top
856
+ - Cookie Consent
857
+ - Custom CSS
858
+ - Form Builder
859
+ [/card]
860
+ [/slideover]
861
+
862
+ ---
863
+
864
+ ## DConfig — Toggle on Click
865
+
866
+ [dconfig]
867
+ {
868
+ "#toggle-demo-btn": {
869
+ "events": {
870
+ "click": { "target": "#toggle-demo-panel", "toggleClass": "hidden" }
871
+ }
872
+ }
873
+ }
874
+ [/dconfig]
875
+
876
+ <button id="toggle-demo-btn" class="btn btn-primary">Toggle panel</button>
877
+
878
+ <div id="toggle-demo-panel" class="card mt-3" style="max-width:400px">
879
+ <div class="card-body">
880
+ <p>This panel toggles when you click the button above.</p>
881
+ </div>
882
+ </div>
883
+
884
+ ---
885
+
886
+ ## Syntax Reference
887
+
888
+ **Slideover attributes:**
889
+
890
+ | Attribute | Default | Description |
891
+ |-----------|---------|-------------|
892
+ | \`title\` | — | Panel header text |
893
+ | \`trigger\` | \`"Open"\` | Button label |
894
+ | \`size\` | \`"md"\` | \`sm\`, \`md\`, \`lg\` |
895
+ | \`position\` | \`"right"\` | \`right\` or \`left\` |
896
+
897
+ **DConfig format:**
898
+
899
+ \`\`\`
900
+ [dconfig]
901
+ {
902
+ "#selector": {
903
+ "events": {
904
+ "click": { "target": "#other-selector", "toggleClass": "class-name" }
905
+ }
906
+ }
907
+ }
908
+ [/dconfig]
909
+ \`\`\`
910
+
911
+ See the [Shortcode Reference](/resources/shortcodes) for full documentation.
912
+
913
+ ---
914
+
615
915
  ← [Back to Resources](/resources)
616
916
  `
917
+ }
918
+ ];
919
+
920
+ // ---------------------------------------------------------------------------
921
+ // Default collections
922
+ // ---------------------------------------------------------------------------
923
+
924
+ const COLLECTIONS = [
925
+ {
926
+ slug: 'contacts',
927
+ schema: {
928
+ slug: 'contacts',
929
+ title: 'Contacts',
930
+ description: 'Submissions from the contact form',
931
+ fields: [
932
+ {name: 'full_name', type: 'string', label: 'Full Name', required: false},
933
+ {name: 'email_address', type: 'string', label: 'Email Address', required: false},
934
+ {name: 'phone_number', type: 'tel', label: 'Phone Number', required: false},
935
+ {name: 'subject', type: 'string', label: 'Subject', required: false},
936
+ {name: 'message', type: 'textarea', label: 'Message', required: false}
937
+ ],
938
+ api: {
939
+ create: {enabled: false, access: 'admin'},
940
+ read: {enabled: true, access: 'public'},
941
+ update: {enabled: false, access: 'admin'},
942
+ delete: {enabled: false, access: 'admin'}
943
+ },
944
+ createdAt: now,
945
+ updatedAt: now
617
946
  }
947
+ },
948
+ {
949
+ slug: 'bookings',
950
+ schema: {
951
+ slug: 'bookings',
952
+ title: 'Bookings',
953
+ description: 'Booking requests — startedAt, endedAt, and createdAt (auto) captured per entry',
954
+ fields: [
955
+ {name: 'full_name', type: 'string', label: 'Full Name', required: true},
956
+ {name: 'email', type: 'string', label: 'Email', required: true},
957
+ {name: 'phone', type: 'tel', label: 'Phone Number', required: false},
958
+ {name: 'started_at', type: 'date', label: 'Start Date', required: true},
959
+ {name: 'ended_at', type: 'date', label: 'End Date', required: true},
960
+ {name: 'notes', type: 'textarea', label: 'Notes', required: false}
961
+ ],
962
+ api: {
963
+ create: {enabled: false, access: 'admin'},
964
+ read: {enabled: true, access: 'public'},
965
+ update: {enabled: false, access: 'admin'},
966
+ delete: {enabled: false, access: 'admin'}
967
+ },
968
+ createdAt: now,
969
+ updatedAt: now
970
+ }
971
+ }
972
+ ];
973
+
974
+ // ---------------------------------------------------------------------------
975
+ // Default forms (with collection actions)
976
+ // ---------------------------------------------------------------------------
977
+
978
+ const FORMS = [
979
+ {
980
+ slug: 'booking',
981
+ data: {
982
+ slug: 'booking',
983
+ title: 'Booking',
984
+ description: 'Request a booking — captures name, contact details, start date, end date, and notes. Entries stored in the Bookings collection with an automatic createdAt timestamp.',
985
+ fields: [
986
+ {
987
+ name: 'full_name',
988
+ type: 'string',
989
+ label: 'Full Name',
990
+ required: true,
991
+ placeholder: 'Your full name'
992
+ },
993
+ {
994
+ name: 'email',
995
+ type: 'string',
996
+ label: 'Email Address',
997
+ required: true,
998
+ placeholder: 'you@example.com'
999
+ },
1000
+ {
1001
+ name: 'phone',
1002
+ type: 'tel',
1003
+ label: 'Phone Number',
1004
+ required: false,
1005
+ placeholder: '+44 7700 000000'
1006
+ },
1007
+ {
1008
+ name: 'started_at',
1009
+ type: 'date',
1010
+ label: 'Start Date',
1011
+ required: true
1012
+ },
1013
+ {
1014
+ name: 'ended_at',
1015
+ type: 'date',
1016
+ label: 'End Date',
1017
+ required: true
1018
+ },
1019
+ {
1020
+ name: 'notes',
1021
+ type: 'textarea',
1022
+ label: 'Notes',
1023
+ required: false,
1024
+ placeholder: 'Any additional details…'
1025
+ }
1026
+ ],
1027
+ settings: {
1028
+ submitText: 'Request Booking',
1029
+ successMessage: 'Thank you — your booking request has been received. We\'ll be in touch to confirm.',
1030
+ layout: 'stacked',
1031
+ honeypot: true,
1032
+ rateLimitPerMinute: 3
1033
+ },
1034
+ actions: {
1035
+ email: {enabled: false, recipients: '', subjectPrefix: '[Booking]'},
1036
+ webhook: {enabled: false, url: '', method: 'POST'},
1037
+ collection: {enabled: true, slug: 'bookings'}
1038
+ },
1039
+ createdAt: now,
1040
+ updatedAt: now
1041
+ }
1042
+ },
1043
+ {
1044
+ slug: 'contact',
1045
+ data: {
1046
+ slug: 'contact',
1047
+ title: 'Contact',
1048
+ description: 'Get in touch with us.',
1049
+ fields: [
1050
+ {
1051
+ name: 'full_name',
1052
+ type: 'string',
1053
+ label: 'Full Name',
1054
+ required: true,
1055
+ placeholder: 'Your full name'
1056
+ },
1057
+ {
1058
+ name: 'email_address',
1059
+ type: 'string',
1060
+ label: 'Email Address',
1061
+ required: true,
1062
+ placeholder: 'you@example.com'
1063
+ },
1064
+ {
1065
+ name: 'phone_number',
1066
+ type: 'tel',
1067
+ label: 'Phone Number',
1068
+ required: false,
1069
+ placeholder: '+44 7700 000000'
1070
+ },
1071
+ {
1072
+ name: 'subject',
1073
+ type: 'string',
1074
+ label: 'Subject',
1075
+ required: false,
1076
+ placeholder: 'What is this regarding?'
1077
+ },
1078
+ {
1079
+ name: 'message',
1080
+ type: 'textarea',
1081
+ label: 'Message',
1082
+ required: true,
1083
+ placeholder: 'Your message…'
1084
+ }
1085
+ ],
1086
+ settings: {
1087
+ submitText: 'Send Message',
1088
+ successMessage: 'Thank you for your message. We\'ll be in touch shortly.',
1089
+ layout: 'stacked',
1090
+ honeypot: true,
1091
+ rateLimitPerMinute: 3
1092
+ },
1093
+ actions: {
1094
+ email: {enabled: false, recipients: '', subjectPrefix: '[Contact]'},
1095
+ webhook: {enabled: false, url: '', method: 'POST'},
1096
+ collection: {enabled: true, slug: 'contacts'}
1097
+ },
1098
+ createdAt: now,
1099
+ updatedAt: now
1100
+ }
1101
+ }
618
1102
  ];
619
1103
 
620
1104
  // ---------------------------------------------------------------------------
@@ -659,8 +1143,34 @@ process.stdout.write(' Updating navigation…');
659
1143
  await writeFile(NAV_CFG, JSON.stringify(SEED_NAV, null, 4) + '\n', 'utf8');
660
1144
  console.log(' done.');
661
1145
 
1146
+ for (const col of COLLECTIONS) {
1147
+ const dir = path.join(COLLECTIONS_DIR, col.slug);
1148
+ await mkdir(dir, {recursive: true});
1149
+ await writeFile(path.join(dir, 'schema.json'), JSON.stringify(col.schema, null, 2) + '\n', 'utf8');
1150
+ // Only initialise data.json if it doesn't already exist (preserve entries)
1151
+ const dataFile = path.join(dir, 'data.json');
1152
+ try {
1153
+ await writeFile(dataFile, '[]\n', {encoding: 'utf8', flag: 'wx'});
1154
+ } catch { /* already exists */
1155
+ }
1156
+ console.log(` ✓ collection: ${col.slug}`);
1157
+ }
1158
+
1159
+ await mkdir(FORMS_DIR, {recursive: true});
1160
+ await mkdir(SUBS_DIR, {recursive: true});
1161
+ for (const form of FORMS) {
1162
+ await writeFile(path.join(FORMS_DIR, `${form.slug}.json`), JSON.stringify(form.data, null, 4) + '\n', 'utf8');
1163
+ // Only initialise submissions file if it doesn't already exist
1164
+ const subsFile = path.join(SUBS_DIR, `${form.slug}.json`);
1165
+ try {
1166
+ await writeFile(subsFile, '[]\n', {encoding: 'utf8', flag: 'wx'});
1167
+ } catch { /* already exists */
1168
+ }
1169
+ console.log(` ✓ form: ${form.slug} → collection: ${form.data.actions.collection.slug}`);
1170
+ }
1171
+
662
1172
  console.log('');
663
- console.log(` ✓ Seed complete — ${PAGES.length} pages created, navigation updated.`);
1173
+ console.log(` ✓ Seed complete — ${PAGES.length} pages, ${COLLECTIONS.length} collection(s), ${FORMS.length} form(s).`);
664
1174
  console.log(' Visit your site at http://localhost:3050/');
665
1175
  console.log(' Resources section available at http://localhost:3050/resources');
666
1176
  console.log('');