modern-cms 1.0.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 (180) hide show
  1. package/README.md +41 -0
  2. package/bin/lib.js +97 -0
  3. package/bin/modern-cms.js +197 -0
  4. package/package.json +37 -0
  5. package/template/.env.example +48 -0
  6. package/template/README.md +210 -0
  7. package/template/apps/api/Dockerfile +26 -0
  8. package/template/apps/api/package.json +45 -0
  9. package/template/apps/api/prisma/backfill-search-text.ts +31 -0
  10. package/template/apps/api/prisma/migrations/20260701034530_init/migration.sql +86 -0
  11. package/template/apps/api/prisma/migrations/20260701081942_add_publish_at_and_revisions/migration.sql +19 -0
  12. package/template/apps/api/prisma/migrations/20260701085011_add_form_submissions/migration.sql +13 -0
  13. package/template/apps/api/prisma/migrations/20260702084049_add_global_sections_and_saved_blocks/migration.sql +24 -0
  14. package/template/apps/api/prisma/migrations/20260702085609_add_page_locale_and_translations/migration.sql +6 -0
  15. package/template/apps/api/prisma/migrations/20260702091914_add_collections/migration.sql +38 -0
  16. package/template/apps/api/prisma/migrations/20260702120000_platform_hardening/migration.sql +15 -0
  17. package/template/apps/api/prisma/migrations/20260705031230_add_collections_redirects_media_meta/migration.sql +17 -0
  18. package/template/apps/api/prisma/migrations/20260705033241_add_audit_log/migration.sql +18 -0
  19. package/template/apps/api/prisma/migrations/20260706031301_standard_cms_platform_pass/migration.sql +48 -0
  20. package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
  21. package/template/apps/api/prisma/reset-admin.ts +19 -0
  22. package/template/apps/api/prisma/schema.prisma +251 -0
  23. package/template/apps/api/prisma/seed-homepage.ts +752 -0
  24. package/template/apps/api/prisma/seed-our-charities.ts +231 -0
  25. package/template/apps/api/prisma/seed-templates.ts +154 -0
  26. package/template/apps/api/prisma/seed.ts +191 -0
  27. package/template/apps/api/src/env.ts +61 -0
  28. package/template/apps/api/src/index.ts +66 -0
  29. package/template/apps/api/src/lib/revalidate.ts +28 -0
  30. package/template/apps/api/src/lib/storage/azure.ts +25 -0
  31. package/template/apps/api/src/lib/storage/cloudinary.ts +42 -0
  32. package/template/apps/api/src/lib/storage/index.ts +25 -0
  33. package/template/apps/api/src/lib/storage/s3.ts +39 -0
  34. package/template/apps/api/src/lib/storage/types.ts +8 -0
  35. package/template/apps/api/src/middleware/auditLog.ts +32 -0
  36. package/template/apps/api/src/middleware/auth.ts +53 -0
  37. package/template/apps/api/src/middleware/rateLimit.ts +31 -0
  38. package/template/apps/api/src/middleware/securityHeaders.ts +10 -0
  39. package/template/apps/api/src/prisma.ts +3 -0
  40. package/template/apps/api/src/routes/audit.routes.ts +26 -0
  41. package/template/apps/api/src/routes/auth.routes.ts +46 -0
  42. package/template/apps/api/src/routes/backup.routes.ts +216 -0
  43. package/template/apps/api/src/routes/blocks.routes.ts +38 -0
  44. package/template/apps/api/src/routes/collections.routes.ts +185 -0
  45. package/template/apps/api/src/routes/forms.routes.ts +114 -0
  46. package/template/apps/api/src/routes/globalSections.routes.ts +43 -0
  47. package/template/apps/api/src/routes/media.routes.ts +77 -0
  48. package/template/apps/api/src/routes/pages.routes.ts +319 -0
  49. package/template/apps/api/src/routes/redirects.routes.ts +52 -0
  50. package/template/apps/api/src/routes/search.routes.ts +83 -0
  51. package/template/apps/api/src/routes/templates.routes.ts +41 -0
  52. package/template/apps/api/src/routes/theme.routes.ts +75 -0
  53. package/template/apps/api/src/routes/users.routes.ts +39 -0
  54. package/template/apps/api/tsconfig.json +11 -0
  55. package/template/apps/web/Dockerfile +26 -0
  56. package/template/apps/web/app/[[...slug]]/page.tsx +254 -0
  57. package/template/apps/web/app/admin/(protected)/audit/page.tsx +80 -0
  58. package/template/apps/web/app/admin/(protected)/collections/item/[itemId]/page.tsx +375 -0
  59. package/template/apps/web/app/admin/(protected)/collections/page.tsx +224 -0
  60. package/template/apps/web/app/admin/(protected)/forms/page.tsx +88 -0
  61. package/template/apps/web/app/admin/(protected)/layout.tsx +5 -0
  62. package/template/apps/web/app/admin/(protected)/media/page.tsx +138 -0
  63. package/template/apps/web/app/admin/(protected)/page.tsx +186 -0
  64. package/template/apps/web/app/admin/(protected)/pages/[id]/page.tsx +560 -0
  65. package/template/apps/web/app/admin/(protected)/settings/backup/page.tsx +97 -0
  66. package/template/apps/web/app/admin/(protected)/settings/global-sections/page.tsx +333 -0
  67. package/template/apps/web/app/admin/(protected)/settings/navigation/page.tsx +192 -0
  68. package/template/apps/web/app/admin/(protected)/settings/redirects/page.tsx +108 -0
  69. package/template/apps/web/app/admin/(protected)/settings/theme/page.tsx +239 -0
  70. package/template/apps/web/app/admin/(protected)/users/page.tsx +94 -0
  71. package/template/apps/web/app/admin/login/page.tsx +76 -0
  72. package/template/apps/web/app/api/revalidate/route.ts +34 -0
  73. package/template/apps/web/app/globals.css +120 -0
  74. package/template/apps/web/app/layout.tsx +39 -0
  75. package/template/apps/web/app/preview/[token]/page.tsx +70 -0
  76. package/template/apps/web/app/robots.ts +11 -0
  77. package/template/apps/web/app/sitemap.ts +27 -0
  78. package/template/apps/web/components/admin/AdminShell.tsx +104 -0
  79. package/template/apps/web/components/builder/BuilderCanvasNode.tsx +546 -0
  80. package/template/apps/web/components/builder/BuilderDeviceContext.tsx +13 -0
  81. package/template/apps/web/components/builder/Canvas.tsx +76 -0
  82. package/template/apps/web/components/builder/CanvasOverlayContext.tsx +22 -0
  83. package/template/apps/web/components/builder/ColorPickerField.tsx +47 -0
  84. package/template/apps/web/components/builder/DroppableChildren.tsx +50 -0
  85. package/template/apps/web/components/builder/FieldRenderer.tsx +277 -0
  86. package/template/apps/web/components/builder/IconRenderer.tsx +7 -0
  87. package/template/apps/web/components/builder/Inspector.tsx +956 -0
  88. package/template/apps/web/components/builder/JsonEditor.tsx +71 -0
  89. package/template/apps/web/components/builder/KeyValueListEditor.tsx +76 -0
  90. package/template/apps/web/components/builder/MediaPickerModal.tsx +95 -0
  91. package/template/apps/web/components/builder/PageSettingsModal.tsx +363 -0
  92. package/template/apps/web/components/builder/Palette.tsx +106 -0
  93. package/template/apps/web/components/builder/RichTextEditor.tsx +140 -0
  94. package/template/apps/web/components/renderer/CmsImage.tsx +46 -0
  95. package/template/apps/web/components/renderer/NodeBackgroundLayers.tsx +35 -0
  96. package/template/apps/web/components/renderer/PublicPageView.tsx +56 -0
  97. package/template/apps/web/components/renderer/Renderer.tsx +146 -0
  98. package/template/apps/web/components/renderer/RendererContext.tsx +20 -0
  99. package/template/apps/web/components/renderer/registry.tsx +87 -0
  100. package/template/apps/web/components/renderer/sections/Accordion.tsx +39 -0
  101. package/template/apps/web/components/renderer/sections/BeforeAfter.tsx +75 -0
  102. package/template/apps/web/components/renderer/sections/Button.tsx +42 -0
  103. package/template/apps/web/components/renderer/sections/ButtonGroup.tsx +22 -0
  104. package/template/apps/web/components/renderer/sections/CardGrid.tsx +66 -0
  105. package/template/apps/web/components/renderer/sections/CollectionList.tsx +84 -0
  106. package/template/apps/web/components/renderer/sections/Column.tsx +26 -0
  107. package/template/apps/web/components/renderer/sections/ContactForm.tsx +96 -0
  108. package/template/apps/web/components/renderer/sections/Container.tsx +44 -0
  109. package/template/apps/web/components/renderer/sections/ContentLinks.tsx +38 -0
  110. package/template/apps/web/components/renderer/sections/Countdown.tsx +69 -0
  111. package/template/apps/web/components/renderer/sections/Cta.tsx +15 -0
  112. package/template/apps/web/components/renderer/sections/Divider.tsx +26 -0
  113. package/template/apps/web/components/renderer/sections/EmbedBlock.tsx +43 -0
  114. package/template/apps/web/components/renderer/sections/Footer.tsx +117 -0
  115. package/template/apps/web/components/renderer/sections/Gallery.tsx +27 -0
  116. package/template/apps/web/components/renderer/sections/GoToTop.tsx +54 -0
  117. package/template/apps/web/components/renderer/sections/Grid.tsx +54 -0
  118. package/template/apps/web/components/renderer/sections/GridCell.tsx +33 -0
  119. package/template/apps/web/components/renderer/sections/Header.tsx +132 -0
  120. package/template/apps/web/components/renderer/sections/Hero.tsx +73 -0
  121. package/template/apps/web/components/renderer/sections/ImageBackgroundSection.tsx +20 -0
  122. package/template/apps/web/components/renderer/sections/ImageBlock.tsx +55 -0
  123. package/template/apps/web/components/renderer/sections/LanguageSwitcher.tsx +51 -0
  124. package/template/apps/web/components/renderer/sections/Logo.tsx +24 -0
  125. package/template/apps/web/components/renderer/sections/LogoCloud.tsx +67 -0
  126. package/template/apps/web/components/renderer/sections/MapEmbed.tsx +24 -0
  127. package/template/apps/web/components/renderer/sections/Marquee.tsx +45 -0
  128. package/template/apps/web/components/renderer/sections/NavLinks.tsx +69 -0
  129. package/template/apps/web/components/renderer/sections/NavZone.tsx +19 -0
  130. package/template/apps/web/components/renderer/sections/PricingTable.tsx +65 -0
  131. package/template/apps/web/components/renderer/sections/RichText.tsx +7 -0
  132. package/template/apps/web/components/renderer/sections/SiteSearch.tsx +67 -0
  133. package/template/apps/web/components/renderer/sections/Slider.tsx +81 -0
  134. package/template/apps/web/components/renderer/sections/Spacer.tsx +8 -0
  135. package/template/apps/web/components/renderer/sections/Stats.tsx +69 -0
  136. package/template/apps/web/components/renderer/sections/Tabs.tsx +59 -0
  137. package/template/apps/web/components/renderer/sections/TeamGrid.tsx +49 -0
  138. package/template/apps/web/components/renderer/sections/Testimonial.tsx +79 -0
  139. package/template/apps/web/components/renderer/sections/ThreeBackground.tsx +43 -0
  140. package/template/apps/web/components/renderer/sections/Timeline.tsx +29 -0
  141. package/template/apps/web/components/renderer/sections/VideoBackgroundSection.tsx +20 -0
  142. package/template/apps/web/components/renderer/sections/VideoEmbed.tsx +70 -0
  143. package/template/apps/web/components/renderer/sections/types.ts +7 -0
  144. package/template/apps/web/components/seo/JsonLd.tsx +111 -0
  145. package/template/apps/web/components/theme/AnalyticsScripts.tsx +68 -0
  146. package/template/apps/web/components/theme/DarkModeToggle.tsx +39 -0
  147. package/template/apps/web/components/theme/SiteDataContext.tsx +39 -0
  148. package/template/apps/web/components/theme/ThemeProvider.tsx +58 -0
  149. package/template/apps/web/eslint.config.mjs +16 -0
  150. package/template/apps/web/lib/api.ts +45 -0
  151. package/template/apps/web/lib/responsiveCss.ts +42 -0
  152. package/template/apps/web/lib/sanitizeHtml.ts +16 -0
  153. package/template/apps/web/lib/serverApi.ts +25 -0
  154. package/template/apps/web/lib/style.ts +73 -0
  155. package/template/apps/web/lib/threeBackgroundScene.ts +298 -0
  156. package/template/apps/web/lib/useCurrentUser.ts +35 -0
  157. package/template/apps/web/lib/useEntranceAnimation.ts +245 -0
  158. package/template/apps/web/next-env.d.ts +6 -0
  159. package/template/apps/web/next.config.mjs +32 -0
  160. package/template/apps/web/package.json +46 -0
  161. package/template/apps/web/postcss.config.mjs +6 -0
  162. package/template/apps/web/proxy.ts +23 -0
  163. package/template/apps/web/tailwind.config.ts +24 -0
  164. package/template/apps/web/tsconfig.json +41 -0
  165. package/template/docker-compose.yml +60 -0
  166. package/template/package.json +29 -0
  167. package/template/packages/shared/package.json +21 -0
  168. package/template/packages/shared/src/components.ts +1626 -0
  169. package/template/packages/shared/src/index.ts +9 -0
  170. package/template/packages/shared/src/schema/collections.ts +70 -0
  171. package/template/packages/shared/src/schema/fields.ts +26 -0
  172. package/template/packages/shared/src/schema/media.ts +13 -0
  173. package/template/packages/shared/src/schema/pageNode.ts +255 -0
  174. package/template/packages/shared/src/schema/templates.ts +20 -0
  175. package/template/packages/shared/src/schema/theme.ts +69 -0
  176. package/template/packages/shared/src/schema/user.ts +25 -0
  177. package/template/packages/shared/src/tree.test.ts +65 -0
  178. package/template/packages/shared/src/tree.ts +303 -0
  179. package/template/packages/shared/tsconfig.json +8 -0
  180. package/template/packages/shared/vitest.config.ts +8 -0
