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,752 @@
1
+ import "dotenv/config";
2
+ import { PrismaClient } from "@prisma/client";
3
+ import {
4
+ createEmptyPage,
5
+ createNode,
6
+ insertNode,
7
+ setContainerColumnCount,
8
+ } from "@pgcms/shared";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ // Builds a full homepage modeled on the section structure of a membership-driven
13
+ // nonprofit federation site (utility bar -> header -> mega-menu nav -> animated 3D
14
+ // hero -> quick actions -> about -> benefits -> impact stats -> testimonials -> news ->
15
+ // FAQ -> contact form -> join CTA -> rich footer). Most sections carry a scroll/load
16
+ // entrance animation, the hero uses the Three.js particle background, and "What We Do /
17
+ // Who We Are" uses the Tabs component. All copy below is original placeholder text for
18
+ // a fictional organization ("Global Impact Alliance") — not any real organization's
19
+ // actual wording, logo, or trademarks.
20
+
21
+ const PLACEHOLDER = (label: string, size = "800x500") =>
22
+ `https://placehold.co/${size}/1e293b/ffffff?text=${encodeURIComponent(label)}`;
23
+ const PLACEHOLDER_BG = (size: string) =>
24
+ `https://placehold.co/${size}/0b1220/0b1220?text=+`;
25
+ const SAMPLE_VIDEO =
26
+ "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
27
+ const IMG_HERO_1 =
28
+ "https://images.unsplash.com/photo-1526256262350-7da7584cf5eb?auto=format&fit=crop&w=2200&q=80";
29
+ const IMG_HERO_2 =
30
+ "https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=2200&q=80";
31
+ const IMG_HERO_3 =
32
+ "https://images.unsplash.com/photo-1503676260728-1c00da094a0b?auto=format&fit=crop&w=2200&q=80";
33
+ const IMG_CARD_1 =
34
+ "https://images.unsplash.com/photo-1559027615-028a7a5e9a0b?auto=format&fit=crop&w=1600&q=80";
35
+ const IMG_CARD_2 =
36
+ "https://images.unsplash.com/photo-1520975916090-3105956dac38?auto=format&fit=crop&w=1600&q=80";
37
+ const IMG_CARD_3 =
38
+ "https://images.unsplash.com/photo-1529156069898-49953e39b3ac?auto=format&fit=crop&w=1600&q=80";
39
+
40
+ export async function seedAdvancedHomepage(prisma: PrismaClient) {
41
+ // --- Navigation: rebuild with mega-menu dropdowns ---
42
+ const navCount = await prisma.navItem.count();
43
+ if (navCount === 0) {
44
+ const topLevel = [
45
+ { label: "Home", href: "/", order: 0 },
46
+ { label: "Who We Are", href: "#who-we-are", order: 10 },
47
+ { label: "What We Do", href: "#what-we-do", order: 20 },
48
+ { label: "Get Involved", href: "#get-involved", order: 30 },
49
+ { label: "Events", href: "#events", order: 40 },
50
+ { label: "News", href: "#news", order: 50 },
51
+ { label: "Contact Us", href: "#contact", order: 60 },
52
+ { label: "Members", href: "#members", order: 70 },
53
+ ];
54
+ const created: Record<string, string> = {};
55
+ for (const item of topLevel) {
56
+ const row = await prisma.navItem.create({ data: item });
57
+ created[item.label] = row.id;
58
+ }
59
+ const subItems = [
60
+ {
61
+ label: "Our History",
62
+ href: "#our-history",
63
+ parent: "Who We Are",
64
+ order: 0,
65
+ },
66
+ {
67
+ label: "Our Foundation",
68
+ href: "#our-foundation",
69
+ parent: "Who We Are",
70
+ order: 10,
71
+ },
72
+ {
73
+ label: "Our Charities",
74
+ href: "/our-charities",
75
+ parent: "What We Do",
76
+ order: 0,
77
+ },
78
+ {
79
+ label: "Our Impact",
80
+ href: "#our-impact",
81
+ parent: "What We Do",
82
+ order: 10,
83
+ },
84
+ {
85
+ label: "Become a Member",
86
+ href: "#become-a-member",
87
+ parent: "Get Involved",
88
+ order: 0,
89
+ },
90
+ {
91
+ label: "Volunteer",
92
+ href: "#volunteer",
93
+ parent: "Get Involved",
94
+ order: 10,
95
+ },
96
+ { label: "Donate", href: "#donate", parent: "Get Involved", order: 20 },
97
+ { label: "Upcoming Events", href: "#events", parent: "Events", order: 0 },
98
+ { label: "Latest News", href: "#news", parent: "News", order: 0 },
99
+ { label: "Press", href: "#press", parent: "News", order: 10 },
100
+ ];
101
+ for (const item of subItems) {
102
+ await prisma.navItem.create({
103
+ data: {
104
+ label: item.label,
105
+ href: item.href,
106
+ order: item.order,
107
+ parentId: created[item.parent],
108
+ },
109
+ });
110
+ }
111
+ }
112
+
113
+ let globalHeader = createEmptyPage();
114
+
115
+ // 1. Top utility bar
116
+ const utilityLeft = createNode("richText");
117
+ utilityLeft.props.html =
118
+ "<p><strong>Join Us</strong> — Together, we make that difference.</p>";
119
+ const utilityRight = createNode("buttonGroup");
120
+ utilityRight.props.buttons = [
121
+ { text: "Members", href: "#members", variant: "text" },
122
+ { text: "Club Finder", href: "#club-finder", variant: "text" },
123
+ { text: "Join Us", href: "#join", variant: "text" },
124
+ { text: "Donate", href: "#donate", variant: "primary" },
125
+ ];
126
+ let utilityBar = createNode("container");
127
+ utilityBar = setContainerColumnCount(utilityBar, 2);
128
+ utilityBar.style = {
129
+ backgroundColor: "#0f172a",
130
+ textColor: "#ffffff",
131
+ paddingY: "10px",
132
+ paddingX: "24px",
133
+ };
134
+ utilityBar.children[0].children.push(utilityLeft);
135
+ utilityBar.children[1].children.push(utilityRight);
136
+ utilityBar.children[1].style = { textAlign: "right" };
137
+ globalHeader = insertNode(globalHeader, globalHeader.id, utilityBar);
138
+
139
+ // 2. Header (logo, nav links, members + join buttons)
140
+ const header = createNode("header");
141
+ header.props.sticky = true;
142
+ header.style = {
143
+ backgroundColor: "#ffffff",
144
+ textColor: "#111827",
145
+ paddingY: "0px",
146
+ paddingX: "0px",
147
+ };
148
+ const startZone = header.children[0];
149
+ const middleZone = header.children[1];
150
+ const endZone = header.children[2];
151
+ const logo = createNode("logo");
152
+ const navLinks = createNode("navLinks");
153
+ startZone.children.push(logo);
154
+ middleZone.children.push(navLinks);
155
+ const headerButtons = createNode("buttonGroup");
156
+ headerButtons.props.buttons = [
157
+ { text: "Members", href: "#members", variant: "text" },
158
+ { text: "Join Us", href: "#join", variant: "primary" },
159
+ ];
160
+ const lang = createNode("languageSwitcher");
161
+ lang.props.compact = true;
162
+ let headerEndCluster = createNode("container");
163
+ headerEndCluster = setContainerColumnCount(headerEndCluster, 2);
164
+ headerEndCluster.style = { paddingY: "0px", paddingX: "0px" };
165
+ headerEndCluster.children[0].children.push(lang);
166
+ headerEndCluster.children[1].children.push(headerButtons);
167
+ headerEndCluster.children[1].style = { textAlign: "right" };
168
+ endZone.children.push(headerEndCluster);
169
+ globalHeader = insertNode(globalHeader, globalHeader.id, header);
170
+
171
+ await prisma.globalSection.upsert({
172
+ where: { kind: "header" },
173
+ update: { enabled: true, content: globalHeader as any },
174
+ create: { kind: "header", enabled: true, content: globalHeader as any },
175
+ });
176
+
177
+ let globalFooter = createEmptyPage();
178
+ const footer = createNode("footer");
179
+ footer.props.description =
180
+ "Global Impact Alliance is a worldwide federation of volunteers advancing education, equality, and " +
181
+ "opportunity for women and girls.";
182
+ footer.props.text =
183
+ "© 2026 Global Impact Alliance. All rights reserved. Registered charity no. 000000.";
184
+ footer.props.linkColumns = [
185
+ {
186
+ title: "Global Impact Websites",
187
+ links: [
188
+ { label: "Global Impact Foundation", href: "#foundation" },
189
+ { label: "Regional Federations", href: "#regions" },
190
+ { label: "Member Shop", href: "#shop" },
191
+ ],
192
+ },
193
+ ];
194
+ footer.props.contactInfo = {
195
+ address: "1 Federation Way, London, UK",
196
+ phone: "+1 (555) 010-2020",
197
+ email: "hello@globalimpactalliance.org",
198
+ extraLine: "Company No. 00000000 · Charity No. 000000",
199
+ };
200
+ footer.props.bottomLinks = [
201
+ { label: "Members Area", href: "#members" },
202
+ { label: "Find A Club", href: "#club-finder" },
203
+ { label: "Join Us", href: "#join" },
204
+ { label: "Donate", href: "#donate" },
205
+ { label: "Privacy Policy", href: "#privacy" },
206
+ { label: "Site Map", href: "#sitemap" },
207
+ { label: "Contact Us", href: "#contact" },
208
+ ];
209
+ footer.props.socialLinks = [
210
+ { platform: "facebook", url: "#" },
211
+ { platform: "twitter", url: "#" },
212
+ { platform: "instagram", url: "#" },
213
+ { platform: "linkedin", url: "#" },
214
+ { platform: "youtube", url: "#" },
215
+ ];
216
+ globalFooter = insertNode(globalFooter, globalFooter.id, footer);
217
+
218
+ await prisma.globalSection.upsert({
219
+ where: { kind: "footer" },
220
+ update: { enabled: true, content: globalFooter as any },
221
+ create: { kind: "footer", enabled: true, content: globalFooter as any },
222
+ });
223
+
224
+ // Go to Top - global section
225
+ let globalGoToTop = createEmptyPage();
226
+ const goToTop = createNode("goToTop");
227
+ goToTop.props.enabled = true;
228
+ goToTop.props.showAfterScroll = 300;
229
+ goToTop.style.backgroundColor = "#3b82f6";
230
+ goToTop.style.textColor = "#ffffff";
231
+ goToTop.style.paddingX = "16px";
232
+ goToTop.style.paddingY = "16px";
233
+ goToTop.style.borderRadius = "50%";
234
+ goToTop.style.boxShadow = "lg";
235
+ globalGoToTop = insertNode(globalGoToTop, globalGoToTop.id, goToTop);
236
+
237
+ await prisma.globalSection.upsert({
238
+ where: { kind: "goToTop" },
239
+ update: { enabled: true, content: globalGoToTop as any },
240
+ create: { kind: "goToTop", enabled: true, content: globalGoToTop as any },
241
+ });
242
+
243
+ let root = createEmptyPage();
244
+
245
+ const hero = createNode("slider");
246
+ hero.props.autoplay = true;
247
+ hero.props.intervalMs = 6500;
248
+ hero.props.slides = [
249
+ {
250
+ image: IMG_HERO_1,
251
+ heading: "Standing Up for Women and Girls",
252
+ subheading:
253
+ "Programs, advocacy, and member-led action in communities worldwide.",
254
+ buttonText: "Find out more",
255
+ buttonHref: "#who-we-are",
256
+ },
257
+ {
258
+ image: IMG_HERO_2,
259
+ heading: "A Global Network of Changemakers",
260
+ subheading:
261
+ "Connect locally, learn globally, and deliver measurable impact.",
262
+ buttonText: "Become a member",
263
+ buttonHref: "#become-a-member",
264
+ },
265
+ {
266
+ image: IMG_HERO_3,
267
+ heading: "Education. Opportunity. Safety.",
268
+ subheading:
269
+ "Support appeals that create lasting outcomes for women and girls.",
270
+ buttonText: "Donate",
271
+ buttonHref: "#donate",
272
+ },
273
+ ];
274
+ hero.style = {
275
+ minHeight: "620px",
276
+ paddingY: "0px",
277
+ paddingX: "0px",
278
+ textColor: "#ffffff",
279
+ animationType: "fadeIn",
280
+ animationTrigger: "onLoad",
281
+ animationDuration: 0.9,
282
+ };
283
+ root = insertNode(root, root.id, hero);
284
+
285
+ // 4. Three quick-action cards
286
+ const quickActions = createNode("cardGrid");
287
+ quickActions.props.heading = "";
288
+ quickActions.props.columns = 3;
289
+ quickActions.props.cards = [
290
+ {
291
+ image: IMG_CARD_1,
292
+ title: "Become A Member",
293
+ excerpt: "Join a global community of changemakers.",
294
+ buttonText: "Get Started",
295
+ href: "#become-a-member",
296
+ },
297
+ {
298
+ image: IMG_CARD_2,
299
+ title: "Find A Local Club",
300
+ excerpt: "Connect with members near you.",
301
+ buttonText: "Find A Club",
302
+ href: "#club-finder",
303
+ },
304
+ {
305
+ image: IMG_CARD_3,
306
+ title: "Donate",
307
+ excerpt: "Support the causes we champion worldwide.",
308
+ buttonText: "Donate Now",
309
+ href: "#donate",
310
+ },
311
+ ];
312
+ quickActions.style.customAttributes = { id: "get-involved" };
313
+ quickActions.style.paddingY = "48px";
314
+ quickActions.style.animationType = "slideUp";
315
+ quickActions.style.animationTrigger = "onScroll";
316
+ quickActions.style.animationStagger = 0.08;
317
+ quickActions.style.hoverEffect = "lift";
318
+ root = insertNode(root, root.id, quickActions);
319
+
320
+ // 5. About intro
321
+ const about = createNode("richText");
322
+ about.props.html =
323
+ "<h2>Who We Are</h2><p>Global Impact Alliance is a worldwide federation of volunteers working to transform the lives of women and girls through education, empowerment, and community action.</p><p>We support local clubs, coordinate international initiatives, and fund appeals that deliver real outcomes.</p>";
324
+ about.style.customAttributes = { id: "who-we-are" };
325
+ about.style.textAlign = "center";
326
+ about.style.animationType = "fadeIn";
327
+ about.style.animationTrigger = "onScroll";
328
+ root = insertNode(root, root.id, about);
329
+ const aboutLinks = createNode("buttonGroup");
330
+ aboutLinks.props.buttons = [
331
+ { text: "Join A Local Club", href: "#club-finder", variant: "outline" },
332
+ { text: "What We Do", href: "#what-we-do", variant: "outline" },
333
+ { text: "Who We Are", href: "#who-we-are", variant: "outline" },
334
+ ];
335
+ aboutLinks.style.animationType = "fadeIn";
336
+ aboutLinks.style.animationTrigger = "onScroll";
337
+ aboutLinks.style.animationDelay = 0.15;
338
+ root = insertNode(root, root.id, aboutLinks);
339
+
340
+ // 6. Find your local club
341
+ const findClub = createNode("cta");
342
+ findClub.props.heading = "Find Your Local Club";
343
+ findClub.props.subheading =
344
+ "With clubs in dozens of countries, there's likely one near you.";
345
+ findClub.props.buttonText = "View Club Finder";
346
+ findClub.props.buttonHref = "#club-finder";
347
+ findClub.style.customAttributes = { id: "club-finder" };
348
+ findClub.style = {
349
+ backgroundColor: "#f1f5f9",
350
+ textColor: "#111827",
351
+ paddingY: "56px",
352
+ animationType: "zoomIn",
353
+ animationTrigger: "onScroll",
354
+ };
355
+ root = insertNode(root, root.id, findClub);
356
+
357
+ // 7 & 8. What We Do / Who We Are — a Tabs component instead of a static 2-column
358
+ // layout, so a visitor picks one at a time rather than scanning both at once.
359
+ const whatWho = createNode("tabs");
360
+ whatWho.props.tabTransition = "slide";
361
+ whatWho.props.items = [
362
+ {
363
+ label: "What We Do",
364
+ content:
365
+ '<h3>What We Do</h3><p>We fund appeals and member-led projects spanning education, health, safety, and emergency response. Every campaign is chosen and supported with transparency and reporting.</p><p><a href="/our-charities">Explore our charities and appeals →</a></p>',
366
+ },
367
+ {
368
+ label: "Who We Are",
369
+ content:
370
+ '<h3>Who We Are</h3><p>We are a federation of independent local clubs united by a shared mission: creating opportunity for women and girls everywhere. Our members volunteer time, skills, and resources to deliver impact.</p><p><a href="#our-history">Learn our story →</a></p>',
371
+ },
372
+ ];
373
+ whatWho.style.customAttributes = { id: "what-we-do" };
374
+ whatWho.style.paddingY = "56px";
375
+ whatWho.style.animationType = "slideUp";
376
+ whatWho.style.animationTrigger = "onScroll";
377
+ root = insertNode(root, root.id, whatWho);
378
+
379
+ const charities = createNode("cardGrid");
380
+ charities.props.heading = "Our Charities & Appeals";
381
+ charities.props.columns = 3;
382
+ charities.props.cards = [
383
+ {
384
+ image:
385
+ "https://images.unsplash.com/photo-1520975682071-a7a18c79a2a6?auto=format&fit=crop&w=1600&q=80",
386
+ title: "Education Grants",
387
+ excerpt:
388
+ "Helping women and girls access education, skills, and training.",
389
+ buttonText: "Learn more",
390
+ href: "/our-charities",
391
+ },
392
+ {
393
+ image:
394
+ "https://images.unsplash.com/photo-1488521787991-ed7bbaae773c?auto=format&fit=crop&w=1600&q=80",
395
+ title: "Emergency Relief",
396
+ excerpt:
397
+ "Rapid response for communities impacted by disaster and crisis.",
398
+ buttonText: "Learn more",
399
+ href: "/our-charities",
400
+ },
401
+ {
402
+ image:
403
+ "https://images.unsplash.com/photo-1520975682071-a7a18c79a2a6?auto=format&fit=crop&w=1600&q=80",
404
+ title: "Health & Safety",
405
+ excerpt: "Programs supporting health equity, protection, and advocacy.",
406
+ buttonText: "Learn more",
407
+ href: "/our-charities",
408
+ },
409
+ ];
410
+ charities.style.animationType = "slideUp";
411
+ charities.style.animationTrigger = "onScroll";
412
+ charities.style.animationStagger = 0.06;
413
+ root = insertNode(root, root.id, charities);
414
+
415
+ const membershipBenefits = createNode("cardGrid");
416
+ membershipBenefits.props.heading = "Why Join";
417
+ membershipBenefits.props.columns = 4;
418
+ membershipBenefits.props.cards = [
419
+ {
420
+ icon: "Globe2",
421
+ title: "International Network",
422
+ excerpt: "Belong to a truly global community.",
423
+ buttonText: "",
424
+ href: "#become-a-member",
425
+ },
426
+ {
427
+ icon: "HeartHandshake",
428
+ title: "Volunteer Impact",
429
+ excerpt: "Hands-on projects that matter locally.",
430
+ buttonText: "",
431
+ href: "#become-a-member",
432
+ },
433
+ {
434
+ icon: "GraduationCap",
435
+ title: "Learn & Grow",
436
+ excerpt: "Workshops, training, and leadership skills.",
437
+ buttonText: "",
438
+ href: "#become-a-member",
439
+ },
440
+ {
441
+ icon: "Users",
442
+ title: "Meet Friends",
443
+ excerpt: "A lifelong network of support and purpose.",
444
+ buttonText: "",
445
+ href: "#become-a-member",
446
+ },
447
+ ];
448
+ membershipBenefits.style.customAttributes = { id: "become-a-member" };
449
+ membershipBenefits.style.animationType = "scrollReveal";
450
+ membershipBenefits.style.animationTrigger = "onScroll";
451
+ membershipBenefits.style.animationStagger = 0.05;
452
+ root = insertNode(root, root.id, membershipBenefits);
453
+
454
+ // 10. Impact stats
455
+ const stats = createNode("stats");
456
+ stats.props.columns = 4;
457
+ stats.props.items = [
458
+ { value: "2M+", label: "Women & Girls Helped" },
459
+ { value: "70K+", label: "Members Worldwide" },
460
+ { value: "25K+", label: "Federation Members" },
461
+ { value: "1.2K", label: "Local Clubs" },
462
+ ];
463
+ stats.style = {
464
+ backgroundColor: "#0f172a",
465
+ textColor: "#ffffff",
466
+ paddingY: "64px",
467
+ animationType: "zoomIn",
468
+ animationTrigger: "onScroll",
469
+ };
470
+ const statsHeading = createNode("richText");
471
+ statsHeading.props.html =
472
+ '<h2 style="text-align:center">Making A Difference Worldwide</h2>';
473
+ statsHeading.style = {
474
+ customAttributes: { id: "our-impact" },
475
+ backgroundColor: "#0f172a",
476
+ textColor: "#ffffff",
477
+ paddingY: "0px",
478
+ animationType: "fadeIn",
479
+ animationTrigger: "onScroll",
480
+ };
481
+ root = insertNode(root, root.id, statsHeading);
482
+ root = insertNode(root, root.id, stats);
483
+
484
+ const eventBanner = createNode("videoBackgroundSection");
485
+ eventBanner.props.heading = "Global Convention 2026";
486
+ eventBanner.props.subheading =
487
+ "Workshops, speakers, and member-led sessions across three days.";
488
+ eventBanner.props.buttonText = "View Agenda";
489
+ eventBanner.props.buttonHref = "#events";
490
+ eventBanner.style = {
491
+ customAttributes: { id: "events" },
492
+ backgroundVideo: SAMPLE_VIDEO,
493
+ backgroundOverlay: "rgba(11,18,32,0.65)",
494
+ textColor: "#ffffff",
495
+ minHeight: "520px",
496
+ paddingY: "96px",
497
+ animationType: "blurIn",
498
+ animationTrigger: "onScroll",
499
+ };
500
+ const countdown = createNode("countdown");
501
+ countdown.props.heading = "Countdown to Opening Keynote";
502
+ countdown.props.expiredText = "The event is live — welcome!";
503
+ countdown.props.buttonText = "Get Tickets";
504
+ countdown.props.buttonHref = "#events";
505
+ countdown.style = {
506
+ maxWidth: "860px",
507
+ marginY: "28px",
508
+ paddingY: "24px",
509
+ paddingX: "24px",
510
+ glass: true,
511
+ hoverEffect: "glow",
512
+ };
513
+ eventBanner.children.push(countdown);
514
+ root = insertNode(root, root.id, eventBanner);
515
+
516
+ const testimonialsSlider = createNode("slider");
517
+ testimonialsSlider.props.autoplay = true;
518
+ testimonialsSlider.props.slides = [
519
+ {
520
+ image: PLACEHOLDER_BG("1600x700"),
521
+ heading: "“A network that turns ideas into action.”",
522
+ subheading: "Regional leader, 2025",
523
+ buttonText: "",
524
+ buttonHref: "",
525
+ },
526
+ {
527
+ image: PLACEHOLDER_BG("1600x700"),
528
+ heading: "“The projects are real, measurable, and member-led.”",
529
+ subheading: "Club coordinator",
530
+ buttonText: "",
531
+ buttonHref: "",
532
+ },
533
+ {
534
+ image: PLACEHOLDER_BG("1600x700"),
535
+ heading: "“The community is global, welcoming, and ambitious.”",
536
+ subheading: "Member since 2018",
537
+ buttonText: "",
538
+ buttonHref: "",
539
+ },
540
+ ];
541
+ testimonialsSlider.style = {
542
+ paddingY: "0px",
543
+ animationType: "fadeIn",
544
+ animationTrigger: "onScroll",
545
+ };
546
+ root = insertNode(root, root.id, testimonialsSlider);
547
+
548
+ const eventsHeading = createNode("richText");
549
+ eventsHeading.props.html =
550
+ '<h2 style="text-align:center">Upcoming Events</h2><p style="text-align:center">Published items from the Events collection.</p>';
551
+ eventsHeading.style = {
552
+ paddingY: "56px",
553
+ paddingX: "24px",
554
+ animationType: "fadeIn",
555
+ animationTrigger: "onScroll",
556
+ };
557
+ root = insertNode(root, root.id, eventsHeading);
558
+ const events = createNode("collectionList");
559
+ events.props.collectionSlug = "events";
560
+ events.props.columns = 3;
561
+ events.props.limit = 6;
562
+ events.style = {
563
+ paddingY: "0px",
564
+ paddingX: "24px",
565
+ animationType: "slideUp",
566
+ animationTrigger: "onScroll",
567
+ animationStagger: 0.06,
568
+ };
569
+ root = insertNode(root, root.id, events);
570
+
571
+ const newsHeading = createNode("richText");
572
+ newsHeading.props.html =
573
+ '<h2 style="text-align:center">Latest News</h2><p style="text-align:center">Published items from the News collection.</p>';
574
+ newsHeading.style = {
575
+ customAttributes: { id: "news" },
576
+ paddingY: "56px",
577
+ paddingX: "24px",
578
+ animationType: "fadeIn",
579
+ animationTrigger: "onScroll",
580
+ };
581
+ root = insertNode(root, root.id, newsHeading);
582
+ const news = createNode("collectionList");
583
+ news.props.collectionSlug = "news";
584
+ news.props.columns = 3;
585
+ news.props.limit = 6;
586
+ news.style = {
587
+ paddingY: "0px",
588
+ paddingX: "24px",
589
+ animationType: "slideUp",
590
+ animationTrigger: "onScroll",
591
+ animationStagger: 0.06,
592
+ };
593
+ root = insertNode(root, root.id, news);
594
+
595
+ // 13. FAQ accordion
596
+ const faq = createNode("accordion");
597
+ faq.style.animationType = "fadeIn";
598
+ faq.style.animationTrigger = "onScroll";
599
+ faq.props.heading = "Frequently Asked Questions";
600
+ faq.props.items = [
601
+ {
602
+ question: "How do I join a local club?",
603
+ answer:
604
+ "Use our Club Finder to locate a club near you and reach out to its membership contact.",
605
+ },
606
+ {
607
+ question: "What does membership cost?",
608
+ answer:
609
+ "Dues vary by club and cover federation and club-level programs — your local club can share exact figures.",
610
+ },
611
+ {
612
+ question: "Can I volunteer without becoming a member?",
613
+ answer:
614
+ "Yes — many of our projects welcome volunteers who aren't yet members.",
615
+ },
616
+ {
617
+ question: "How are donations used?",
618
+ answer:
619
+ "Donations fund our appeals directly, with full transparency published in our annual report.",
620
+ },
621
+ {
622
+ question: "Do you operate outside your founding country?",
623
+ answer: "Yes, we have member clubs across dozens of countries worldwide.",
624
+ },
625
+ ];
626
+ root = insertNode(root, root.id, faq);
627
+
628
+ // 14. Contact form
629
+ let contactSection = createNode("container");
630
+ contactSection = setContainerColumnCount(contactSection, 2);
631
+ contactSection.style.paddingY = "56px";
632
+ contactSection.style.backgroundColor = "#f8fafc";
633
+ contactSection.style.customAttributes = { id: "contact" };
634
+ contactSection.style.animationType = "slideUp";
635
+ contactSection.style.animationTrigger = "onScroll";
636
+ const contactForm = createNode("contactForm");
637
+ contactForm.props.heading = "Contact Us";
638
+ contactForm.props.formName = "Homepage Contact";
639
+ const contactInfoText = createNode("richText");
640
+ contactInfoText.props.html =
641
+ "<h3>Head Office</h3><p>Have a question? Reach out directly:</p><p><strong>Phone:</strong> +1 (555) 010-2020<br/>" +
642
+ "<strong>Email:</strong> hello@globalimpactalliance.org</p>";
643
+ contactSection.children[0].children.push(contactForm);
644
+ contactSection.children[1].children.push(contactInfoText);
645
+ root = insertNode(root, root.id, contactSection);
646
+
647
+ // 15. Join CTA banner
648
+ const joinCta = createNode("cta");
649
+ joinCta.props.heading = "Join Global Impact Alliance Today";
650
+ joinCta.props.subheading =
651
+ "Add your voice to a worldwide community standing up for women and girls.";
652
+ joinCta.props.buttonText = "Join Us";
653
+ joinCta.props.buttonHref = "#join";
654
+ joinCta.style.animationType = "zoomIn";
655
+ joinCta.style.animationTrigger = "onScroll";
656
+ root = insertNode(root, root.id, joinCta);
657
+
658
+ const data = {
659
+ title: "Home",
660
+ status: "PUBLISHED" as const,
661
+ content: root as any,
662
+ seoTitle: "Global Impact Alliance — Standing Up for Women and Girls",
663
+ seoDescription:
664
+ "A worldwide federation of volunteers advancing education, equality, and opportunity for women and girls.",
665
+ ogImage: PLACEHOLDER("Global Impact Alliance", "1200x630"),
666
+ };
667
+
668
+ const existing = await prisma.page.findUnique({ where: { slug: "" } });
669
+ if (existing) {
670
+ await prisma.page.update({ where: { id: existing.id }, data });
671
+ const existingEs = await prisma.page.findFirst({
672
+ where: { translationOfId: existing.id, locale: "es" },
673
+ });
674
+ const existingAr = await prisma.page.findFirst({
675
+ where: { translationOfId: existing.id, locale: "ar" },
676
+ });
677
+ const base = await prisma.page.findUnique({ where: { slug: "" } });
678
+ if (base) {
679
+ const esData = {
680
+ ...data,
681
+ title: "Inicio",
682
+ locale: "es",
683
+ seoTitle: "Alianza de Impacto Global — Igualdad, salud y seguridad",
684
+ seoDescription:
685
+ "Un sitio demo multi-idioma creado con pg-cms, con animaciones modernas y contenido dinámico.",
686
+ translationOfId: base.id,
687
+ };
688
+ const arData = {
689
+ ...data,
690
+ title: "الرئيسية",
691
+ locale: "ar",
692
+ seoTitle: "تحالف الأثر العالمي — المساواة والصحة والسلامة",
693
+ seoDescription:
694
+ "موقع تجريبي متعدد اللغات مبني بـ pg-cms، مع حركات حديثة ومحتوى ديناميكي.",
695
+ translationOfId: base.id,
696
+ };
697
+ if (existingEs)
698
+ await prisma.page.update({
699
+ where: { id: existingEs.id },
700
+ data: { ...esData, slug: "es" },
701
+ });
702
+ else await prisma.page.create({ data: { ...esData, slug: "es" } });
703
+ if (existingAr)
704
+ await prisma.page.update({
705
+ where: { id: existingAr.id },
706
+ data: { ...arData, slug: "ar" },
707
+ });
708
+ else await prisma.page.create({ data: { ...arData, slug: "ar" } });
709
+ }
710
+ } else {
711
+ const created = await prisma.page.create({ data: { ...data, slug: "" } });
712
+ await prisma.page.create({
713
+ data: {
714
+ ...data,
715
+ slug: "es",
716
+ title: "Inicio",
717
+ locale: "es",
718
+ seoTitle: "Alianza de Impacto Global — Igualdad, salud y seguridad",
719
+ seoDescription:
720
+ "Un sitio demo multi-idioma creado con pg-cms, con animaciones modernas y contenido dinámico.",
721
+ translationOfId: created.id,
722
+ },
723
+ });
724
+ await prisma.page.create({
725
+ data: {
726
+ ...data,
727
+ slug: "ar",
728
+ title: "الرئيسية",
729
+ locale: "ar",
730
+ seoTitle: "تحالف الأثر العالمي — المساواة والصحة والسلامة",
731
+ seoDescription:
732
+ "موقع تجريبي متعدد اللغات مبني بـ pg-cms، مع حركات حديثة ومحتوى ديناميكي.",
733
+ translationOfId: created.id,
734
+ },
735
+ });
736
+ }
737
+ }
738
+
739
+ if (
740
+ process.argv[1] &&
741
+ path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
742
+ ) {
743
+ const prisma = new PrismaClient();
744
+ seedAdvancedHomepage(prisma)
745
+ .catch((err) => {
746
+ console.error(err);
747
+ process.exit(1);
748
+ })
749
+ .finally(async () => {
750
+ await prisma.$disconnect();
751
+ });
752
+ }