@@ -0,0 +1,231 @@
1
+ import "dotenv/config";
2
+ import { PrismaClient } from "@prisma/client";
3
+ import { createEmptyPage, createNode, insertNode, setContainerColumnCount } from "@pgcms/shared";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ // Builds a demo "Our Charities" page, structurally modeled on the kind of nonprofit
8
+ // charities-listing page you'd find at an organization like sigbi.org/what-we-do/our-charities/
9
+ // (intro -> charity listings -> impact stats -> donation deadline -> foundation blurb ->
10
+ // testimonials -> gallery -> video -> map -> news -> donate -> join CTA -> footer), using
11
+ // original/generic content and placeholder imagery — not any real organization's text,
12
+ // logo, or trademarks. Exercises every component type currently in the registry.
13
+
14
+ const PLACEHOLDER = (label: string, size = "800x500") =>
15
+ `https://placehold.co/${size}/1e293b/ffffff?text=${encodeURIComponent(label)}`;
16
+ // For images used as a *background behind real heading text* — a baked-in text label
17
+ // would visually clash with the actual copy layered on top, so keep these blank.
18
+ const PLACEHOLDER_BG = (size: string) => `https://placehold.co/${size}/1e293b/1e293b?text=+`;
19
+
20
+ const SAMPLE_VIDEO = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
21
+
22
+ export async function seedOurCharitiesPage(prisma: PrismaClient) {
23
+ let root = createEmptyPage();
24
+
25
+ // 1. Hero banner (image background)
26
+ const heroBanner = createNode("imageBackgroundSection");
27
+ heroBanner.props.heading = "Standing Up for Women and Girls";
28
+ heroBanner.props.subheading =
29
+ "Discover the charities and appeals our federation supports to change lives around the world.";
30
+ heroBanner.props.buttonText = "Donate Now";
31
+ heroBanner.props.buttonHref = "#donate";
32
+ heroBanner.style.backgroundImage = PLACEHOLDER_BG("1600x700");
33
+ root = insertNode(root, root.id, heroBanner);
34
+
35
+ // 2. Intro rich text
36
+ const intro = createNode("richText");
37
+ intro.props.html =
38
+ "<p>Every year, our federation raises funds for a small number of carefully chosen appeals. " +
39
+ "Explore the funds below, see the impact your support has already made, and find out how to " +
40
+ "give to the causes that matter most.</p>";
41
+ root = insertNode(root, root.id, intro);
42
+
43
+ // 3. Card grid — the charity listings themselves
44
+ const charities = createNode("cardGrid");
45
+ charities.props.heading = "Our Charities";
46
+ charities.props.columns = 3;
47
+ charities.props.cards = [
48
+ {
49
+ image: PLACEHOLDER("Benevolent Fund"),
50
+ title: "Benevolent Fund",
51
+ excerpt: "Financial assistance for members and their families facing hardship.",
52
+ buttonText: "Learn more",
53
+ href: "#benevolent-fund",
54
+ },
55
+ {
56
+ image: PLACEHOLDER("Education Grant"),
57
+ title: "Diamond Education Grant",
58
+ excerpt: "Funding education and skills training for women rebuilding their lives.",
59
+ buttonText: "Learn more",
60
+ href: "#education-grant",
61
+ },
62
+ {
63
+ image: PLACEHOLDER("Emergency Relief"),
64
+ title: "Emergency Relief Fund",
65
+ excerpt: "Rapid-response aid for communities affected by disaster and crisis.",
66
+ buttonText: "Learn more",
67
+ href: "#emergency-relief",
68
+ },
69
+ ];
70
+ root = insertNode(root, root.id, charities);
71
+
72
+ // 4. Impact stats
73
+ const stats = createNode("stats");
74
+ stats.props.columns = 4;
75
+ stats.props.items = [
76
+ { value: "500+", label: "Charities Supported" },
77
+ { value: "$2M+", label: "Raised This Year" },
78
+ { value: "40", label: "Countries Reached" },
79
+ { value: "12K", label: "Volunteer Hours" },
80
+ ];
81
+ stats.style.backgroundColor = "#f1f5f9";
82
+ root = insertNode(root, root.id, stats);
83
+
84
+ // 5. Countdown to the next grant cycle deadline
85
+ const countdown = createNode("countdown");
86
+ countdown.props.heading = "Next Grant Cycle Deadline";
87
+ countdown.props.expiredText = "Applications for this cycle are now closed — check back soon for the next round.";
88
+ countdown.props.buttonText = "Apply Now";
89
+ countdown.props.buttonHref = "#apply";
90
+ root = insertNode(root, root.id, countdown);
91
+
92
+ // 6. Foundation feature — two-column layout (image + copy)
93
+ let foundation = createNode("container");
94
+ foundation = setContainerColumnCount(foundation, 2);
95
+ const foundationImage = createNode("imageBlock");
96
+ foundationImage.props.src = PLACEHOLDER("Our Foundation", "700x500");
97
+ foundationImage.props.alt = "Our Foundation";
98
+ const foundationText = createNode("richText");
99
+ foundationText.props.html =
100
+ "<h2>Our Foundation</h2><p>Our federation's charitable foundation focuses on education, poverty " +
101
+ "prevention, and human rights for women and girls worldwide. Together with our member clubs, it " +
102
+ "funds grassroots projects that create lasting change.</p><p><a href=\"#foundation\">Visit the Foundation →</a></p>";
103
+ foundation.children[0].children.push(foundationImage);
104
+ foundation.children[1].children.push(foundationText);
105
+ root = insertNode(root, root.id, foundation);
106
+
107
+ // 7. Testimonial slider
108
+ const testimonials = createNode("slider");
109
+ testimonials.props.autoplay = true;
110
+ testimonials.props.slides = [
111
+ {
112
+ image: PLACEHOLDER_BG("1600x600"),
113
+ heading: "“The Emergency Relief Fund reached our village within days.”",
114
+ subheading: "Flood relief recipient, 2025",
115
+ buttonText: "",
116
+ buttonHref: "",
117
+ },
118
+ {
119
+ image: PLACEHOLDER_BG("1600x600"),
120
+ heading: "“The Education Grant let me finish my nursing degree.”",
121
+ subheading: "Diamond Education Grant recipient",
122
+ buttonText: "",
123
+ buttonHref: "",
124
+ },
125
+ {
126
+ image: PLACEHOLDER_BG("1600x600"),
127
+ heading: "“Membership gave me a global community of changemakers.”",
128
+ subheading: "Federation member since 2019",
129
+ buttonText: "",
130
+ buttonHref: "",
131
+ },
132
+ ];
133
+ root = insertNode(root, root.id, testimonials);
134
+
135
+ // 8. Photo gallery
136
+ const gallery = createNode("gallery");
137
+ gallery.props.columns = 4;
138
+ gallery.props.images = [1, 2, 3, 4].map((n) => ({ src: PLACEHOLDER(`Impact ${n}`, "500x500"), alt: `Impact photo ${n}` }));
139
+ root = insertNode(root, root.id, gallery);
140
+
141
+ // 9. Video background section
142
+ const video = createNode("videoBackgroundSection");
143
+ video.props.heading = "See Our Work in Action";
144
+ video.props.subheading = "A look at the projects your donations make possible.";
145
+ video.style.backgroundVideo = SAMPLE_VIDEO;
146
+ root = insertNode(root, root.id, video);
147
+
148
+ // 10. Map — find a local club
149
+ const map = createNode("mapEmbed");
150
+ map.props.lat = 51.5074;
151
+ map.props.lng = -0.1278;
152
+ map.props.zoom = 11;
153
+ root = insertNode(root, root.id, map);
154
+
155
+ // 11. Latest news
156
+ const news = createNode("cardGrid");
157
+ news.props.heading = "Latest News";
158
+ news.props.columns = 3;
159
+ news.props.cards = [
160
+ {
161
+ image: PLACEHOLDER("News 1"),
162
+ title: "Federation Announces Record Fundraising Year",
163
+ excerpt: "Member clubs raised more for our appeals in the last year than ever before.",
164
+ buttonText: "Read more",
165
+ href: "#news-1",
166
+ },
167
+ {
168
+ image: PLACEHOLDER("News 2"),
169
+ title: "Education Grant Opens for New Applications",
170
+ excerpt: "The Diamond Education Grant is now accepting applications for the next cohort.",
171
+ buttonText: "Read more",
172
+ href: "#news-2",
173
+ },
174
+ {
175
+ image: PLACEHOLDER("News 3"),
176
+ title: "Emergency Relief Fund Responds to Flooding",
177
+ excerpt: "Rapid-response aid was distributed to affected communities within 72 hours.",
178
+ buttonText: "Read more",
179
+ href: "#news-3",
180
+ },
181
+ ];
182
+ root = insertNode(root, root.id, news);
183
+
184
+ // 12. Donation buttons
185
+ const donate = createNode("buttonGroup");
186
+ donate.style.paddingY = "48px";
187
+ donate.props.buttons = [
188
+ { text: "Donate to Benevolent Fund", href: "#donate-benevolent", variant: "primary" },
189
+ { text: "Donate to Education Grant", href: "#donate-education", variant: "secondary" },
190
+ { text: "Donate to Emergency Relief", href: "#donate-emergency", variant: "outline" },
191
+ ];
192
+ root = insertNode(root, root.id, donate);
193
+
194
+ // 13. Join CTA
195
+ const cta = createNode("cta");
196
+ cta.props.heading = "Ready to Make a Difference?";
197
+ cta.props.subheading = "Join a community of changemakers standing up for women and girls worldwide.";
198
+ cta.props.buttonText = "Join Us";
199
+ cta.props.buttonHref = "#join";
200
+ root = insertNode(root, root.id, cta);
201
+
202
+ const data = {
203
+ title: "Our Charities",
204
+ status: "PUBLISHED" as const,
205
+ content: root as any,
206
+ seoTitle: "Our Charities",
207
+ seoDescription:
208
+ "Explore the charities and appeals we support, see our impact, and find out how to give or get involved.",
209
+ };
210
+
211
+ const existing = await prisma.page.findUnique({ where: { slug: "our-charities" } });
212
+ if (existing) {
213
+ await prisma.page.update({ where: { id: existing.id }, data });
214
+ console.log("Updated existing 'Our Charities' page");
215
+ } else {
216
+ await prisma.page.create({ data: { ...data, slug: "our-charities" } });
217
+ console.log("Created 'Our Charities' page");
218
+ }
219
+ }
220
+
221
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
222
+ const prisma = new PrismaClient();
223
+ seedOurCharitiesPage(prisma)
224
+ .catch((err) => {
225
+ console.error(err);
226
+ process.exit(1);
227
+ })
228
+ .finally(async () => {
229
+ await prisma.$disconnect();
230
+ });
231
+ }
@@ -0,0 +1,154 @@
1
+ // One-off: seeds a handful of starter PageTemplate rows so a new page can begin from
2
+ // a real, populated layout instead of a blank canvas. Idempotent (matches by name).
3
+ import "dotenv/config";
4
+ import { PrismaClient } from "@prisma/client";
5
+ import { createEmptyPage, createNode, insertNode } from "@pgcms/shared";
6
+
7
+ const prisma = new PrismaClient();
8
+
9
+ const PLACEHOLDER = (label: string, size = "1600x700") =>
10
+ `https://placehold.co/${size}/0b1220/ffffff?text=${encodeURIComponent(label)}`;
11
+
12
+ function landingPage() {
13
+ let root = createEmptyPage();
14
+
15
+ const hero = createNode("hero");
16
+ hero.props.heading = "Build your next website in minutes";
17
+ hero.props.subheading = "A visual, drag-and-drop CMS with everything a modern site needs — no code required.";
18
+ hero.props.primaryButtonText = "Get Started";
19
+ hero.props.primaryButtonHref = "#";
20
+ hero.props.backgroundAnimationEnabled = true;
21
+ hero.style.minHeight = "480px";
22
+ root = insertNode(root, root.id, hero);
23
+
24
+ const stats = createNode("stats");
25
+ root = insertNode(root, root.id, stats);
26
+
27
+ const cardGrid = createNode("cardGrid");
28
+ cardGrid.props.heading = "Everything you need";
29
+ root = insertNode(root, root.id, cardGrid);
30
+
31
+ const testimonial = createNode("testimonial");
32
+ root = insertNode(root, root.id, testimonial);
33
+
34
+ const pricing = createNode("pricingTable");
35
+ root = insertNode(root, root.id, pricing);
36
+
37
+ const cta = createNode("cta");
38
+ root = insertNode(root, root.id, cta);
39
+
40
+ return root;
41
+ }
42
+
43
+ function aboutPage() {
44
+ let root = createEmptyPage();
45
+
46
+ const hero = createNode("imageBackgroundSection");
47
+ hero.props.heading = "About Us";
48
+ hero.props.subheading = "Learn about our mission, our story, and the people behind it.";
49
+ hero.style.backgroundImage = PLACEHOLDER("About");
50
+ hero.style.minHeight = "360px";
51
+ root = insertNode(root, root.id, hero);
52
+
53
+ const richText = createNode("richText");
54
+ richText.props.html =
55
+ "<h2>Our Story</h2><p>Replace this with your company's story — where you started, what you believe in, and where you're headed.</p>";
56
+ root = insertNode(root, root.id, richText);
57
+
58
+ const team = createNode("teamGrid");
59
+ root = insertNode(root, root.id, team);
60
+
61
+ const logoCloud = createNode("logoCloud");
62
+ logoCloud.props.heading = "Trusted by teams at";
63
+ root = insertNode(root, root.id, logoCloud);
64
+
65
+ return root;
66
+ }
67
+
68
+ function servicesPage() {
69
+ let root = createEmptyPage();
70
+
71
+ const hero = createNode("hero");
72
+ hero.props.heading = "Our Services";
73
+ hero.props.subheading = "What we offer, and how it helps you.";
74
+ root = insertNode(root, root.id, hero);
75
+
76
+ const cardGrid = createNode("cardGrid");
77
+ cardGrid.props.heading = "What we do";
78
+ root = insertNode(root, root.id, cardGrid);
79
+
80
+ const timeline = createNode("timeline");
81
+ timeline.props.heading = "How it works";
82
+ root = insertNode(root, root.id, timeline);
83
+
84
+ const accordion = createNode("accordion");
85
+ root = insertNode(root, root.id, accordion);
86
+
87
+ const cta = createNode("cta");
88
+ root = insertNode(root, root.id, cta);
89
+
90
+ return root;
91
+ }
92
+
93
+ function blogHomePage() {
94
+ let root = createEmptyPage();
95
+
96
+ const hero = createNode("richText");
97
+ hero.props.html = "<h1>Blog</h1><p>The latest updates, stories, and insights.</p>";
98
+ hero.style.textAlign = "center";
99
+ hero.style.paddingY = "64px";
100
+ root = insertNode(root, root.id, hero);
101
+
102
+ const list = createNode("collectionList");
103
+ list.props.collectionSlug = "blog";
104
+ root = insertNode(root, root.id, list);
105
+
106
+ return root;
107
+ }
108
+
109
+ function contactPage() {
110
+ let root = createEmptyPage();
111
+
112
+ const hero = createNode("richText");
113
+ hero.props.html = "<h1>Contact Us</h1><p>We'd love to hear from you.</p>";
114
+ hero.style.textAlign = "center";
115
+ hero.style.paddingY = "48px";
116
+ root = insertNode(root, root.id, hero);
117
+
118
+ const form = createNode("contactForm");
119
+ root = insertNode(root, root.id, form);
120
+
121
+ const map = createNode("mapEmbed");
122
+ root = insertNode(root, root.id, map);
123
+
124
+ return root;
125
+ }
126
+
127
+ const TEMPLATES: { name: string; category: string; build: () => ReturnType<typeof createEmptyPage> }[] = [
128
+ { name: "Landing Page", category: "Marketing", build: landingPage },
129
+ { name: "About Us", category: "Company", build: aboutPage },
130
+ { name: "Services", category: "Marketing", build: servicesPage },
131
+ { name: "Blog Home", category: "Content", build: blogHomePage },
132
+ { name: "Contact", category: "Company", build: contactPage },
133
+ ];
134
+
135
+ async function main() {
136
+ for (const t of TEMPLATES) {
137
+ const existing = await prisma.pageTemplate.findFirst({ where: { name: t.name } });
138
+ if (existing) {
139
+ console.log(`Skipping "${t.name}" — already exists.`);
140
+ continue;
141
+ }
142
+ await prisma.pageTemplate.create({
143
+ data: { name: t.name, category: t.category, content: t.build() as object },
144
+ });
145
+ console.log(`Created template "${t.name}".`);
146
+ }
147
+ }
148
+
149
+ main()
150
+ .catch((err) => {
151
+ console.error(err);
152
+ process.exitCode = 1;
153
+ })
154
+ .finally(() => prisma.$disconnect());
@@ -0,0 +1,191 @@
1
+ import "dotenv/config";
2
+ import bcrypt from "bcryptjs";
3
+ import { PrismaClient } from "@prisma/client";
4
+ import { createEmptyPage, createNode, insertNode } from "@pgcms/shared";
5
+ import { seedAdvancedHomepage } from "./seed-homepage.ts";
6
+ import { seedOurCharitiesPage } from "./seed-our-charities.ts";
7
+
8
+ const prisma = new PrismaClient();
9
+
10
+ const PLACEHOLDER = (label: string, size = "1200x630") =>
11
+ `https://placehold.co/${size}/0b1220/ffffff?text=${encodeURIComponent(label)}`;
12
+
13
+ async function seedDemoCollections() {
14
+ const newsCollection = await prisma.collection.upsert({
15
+ where: { slug: "news" },
16
+ update: {},
17
+ create: { name: "News", slug: "news" },
18
+ });
19
+ const eventsCollection = await prisma.collection.upsert({
20
+ where: { slug: "events" },
21
+ update: {},
22
+ create: { name: "Events", slug: "events" },
23
+ });
24
+
25
+ const now = new Date();
26
+
27
+ function buildItemContent(title: string, subtitle: string) {
28
+ let root = createEmptyPage();
29
+ const hero = createNode("imageBackgroundSection");
30
+ hero.props.heading = title;
31
+ hero.props.subheading = subtitle;
32
+ hero.style.backgroundImage = PLACEHOLDER(title, "1600x700");
33
+ hero.style.minHeight = "420px";
34
+ root = insertNode(root, root.id, hero);
35
+ const body = createNode("richText");
36
+ body.props.html =
37
+ "<p>This is demo content generated by the seed script. Replace it using the page builder to publish your own articles, events, and resources.</p>" +
38
+ "<h3>What you'll get</h3><ul><li>Reusable sections and blocks</li><li>Performance-friendly motion</li><li>SEO fields and OpenGraph images</li></ul>";
39
+ root = insertNode(root, root.id, body);
40
+ return root;
41
+ }
42
+
43
+ const newsItems = [
44
+ {
45
+ slug: "record-fundraising-year",
46
+ title: "Federation Announces Record Fundraising Year",
47
+ excerpt:
48
+ "Member clubs raised more for our appeals in the last year than ever before.",
49
+ },
50
+ {
51
+ slug: "education-grant-opens",
52
+ title: "Education Grant Opens for New Applications",
53
+ excerpt:
54
+ "The Diamond Education Grant is now accepting applications for the next cohort.",
55
+ },
56
+ {
57
+ slug: "emergency-fund-response",
58
+ title: "Emergency Relief Fund Responds to Flooding",
59
+ excerpt:
60
+ "Rapid-response aid was distributed to affected communities within 72 hours.",
61
+ },
62
+ ];
63
+ for (const item of newsItems) {
64
+ await prisma.collectionItem.upsert({
65
+ where: {
66
+ collectionId_slug: { collectionId: newsCollection.id, slug: item.slug },
67
+ },
68
+ update: {
69
+ title: item.title,
70
+ excerpt: item.excerpt,
71
+ coverImage: PLACEHOLDER(item.title, "1200x630"),
72
+ content: buildItemContent(item.title, item.excerpt) as any,
73
+ status: "PUBLISHED",
74
+ publishedAt: now,
75
+ seoTitle: item.title,
76
+ seoDescription: item.excerpt,
77
+ ogImage: PLACEHOLDER(item.title, "1200x630"),
78
+ },
79
+ create: {
80
+ collectionId: newsCollection.id,
81
+ slug: item.slug,
82
+ title: item.title,
83
+ excerpt: item.excerpt,
84
+ coverImage: PLACEHOLDER(item.title, "1200x630"),
85
+ content: buildItemContent(item.title, item.excerpt) as any,
86
+ status: "PUBLISHED",
87
+ publishedAt: now,
88
+ seoTitle: item.title,
89
+ seoDescription: item.excerpt,
90
+ ogImage: PLACEHOLDER(item.title, "1200x630"),
91
+ },
92
+ });
93
+ }
94
+
95
+ const eventItems = [
96
+ {
97
+ slug: "global-convention-2026",
98
+ title: "Global Convention 2026",
99
+ excerpt: "Three days of workshops, speakers, and member-led sessions.",
100
+ },
101
+ {
102
+ slug: "regional-leadership-summit",
103
+ title: "Regional Leadership Summit",
104
+ excerpt: "Skill-building sessions for club leaders and new members.",
105
+ },
106
+ {
107
+ slug: "community-action-week",
108
+ title: "Community Action Week",
109
+ excerpt:
110
+ "A coordinated week of grassroots projects across local clubs worldwide.",
111
+ },
112
+ ];
113
+ for (const item of eventItems) {
114
+ await prisma.collectionItem.upsert({
115
+ where: {
116
+ collectionId_slug: {
117
+ collectionId: eventsCollection.id,
118
+ slug: item.slug,
119
+ },
120
+ },
121
+ update: {
122
+ title: item.title,
123
+ excerpt: item.excerpt,
124
+ coverImage: PLACEHOLDER(item.title, "1200x630"),
125
+ content: buildItemContent(item.title, item.excerpt) as any,
126
+ status: "PUBLISHED",
127
+ publishedAt: now,
128
+ seoTitle: item.title,
129
+ seoDescription: item.excerpt,
130
+ ogImage: PLACEHOLDER(item.title, "1200x630"),
131
+ },
132
+ create: {
133
+ collectionId: eventsCollection.id,
134
+ slug: item.slug,
135
+ title: item.title,
136
+ excerpt: item.excerpt,
137
+ coverImage: PLACEHOLDER(item.title, "1200x630"),
138
+ content: buildItemContent(item.title, item.excerpt) as any,
139
+ status: "PUBLISHED",
140
+ publishedAt: now,
141
+ seoTitle: item.title,
142
+ seoDescription: item.excerpt,
143
+ ogImage: PLACEHOLDER(item.title, "1200x630"),
144
+ },
145
+ });
146
+ }
147
+ }
148
+
149
+ async function main() {
150
+ const email = process.env.SEED_ADMIN_EMAIL ?? "admin@example.com";
151
+ const password = process.env.SEED_ADMIN_PASSWORD ?? "ChangeMe123!";
152
+
153
+ const existingAdmin = await prisma.user.findUnique({ where: { email } });
154
+ if (!existingAdmin) {
155
+ const passwordHash = await bcrypt.hash(password, 10);
156
+ await prisma.user.create({ data: { email, passwordHash, role: "ADMIN" } });
157
+ console.log(`Seeded admin user: ${email}`);
158
+ } else {
159
+ console.log(`Admin user already exists: ${email}`);
160
+ }
161
+
162
+ const themeCount = await prisma.siteTheme.count();
163
+ if (themeCount === 0) {
164
+ await prisma.siteTheme.create({
165
+ data: {
166
+ siteName: "Global Impact Alliance",
167
+ primaryColor: "#f97316",
168
+ secondaryColor: "#0b1220",
169
+ backgroundColor: "#ffffff",
170
+ textColor: "#111827",
171
+ headingFont: "Poppins",
172
+ bodyFont: "Inter",
173
+ logoUrl: "https://placehold.co/180x48/ffffff/0b1220?text=Global+Impact",
174
+ faviconUrl: "https://placehold.co/64x64/f97316/0b1220?text=G",
175
+ },
176
+ });
177
+ }
178
+
179
+ await seedDemoCollections();
180
+ await seedAdvancedHomepage(prisma);
181
+ await seedOurCharitiesPage(prisma);
182
+ }
183
+
184
+ main()
185
+ .catch((err) => {
186
+ console.error(err);
187
+ process.exit(1);
188
+ })
189
+ .finally(async () => {
190
+ await prisma.$disconnect();
191
+ });
@@ -0,0 +1,61 @@
1
+ import "dotenv/config";
2
+
3
+ function required(name: string): string {
4
+ const value = process.env[name];
5
+ if (!value) throw new Error(`Missing required env var: ${name}`);
6
+ return value;
7
+ }
8
+
9
+ export type StorageProviderName = "s3" | "azure" | "cloudinary";
10
+
11
+ // Defaults to "s3" so every install that predates this setting (no STORAGE_PROVIDER in
12
+ // its .env) keeps behaving exactly as before, unchanged.
13
+ const storageProvider = (process.env.STORAGE_PROVIDER || "s3") as StorageProviderName;
14
+
15
+ function s3Config() {
16
+ return {
17
+ endpoint: required("S3_ENDPOINT"),
18
+ region: process.env.S3_REGION ?? "us-east-1",
19
+ accessKeyId: required("S3_ACCESS_KEY_ID"),
20
+ secretAccessKey: required("S3_SECRET_ACCESS_KEY"),
21
+ bucket: required("S3_BUCKET"),
22
+ forcePathStyle: process.env.S3_FORCE_PATH_STYLE !== "false",
23
+ publicUrl: required("S3_PUBLIC_URL"),
24
+ };
25
+ }
26
+
27
+ function azureConfig() {
28
+ return {
29
+ connectionString: required("AZURE_STORAGE_CONNECTION_STRING"),
30
+ container: required("AZURE_STORAGE_CONTAINER"),
31
+ // Optional — set when serving through a CDN/custom domain instead of the storage
32
+ // account's own blob endpoint. Unset falls back to the blob client's own .url.
33
+ publicUrl: process.env.AZURE_STORAGE_PUBLIC_URL,
34
+ };
35
+ }
36
+
37
+ function cloudinaryConfig() {
38
+ return {
39
+ cloudName: required("CLOUDINARY_CLOUD_NAME"),
40
+ apiKey: required("CLOUDINARY_API_KEY"),
41
+ apiSecret: required("CLOUDINARY_API_SECRET"),
42
+ folder: process.env.CLOUDINARY_FOLDER || "pgcms",
43
+ };
44
+ }
45
+
46
+ export const env = {
47
+ port: Number(process.env.API_PORT ?? 4000),
48
+ webOrigin: process.env.WEB_ORIGIN ?? "http://localhost:3000",
49
+ jwtSecret: required("JWT_SECRET"),
50
+ jwtExpiresIn: process.env.JWT_EXPIRES_IN ?? "7d",
51
+ cookieName: process.env.COOKIE_NAME ?? "pgcms_token",
52
+ // Only the selected provider's vars are actually required — an S3 install is never
53
+ // forced to set Azure/Cloudinary credentials, and vice versa (see lib/storage/).
54
+ storageProvider,
55
+ s3: storageProvider === "s3" ? s3Config() : undefined,
56
+ azure: storageProvider === "azure" ? azureConfig() : undefined,
57
+ cloudinary: storageProvider === "cloudinary" ? cloudinaryConfig() : undefined,
58
+ formsWebhookUrl: process.env.FORMS_WEBHOOK_URL,
59
+ revalidateUrl: process.env.REVALIDATE_URL,
60
+ revalidateSecret: process.env.REVALIDATE_SECRET,
61
+ };