@nextsparkjs/theme-default 0.1.0-beta.1

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 (333) hide show
  1. package/about/business.md +49 -0
  2. package/about/features.json +302 -0
  3. package/about/team.md +79 -0
  4. package/api/ai/chat/stream/route.ts +212 -0
  5. package/api/ai/orchestrator/route.ts +226 -0
  6. package/api/ai/single-agent/route.ts +291 -0
  7. package/api/ai/usage/route.ts +122 -0
  8. package/blocks/benefits/component.tsx +100 -0
  9. package/blocks/benefits/config.ts +11 -0
  10. package/blocks/benefits/examples.ts +85 -0
  11. package/blocks/benefits/fields.ts +156 -0
  12. package/blocks/benefits/schema.ts +33 -0
  13. package/blocks/cta-section/component.tsx +100 -0
  14. package/blocks/cta-section/config.ts +11 -0
  15. package/blocks/cta-section/examples.ts +41 -0
  16. package/blocks/cta-section/fields.ts +89 -0
  17. package/blocks/cta-section/index.ts +6 -0
  18. package/blocks/cta-section/schema.ts +32 -0
  19. package/blocks/cta-section/thumbnail.png +1 -0
  20. package/blocks/faq-accordion/component.tsx +156 -0
  21. package/blocks/faq-accordion/config.ts +11 -0
  22. package/blocks/faq-accordion/examples.ts +77 -0
  23. package/blocks/faq-accordion/fields.ts +119 -0
  24. package/blocks/faq-accordion/index.ts +6 -0
  25. package/blocks/faq-accordion/schema.ts +45 -0
  26. package/blocks/features-grid/component.tsx +112 -0
  27. package/blocks/features-grid/config.ts +11 -0
  28. package/blocks/features-grid/examples.ts +63 -0
  29. package/blocks/features-grid/fields.ts +97 -0
  30. package/blocks/features-grid/index.ts +6 -0
  31. package/blocks/features-grid/schema.ts +40 -0
  32. package/blocks/features-grid/thumbnail.png +1 -0
  33. package/blocks/hero/component.tsx +100 -0
  34. package/blocks/hero/config.ts +11 -0
  35. package/blocks/hero/examples.ts +35 -0
  36. package/blocks/hero/fields.ts +60 -0
  37. package/blocks/hero/index.ts +6 -0
  38. package/blocks/hero/schema.ts +32 -0
  39. package/blocks/hero/thumbnail.png +1 -0
  40. package/blocks/hero/thumbnail.png.txt +6 -0
  41. package/blocks/hero-with-form/component.tsx +232 -0
  42. package/blocks/hero-with-form/config.ts +11 -0
  43. package/blocks/hero-with-form/examples.ts +16 -0
  44. package/blocks/hero-with-form/fields.ts +207 -0
  45. package/blocks/hero-with-form/index.ts +6 -0
  46. package/blocks/hero-with-form/schema.ts +54 -0
  47. package/blocks/jumbotron/component.tsx +136 -0
  48. package/blocks/jumbotron/config.ts +11 -0
  49. package/blocks/jumbotron/examples.ts +36 -0
  50. package/blocks/jumbotron/fields.ts +202 -0
  51. package/blocks/jumbotron/index.ts +6 -0
  52. package/blocks/jumbotron/schema.ts +55 -0
  53. package/blocks/logo-cloud/component.tsx +154 -0
  54. package/blocks/logo-cloud/config.ts +11 -0
  55. package/blocks/logo-cloud/examples.ts +34 -0
  56. package/blocks/logo-cloud/fields.ts +133 -0
  57. package/blocks/logo-cloud/index.ts +6 -0
  58. package/blocks/logo-cloud/schema.ts +46 -0
  59. package/blocks/post-content/component.tsx +197 -0
  60. package/blocks/post-content/config.ts +11 -0
  61. package/blocks/post-content/examples.ts +33 -0
  62. package/blocks/post-content/fields.ts +165 -0
  63. package/blocks/post-content/index.ts +4 -0
  64. package/blocks/post-content/schema.ts +46 -0
  65. package/blocks/pricing-table/component.tsx +154 -0
  66. package/blocks/pricing-table/config.ts +11 -0
  67. package/blocks/pricing-table/examples.ts +96 -0
  68. package/blocks/pricing-table/fields.ts +161 -0
  69. package/blocks/pricing-table/index.ts +4 -0
  70. package/blocks/pricing-table/schema.ts +50 -0
  71. package/blocks/split-content/component.tsx +135 -0
  72. package/blocks/split-content/config.ts +11 -0
  73. package/blocks/split-content/examples.ts +38 -0
  74. package/blocks/split-content/fields.ts +198 -0
  75. package/blocks/split-content/index.ts +6 -0
  76. package/blocks/split-content/schema.ts +67 -0
  77. package/blocks/stats-counter/component.tsx +124 -0
  78. package/blocks/stats-counter/config.ts +11 -0
  79. package/blocks/stats-counter/examples.ts +61 -0
  80. package/blocks/stats-counter/fields.ts +134 -0
  81. package/blocks/stats-counter/index.ts +6 -0
  82. package/blocks/stats-counter/schema.ts +47 -0
  83. package/blocks/testimonials/component.tsx +114 -0
  84. package/blocks/testimonials/config.ts +11 -0
  85. package/blocks/testimonials/examples.ts +65 -0
  86. package/blocks/testimonials/fields.ts +105 -0
  87. package/blocks/testimonials/index.ts +6 -0
  88. package/blocks/testimonials/schema.ts +41 -0
  89. package/blocks/testimonials/thumbnail.png +1 -0
  90. package/blocks/text-content/component.tsx +97 -0
  91. package/blocks/text-content/config.ts +11 -0
  92. package/blocks/text-content/examples.ts +30 -0
  93. package/blocks/text-content/fields.ts +88 -0
  94. package/blocks/text-content/index.ts +6 -0
  95. package/blocks/text-content/schema.ts +30 -0
  96. package/blocks/text-content/thumbnail.png +1 -0
  97. package/blocks/timeline/component.tsx +267 -0
  98. package/blocks/timeline/config.ts +11 -0
  99. package/blocks/timeline/examples.ts +68 -0
  100. package/blocks/timeline/fields.ts +147 -0
  101. package/blocks/timeline/index.ts +6 -0
  102. package/blocks/timeline/schema.ts +49 -0
  103. package/blocks/video-hero/component.tsx +270 -0
  104. package/blocks/video-hero/config.ts +11 -0
  105. package/blocks/video-hero/examples.ts +24 -0
  106. package/blocks/video-hero/fields.ts +98 -0
  107. package/blocks/video-hero/index.ts +6 -0
  108. package/blocks/video-hero/schema.ts +39 -0
  109. package/components/ai-chat/ChatPanel.tsx +575 -0
  110. package/components/ai-chat/ConversationItem.tsx +266 -0
  111. package/components/ai-chat/ConversationSidebar.tsx +99 -0
  112. package/components/ai-chat/MarkdownRenderer.tsx +15 -0
  113. package/components/ai-chat/Message.tsx +42 -0
  114. package/components/ai-chat/MessageInput.tsx +49 -0
  115. package/components/ai-chat/MessageList.tsx +46 -0
  116. package/components/ai-chat/TypingIndicator.tsx +11 -0
  117. package/config/app.config.ts +367 -0
  118. package/config/billing.config.ts +349 -0
  119. package/config/dashboard.config.ts +506 -0
  120. package/config/dev.config.ts +104 -0
  121. package/config/features.config.ts +203 -0
  122. package/config/flows.config.ts +129 -0
  123. package/config/permissions.config.ts +245 -0
  124. package/config/theme.config.ts +74 -0
  125. package/docs/01-overview/01-introduction.md +335 -0
  126. package/docs/01-overview/02-customization.md +671 -0
  127. package/docs/02-features/01-components.md +155 -0
  128. package/docs/02-features/02-styling.md +139 -0
  129. package/docs/02-features/03-tasks-entity.md +407 -0
  130. package/docs/03-ai/01-overview.md +211 -0
  131. package/docs/03-ai/02-customization.md +436 -0
  132. package/entities/customers/customers.config.ts +75 -0
  133. package/entities/customers/customers.fields.ts +165 -0
  134. package/entities/customers/customers.service.ts +516 -0
  135. package/entities/customers/customers.types.ts +83 -0
  136. package/entities/customers/messages/en.json +66 -0
  137. package/entities/customers/messages/es.json +66 -0
  138. package/entities/customers/migrations/001_customers_table.sql +102 -0
  139. package/entities/customers/migrations/002_customers_metas.sql +92 -0
  140. package/entities/pages/messages/en.json +41 -0
  141. package/entities/pages/messages/es.json +41 -0
  142. package/entities/pages/migrations/001_pages_table.sql +112 -0
  143. package/entities/pages/migrations/002_pages_metas.sql +56 -0
  144. package/entities/pages/migrations/003_add_status.sql +50 -0
  145. package/entities/pages/pages-management.service.ts +610 -0
  146. package/entities/pages/pages.config.ts +94 -0
  147. package/entities/pages/pages.fields.ts +101 -0
  148. package/entities/pages/pages.service.ts +290 -0
  149. package/entities/pages/pages.types.ts +124 -0
  150. package/entities/posts/components/post-header.tsx +97 -0
  151. package/entities/posts/messages/en.json +55 -0
  152. package/entities/posts/messages/es.json +55 -0
  153. package/entities/posts/migrations/001_posts_table.sql +115 -0
  154. package/entities/posts/migrations/003_add_status.sql +44 -0
  155. package/entities/posts/migrations/004_entity_taxonomy_relations.sql +129 -0
  156. package/entities/posts/migrations/006_posts_metas.sql +56 -0
  157. package/entities/posts/posts.config.ts +101 -0
  158. package/entities/posts/posts.fields.ts +116 -0
  159. package/entities/posts/posts.service.ts +376 -0
  160. package/entities/posts/posts.types.ts +74 -0
  161. package/entities/tasks/messages/en.json +204 -0
  162. package/entities/tasks/messages/es.json +204 -0
  163. package/entities/tasks/migrations/001_tasks_table.sql +105 -0
  164. package/entities/tasks/migrations/002_task_metas.sql +85 -0
  165. package/entities/tasks/migrations/sample_data.json +77 -0
  166. package/entities/tasks/tasks.config.ts +79 -0
  167. package/entities/tasks/tasks.fields.ts +196 -0
  168. package/entities/tasks/tasks.service.ts +541 -0
  169. package/entities/tasks/tasks.types.ts +56 -0
  170. package/lib/hooks/useAiChat.ts +114 -0
  171. package/lib/hooks/useConversations.ts +376 -0
  172. package/lib/hooks/useOrchestratorChat.ts +122 -0
  173. package/lib/hooks/usePersistentChat.ts +315 -0
  174. package/lib/hooks/useStreamingChat.ts +127 -0
  175. package/lib/hooks/useTokenUsage.ts +63 -0
  176. package/lib/langchain/agents/customer-assistant.md +69 -0
  177. package/lib/langchain/agents/index.ts +61 -0
  178. package/lib/langchain/agents/orchestrator.md +59 -0
  179. package/lib/langchain/agents/page-assistant.md +85 -0
  180. package/lib/langchain/agents/single-agent.md +46 -0
  181. package/lib/langchain/agents/task-assistant.md +55 -0
  182. package/lib/langchain/config.ts +45 -0
  183. package/lib/langchain/handlers/customer-handler.ts +338 -0
  184. package/lib/langchain/handlers/page-handler.ts +232 -0
  185. package/lib/langchain/handlers/task-handler.ts +323 -0
  186. package/lib/langchain/langchain.config.ts +223 -0
  187. package/lib/langchain/observability.config.ts +30 -0
  188. package/lib/langchain/orchestrator.ts +562 -0
  189. package/lib/langchain/tools/customers.ts +176 -0
  190. package/lib/langchain/tools/index.ts +10 -0
  191. package/lib/langchain/tools/orchestrator.ts +92 -0
  192. package/lib/langchain/tools/pages.ts +289 -0
  193. package/lib/langchain/tools/tasks.ts +167 -0
  194. package/lib/scheduled-actions/billing.ts +149 -0
  195. package/lib/scheduled-actions/index.ts +170 -0
  196. package/lib/scheduled-actions/webhook.ts +231 -0
  197. package/lib/selectors.ts +197 -0
  198. package/messages/de/admin.json +219 -0
  199. package/messages/de/aiUsage.json +36 -0
  200. package/messages/de/buttons.json +19 -0
  201. package/messages/de/categories.json +35 -0
  202. package/messages/de/common.json +16 -0
  203. package/messages/de/dev.json +101 -0
  204. package/messages/de/docs.json +27 -0
  205. package/messages/de/entities.json +7 -0
  206. package/messages/de/features.json +119 -0
  207. package/messages/de/footer.json +22 -0
  208. package/messages/de/home.json +57 -0
  209. package/messages/de/index.ts +39 -0
  210. package/messages/de/mobileNav.json +13 -0
  211. package/messages/de/navigation.json +8 -0
  212. package/messages/de/observability.json +74 -0
  213. package/messages/de/posts.json +54 -0
  214. package/messages/de/pricing.json +102 -0
  215. package/messages/de/support.json +9 -0
  216. package/messages/de/teams.json +8 -0
  217. package/messages/en/admin.json +219 -0
  218. package/messages/en/aiUsage.json +36 -0
  219. package/messages/en/buttons.json +19 -0
  220. package/messages/en/categories.json +35 -0
  221. package/messages/en/common.json +16 -0
  222. package/messages/en/dev.json +106 -0
  223. package/messages/en/docs.json +27 -0
  224. package/messages/en/entities.json +7 -0
  225. package/messages/en/features.json +119 -0
  226. package/messages/en/footer.json +22 -0
  227. package/messages/en/home.json +57 -0
  228. package/messages/en/index.ts +39 -0
  229. package/messages/en/mobileNav.json +13 -0
  230. package/messages/en/navigation.json +8 -0
  231. package/messages/en/observability.json +74 -0
  232. package/messages/en/posts.json +54 -0
  233. package/messages/en/pricing.json +102 -0
  234. package/messages/en/support.json +9 -0
  235. package/messages/en/teams.json +8 -0
  236. package/messages/es/admin.json +219 -0
  237. package/messages/es/aiUsage.json +36 -0
  238. package/messages/es/buttons.json +19 -0
  239. package/messages/es/categories.json +35 -0
  240. package/messages/es/common.json +16 -0
  241. package/messages/es/dev.json +101 -0
  242. package/messages/es/docs.json +27 -0
  243. package/messages/es/entities.json +7 -0
  244. package/messages/es/features.json +119 -0
  245. package/messages/es/footer.json +22 -0
  246. package/messages/es/home.json +57 -0
  247. package/messages/es/index.ts +39 -0
  248. package/messages/es/mobileNav.json +13 -0
  249. package/messages/es/navigation.json +8 -0
  250. package/messages/es/observability.json +74 -0
  251. package/messages/es/posts.json +54 -0
  252. package/messages/es/pricing.json +102 -0
  253. package/messages/es/support.json +9 -0
  254. package/messages/es/teams.json +8 -0
  255. package/messages/fr/admin.json +219 -0
  256. package/messages/fr/aiUsage.json +36 -0
  257. package/messages/fr/buttons.json +19 -0
  258. package/messages/fr/categories.json +35 -0
  259. package/messages/fr/common.json +16 -0
  260. package/messages/fr/dev.json +101 -0
  261. package/messages/fr/docs.json +27 -0
  262. package/messages/fr/entities.json +7 -0
  263. package/messages/fr/features.json +119 -0
  264. package/messages/fr/footer.json +22 -0
  265. package/messages/fr/home.json +57 -0
  266. package/messages/fr/index.ts +39 -0
  267. package/messages/fr/mobileNav.json +13 -0
  268. package/messages/fr/navigation.json +8 -0
  269. package/messages/fr/observability.json +74 -0
  270. package/messages/fr/posts.json +54 -0
  271. package/messages/fr/pricing.json +102 -0
  272. package/messages/fr/support.json +9 -0
  273. package/messages/fr/teams.json +8 -0
  274. package/messages/it/admin.json +219 -0
  275. package/messages/it/aiUsage.json +36 -0
  276. package/messages/it/buttons.json +19 -0
  277. package/messages/it/categories.json +35 -0
  278. package/messages/it/common.json +16 -0
  279. package/messages/it/dev.json +101 -0
  280. package/messages/it/docs.json +27 -0
  281. package/messages/it/entities.json +7 -0
  282. package/messages/it/features.json +119 -0
  283. package/messages/it/footer.json +22 -0
  284. package/messages/it/home.json +57 -0
  285. package/messages/it/index.ts +39 -0
  286. package/messages/it/mobileNav.json +13 -0
  287. package/messages/it/navigation.json +8 -0
  288. package/messages/it/observability.json +74 -0
  289. package/messages/it/posts.json +54 -0
  290. package/messages/it/pricing.json +102 -0
  291. package/messages/it/support.json +9 -0
  292. package/messages/it/teams.json +8 -0
  293. package/messages/pt/admin.json +219 -0
  294. package/messages/pt/aiUsage.json +36 -0
  295. package/messages/pt/buttons.json +19 -0
  296. package/messages/pt/categories.json +35 -0
  297. package/messages/pt/common.json +16 -0
  298. package/messages/pt/dev.json +101 -0
  299. package/messages/pt/docs.json +27 -0
  300. package/messages/pt/entities.json +7 -0
  301. package/messages/pt/features.json +119 -0
  302. package/messages/pt/footer.json +22 -0
  303. package/messages/pt/home.json +57 -0
  304. package/messages/pt/index.ts +39 -0
  305. package/messages/pt/mobileNav.json +13 -0
  306. package/messages/pt/navigation.json +8 -0
  307. package/messages/pt/observability.json +74 -0
  308. package/messages/pt/posts.json +54 -0
  309. package/messages/pt/pricing.json +102 -0
  310. package/messages/pt/support.json +9 -0
  311. package/messages/pt/teams.json +8 -0
  312. package/migrations/089_add_editor_team_role.sql +39 -0
  313. package/migrations/090_demo_users_teams.sql +540 -0
  314. package/migrations/091_greek_teams_billing.sql +523 -0
  315. package/migrations/092_billing_sample_data.sql +774 -0
  316. package/migrations/093_pages_sample_data.sql +1158 -0
  317. package/migrations/094_posts_sample_data.sql +278 -0
  318. package/migrations/095_tasks_sample_data.sql +440 -0
  319. package/migrations/096_customers_sample_data.sql +358 -0
  320. package/migrations/097_scheduled_actions_sample_data.sql +111 -0
  321. package/package.json +22 -0
  322. package/public/docs/desktop-layout-example.png +0 -0
  323. package/styles/components.css +11 -0
  324. package/styles/globals.css +179 -0
  325. package/templates/(public)/blog/[slug]/page.tsx +65 -0
  326. package/templates/(public)/layout.tsx +25 -0
  327. package/templates/(public)/page.tsx +200 -0
  328. package/templates/(public)/support/page.tsx +321 -0
  329. package/templates/dashboard/(main)/agent-multi/page.tsx +63 -0
  330. package/templates/dashboard/(main)/agent-single/page.tsx +142 -0
  331. package/templates/dashboard/(main)/settings/ai-usage/page.tsx +157 -0
  332. package/templates/superadmin/ai-observability/[traceId]/page.tsx +27 -0
  333. package/templates/superadmin/ai-observability/page.tsx +17 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Pages Entity Configuration
3
+ *
4
+ * Pages are builder-enabled entities that render at root URLs (e.g., /about, /contact).
5
+ * This configuration enables the WordPress-like page builder experience for content creation.
6
+ */
7
+
8
+ import { FileText } from 'lucide-react'
9
+ import type { EntityConfig } from '@nextsparkjs/core/lib/entities/types'
10
+ import { pagesFields } from './pages.fields'
11
+
12
+ export const pagesEntityConfig: EntityConfig = {
13
+ // ==========================================
14
+ // 1. BASIC IDENTIFICATION
15
+ // ==========================================
16
+ slug: 'pages',
17
+ enabled: true,
18
+
19
+ names: {
20
+ singular: 'page',
21
+ plural: 'Pages',
22
+ },
23
+
24
+ icon: FileText,
25
+ tableName: 'pages',
26
+
27
+ // ==========================================
28
+ // 2. ACCESS AND SCOPE CONFIGURATION
29
+ // ==========================================
30
+ access: {
31
+ public: true, // Pages are public content
32
+ api: true, // Has external API endpoints
33
+ metadata: false, // Pages don't use metadata system
34
+ shared: true, // All authenticated users can see all pages
35
+ basePath: '/', // Pages render at root: /about, /contact, etc.
36
+ },
37
+
38
+ // ==========================================
39
+ // 3. UI/UX FEATURES
40
+ // ==========================================
41
+ ui: {
42
+ dashboard: {
43
+ showInMenu: true,
44
+ showInTopbar: true,
45
+ filters: [
46
+ { field: 'status', type: 'multiSelect' },
47
+ ],
48
+ },
49
+ public: {
50
+ hasArchivePage: false, // Pages don't have an archive page
51
+ hasSinglePage: true, // Individual pages render at /[slug]
52
+ },
53
+ features: {
54
+ searchable: true,
55
+ sortable: true,
56
+ filterable: true,
57
+ bulkOperations: true,
58
+ importExport: false,
59
+ },
60
+ },
61
+
62
+ // ==========================================
63
+ // 4. PERMISSIONS SYSTEM
64
+ // ==========================================
65
+ // Permissions are now centralized in permissions.config.ts
66
+ // See: contents/themes/default/permissions.config.ts -> entities.pages
67
+
68
+ // ==========================================
69
+ // 6. BUILDER CONFIGURATION
70
+ // ==========================================
71
+ builder: {
72
+ enabled: true,
73
+ sidebarFields: ['locale'], // Pages don't have extra sidebar fields beyond basic settings
74
+ seo: true, // Enable SEO fields panel in editor
75
+ },
76
+
77
+ // ==========================================
78
+ // 7. TAXONOMIES CONFIGURATION
79
+ // ==========================================
80
+ taxonomies: {
81
+ enabled: false, // Pages don't use taxonomies by default
82
+ types: [],
83
+ },
84
+
85
+ // ==========================================
86
+ // FIELDS
87
+ // ==========================================
88
+ fields: pagesFields,
89
+
90
+ // ==========================================
91
+ // METADATA
92
+ // ==========================================
93
+ source: 'theme',
94
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Pages Entity Fields Configuration
3
+ *
4
+ * Fields required for builder-enabled pages.
5
+ * Note: 'blocks' field is NOT in fields[] - it's managed by the builder view automatically.
6
+ */
7
+
8
+ import type { EntityField } from '@nextsparkjs/core/lib/entities/types'
9
+
10
+ export const pagesFields: EntityField[] = [
11
+ {
12
+ name: 'title',
13
+ type: 'text',
14
+ required: true,
15
+ display: {
16
+ label: 'Title',
17
+ description: 'Page title',
18
+ placeholder: 'Enter page title...',
19
+ showInList: true,
20
+ showInDetail: true,
21
+ showInForm: true,
22
+ order: 1,
23
+ },
24
+ api: {
25
+ searchable: true,
26
+ sortable: true,
27
+ readOnly: false,
28
+ },
29
+ },
30
+ {
31
+ name: 'slug',
32
+ type: 'text',
33
+ required: true,
34
+ display: {
35
+ label: 'Slug',
36
+ description: 'URL-friendly identifier',
37
+ placeholder: 'page-slug',
38
+ showInList: true,
39
+ showInDetail: true,
40
+ showInForm: true,
41
+ order: 2,
42
+ },
43
+ api: {
44
+ searchable: true,
45
+ sortable: true,
46
+ readOnly: false,
47
+ },
48
+ },
49
+ {
50
+ name: 'status',
51
+ type: 'select',
52
+ required: true,
53
+ defaultValue: 'draft',
54
+ options: [
55
+ { value: 'draft', label: 'Draft' },
56
+ { value: 'published', label: 'Published' },
57
+ { value: 'scheduled', label: 'Scheduled' },
58
+ { value: 'archived', label: 'Archived' },
59
+ ],
60
+ display: {
61
+ label: 'Status',
62
+ description: 'Publication status',
63
+ showInList: true,
64
+ showInDetail: true,
65
+ showInForm: true,
66
+ order: 3,
67
+ },
68
+ api: {
69
+ searchable: false,
70
+ sortable: true,
71
+ filterable: true,
72
+ readOnly: false,
73
+ },
74
+ },
75
+ {
76
+ name: 'locale',
77
+ type: 'select',
78
+ required: true,
79
+ defaultValue: 'en',
80
+ options: [
81
+ { value: 'en', label: 'English' },
82
+ { value: 'es', label: 'Español' },
83
+ ],
84
+ display: {
85
+ label: 'Locale',
86
+ description: 'Page language',
87
+ showInList: true,
88
+ showInDetail: true,
89
+ showInForm: true,
90
+ order: 4,
91
+ },
92
+ api: {
93
+ searchable: false,
94
+ sortable: true,
95
+ filterable: true,
96
+ readOnly: false,
97
+ },
98
+ },
99
+ // Note: SEO fields (seoTitle, seoDescription, etc.) are managed by the builder SEO panel
100
+ // Note: 'blocks' field is a system field for builder-enabled entities - NOT defined here
101
+ ]
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Pages Service
3
+ *
4
+ * Provides data access methods for pages, separating public queries
5
+ * (without RLS) from authenticated queries (with RLS).
6
+ *
7
+ * @module PagesService
8
+ */
9
+
10
+ import { query, queryOne, queryOneWithRLS } from '@nextsparkjs/core/lib/db'
11
+ import type {
12
+ PagePublic,
13
+ PageMetadata,
14
+ PageListOptions,
15
+ PageListResult,
16
+ Block,
17
+ } from './pages.types'
18
+
19
+ // Database row type for page
20
+ interface DbPage {
21
+ id: string
22
+ slug: string
23
+ title: string
24
+ blocks: Block[]
25
+ locale: string
26
+ status: string
27
+ createdAt: string
28
+ }
29
+
30
+ // Database row type for metadata query
31
+ interface DbPageMetadata {
32
+ title: string
33
+ seoTitle: string | null
34
+ seoDescription: string | null
35
+ ogImage: string | null
36
+ }
37
+
38
+ export class PagesService {
39
+ // ============================================
40
+ // PUBLIC METHODS (sin RLS)
41
+ // ============================================
42
+
43
+ /**
44
+ * Get a published page by slug
45
+ *
46
+ * Fetches the full page data including blocks.
47
+ * Only returns published pages.
48
+ *
49
+ * @param slug - Page slug
50
+ * @param locale - Optional locale filter (defaults to 'en')
51
+ * @returns Page data or null if not found/not published
52
+ *
53
+ * @example
54
+ * const page = await PagesService.getPublishedBySlug('about')
55
+ * if (!page) notFound()
56
+ */
57
+ static async getPublishedBySlug(
58
+ slug: string,
59
+ locale: string = 'en'
60
+ ): Promise<PagePublic | null> {
61
+ try {
62
+ if (!slug || slug.trim() === '') {
63
+ throw new Error('Page slug is required')
64
+ }
65
+
66
+ const page = await queryOne<DbPage>(
67
+ `
68
+ SELECT
69
+ id,
70
+ slug,
71
+ title,
72
+ blocks,
73
+ locale,
74
+ status,
75
+ "createdAt"
76
+ FROM pages
77
+ WHERE slug = $1 AND status = 'published' AND locale = $2
78
+ `,
79
+ [slug, locale]
80
+ )
81
+
82
+ if (!page) {
83
+ return null
84
+ }
85
+
86
+ return {
87
+ id: page.id,
88
+ slug: page.slug,
89
+ title: page.title,
90
+ blocks: page.blocks,
91
+ locale: page.locale,
92
+ }
93
+ } catch (error) {
94
+ console.error('PagesService.getPublishedBySlug error:', error)
95
+ throw new Error(
96
+ error instanceof Error ? error.message : 'Failed to fetch page'
97
+ )
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Get page metadata for SEO/head tags
103
+ *
104
+ * Lightweight query that only fetches SEO-related fields.
105
+ * Used in generateMetadata for better performance.
106
+ *
107
+ * @param slug - Page slug
108
+ * @param locale - Optional locale filter (defaults to 'en')
109
+ * @returns Metadata or null if not found/not published
110
+ *
111
+ * @example
112
+ * const metadata = await PagesService.getPublishedMetadata('about')
113
+ * return { title: metadata?.seoTitle || metadata?.title }
114
+ */
115
+ static async getPublishedMetadata(
116
+ slug: string,
117
+ locale: string = 'en'
118
+ ): Promise<PageMetadata | null> {
119
+ try {
120
+ if (!slug || slug.trim() === '') {
121
+ throw new Error('Page slug is required')
122
+ }
123
+
124
+ const result = await query<DbPageMetadata>(
125
+ `
126
+ SELECT
127
+ title,
128
+ "seoTitle",
129
+ "seoDescription",
130
+ "ogImage"
131
+ FROM pages
132
+ WHERE slug = $1 AND status = 'published' AND locale = $2
133
+ `,
134
+ [slug, locale]
135
+ )
136
+
137
+ if (result.rows.length === 0) {
138
+ return null
139
+ }
140
+
141
+ const page = result.rows[0]
142
+
143
+ return {
144
+ title: page.title,
145
+ seoTitle: page.seoTitle ?? undefined,
146
+ seoDescription: page.seoDescription ?? undefined,
147
+ ogImage: page.ogImage ?? undefined,
148
+ }
149
+ } catch (error) {
150
+ console.error('PagesService.getPublishedMetadata error:', error)
151
+ throw new Error(
152
+ error instanceof Error ? error.message : 'Failed to fetch page metadata'
153
+ )
154
+ }
155
+ }
156
+
157
+ /**
158
+ * List published pages with pagination
159
+ *
160
+ * @param options - List options (limit, offset, locale, orderBy, orderDir)
161
+ * @returns Object with pages array and total count
162
+ *
163
+ * @example
164
+ * const { pages, total } = await PagesService.listPublished({ limit: 10 })
165
+ */
166
+ static async listPublished(
167
+ options: PageListOptions = {}
168
+ ): Promise<PageListResult> {
169
+ try {
170
+ const {
171
+ limit = 10,
172
+ offset = 0,
173
+ locale = 'en',
174
+ orderBy = 'createdAt',
175
+ orderDir = 'desc',
176
+ } = options
177
+
178
+ // Validate orderBy to prevent SQL injection
179
+ const validOrderBy = ['createdAt', 'title'].includes(orderBy) ? orderBy : 'createdAt'
180
+ const validOrderDir = orderDir === 'asc' ? 'ASC' : 'DESC'
181
+ const orderColumn = validOrderBy === 'createdAt' ? '"createdAt"' : 'title'
182
+
183
+ // Get total count
184
+ const countResult = await query<{ count: string }>(
185
+ `SELECT COUNT(*)::text as count FROM pages WHERE status = 'published' AND locale = $1`,
186
+ [locale]
187
+ )
188
+ const total = parseInt(countResult.rows[0]?.count || '0', 10)
189
+
190
+ // Get pages
191
+ const pagesResult = await query<DbPage>(
192
+ `
193
+ SELECT
194
+ id,
195
+ slug,
196
+ title,
197
+ blocks,
198
+ locale,
199
+ status,
200
+ "createdAt"
201
+ FROM pages
202
+ WHERE status = 'published' AND locale = $1
203
+ ORDER BY ${orderColumn} ${validOrderDir}
204
+ LIMIT $2 OFFSET $3
205
+ `,
206
+ [locale, limit, offset]
207
+ )
208
+
209
+ const pages: PagePublic[] = pagesResult.rows.map((page) => ({
210
+ id: page.id,
211
+ slug: page.slug,
212
+ title: page.title,
213
+ blocks: page.blocks,
214
+ locale: page.locale,
215
+ }))
216
+
217
+ return { pages, total }
218
+ } catch (error) {
219
+ console.error('PagesService.listPublished error:', error)
220
+ throw new Error(
221
+ error instanceof Error ? error.message : 'Failed to list pages'
222
+ )
223
+ }
224
+ }
225
+
226
+ // ============================================
227
+ // AUTHENTICATED METHODS (con RLS)
228
+ // ============================================
229
+
230
+ /**
231
+ * Get a page by ID (authenticated)
232
+ *
233
+ * For dashboard/admin views. Respects RLS policies.
234
+ *
235
+ * @param id - Page ID
236
+ * @param userId - Current user ID for RLS
237
+ * @returns Page data or null if not found/not authorized
238
+ *
239
+ * @example
240
+ * const page = await PagesService.getById('page-uuid', currentUserId)
241
+ */
242
+ static async getById(
243
+ id: string,
244
+ userId: string
245
+ ): Promise<PagePublic | null> {
246
+ try {
247
+ if (!id || id.trim() === '') {
248
+ throw new Error('Page ID is required')
249
+ }
250
+
251
+ if (!userId || userId.trim() === '') {
252
+ throw new Error('User ID is required for authentication')
253
+ }
254
+
255
+ const page = await queryOneWithRLS<DbPage>(
256
+ `
257
+ SELECT
258
+ id,
259
+ slug,
260
+ title,
261
+ blocks,
262
+ locale,
263
+ status,
264
+ "createdAt"
265
+ FROM pages
266
+ WHERE id = $1
267
+ `,
268
+ [id],
269
+ userId
270
+ )
271
+
272
+ if (!page) {
273
+ return null
274
+ }
275
+
276
+ return {
277
+ id: page.id,
278
+ slug: page.slug,
279
+ title: page.title,
280
+ blocks: page.blocks,
281
+ locale: page.locale,
282
+ }
283
+ } catch (error) {
284
+ console.error('PagesService.getById error:', error)
285
+ throw new Error(
286
+ error instanceof Error ? error.message : 'Failed to fetch page'
287
+ )
288
+ }
289
+ }
290
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Pages Service Types
3
+ *
4
+ * Type definitions for the PagesService.
5
+ * Separates public types (for rendering) from internal types.
6
+ *
7
+ * @module PagesTypes
8
+ */
9
+
10
+ /**
11
+ * Block structure for page builder content
12
+ */
13
+ export interface Block {
14
+ id: string
15
+ blockSlug: string
16
+ props: Record<string, unknown>
17
+ }
18
+
19
+ /**
20
+ * Page data for public rendering
21
+ * Includes blocks for full page display
22
+ */
23
+ export interface PagePublic {
24
+ id: string
25
+ slug: string
26
+ title: string
27
+ blocks: Block[]
28
+ locale: string
29
+ }
30
+
31
+ /**
32
+ * Lightweight page metadata for SEO/head tags
33
+ * Does not include blocks
34
+ */
35
+ export interface PageMetadata {
36
+ title: string
37
+ seoTitle?: string
38
+ seoDescription?: string
39
+ ogImage?: string
40
+ }
41
+
42
+ /**
43
+ * Options for listing published pages
44
+ */
45
+ export interface PageListOptions {
46
+ limit?: number
47
+ offset?: number
48
+ locale?: string
49
+ orderBy?: 'createdAt' | 'title'
50
+ orderDir?: 'asc' | 'desc'
51
+ }
52
+
53
+ /**
54
+ * Result of listing pages with pagination
55
+ */
56
+ export interface PageListResult {
57
+ pages: PagePublic[]
58
+ total: number
59
+ }
60
+
61
+ // ============================================
62
+ // MANAGEMENT TYPES (for authenticated CRUD)
63
+ // ============================================
64
+
65
+ /**
66
+ * Full page data including status and SEO fields
67
+ * Used for dashboard/admin operations
68
+ */
69
+ export interface PageFull extends PagePublic {
70
+ status: 'draft' | 'published'
71
+ seoTitle?: string
72
+ seoDescription?: string
73
+ ogImage?: string
74
+ createdAt: string
75
+ updatedAt: string
76
+ }
77
+
78
+ /**
79
+ * Data for creating a new page
80
+ */
81
+ export interface PageCreateData {
82
+ slug: string
83
+ title: string
84
+ blocks?: Block[]
85
+ locale?: string
86
+ status?: 'draft' | 'published'
87
+ seoTitle?: string
88
+ seoDescription?: string
89
+ ogImage?: string
90
+ teamId: string
91
+ }
92
+
93
+ /**
94
+ * Data for updating a page (metadata only, not blocks)
95
+ */
96
+ export interface PageUpdateData {
97
+ slug?: string
98
+ title?: string
99
+ status?: 'draft' | 'published'
100
+ seoTitle?: string
101
+ seoDescription?: string
102
+ ogImage?: string
103
+ }
104
+
105
+ /**
106
+ * Options for listing pages in management context
107
+ */
108
+ export interface PageManagementListOptions {
109
+ limit?: number
110
+ offset?: number
111
+ locale?: string
112
+ status?: 'draft' | 'published' | 'all'
113
+ orderBy?: 'createdAt' | 'updatedAt' | 'title'
114
+ orderDir?: 'asc' | 'desc'
115
+ teamId?: string
116
+ }
117
+
118
+ /**
119
+ * Result of listing pages in management context
120
+ */
121
+ export interface PageManagementListResult {
122
+ pages: PageFull[]
123
+ total: number
124
+ }
@@ -0,0 +1,97 @@
1
+ import { Badge } from '@nextsparkjs/core/components/ui/badge'
2
+ import Image from 'next/image'
3
+
4
+ interface Category {
5
+ id: string
6
+ name: string
7
+ color?: string
8
+ }
9
+
10
+ interface PostHeaderProps {
11
+ post: {
12
+ title: string
13
+ excerpt?: string
14
+ featuredImage?: string
15
+ createdAt: string
16
+ categories?: Category[]
17
+ }
18
+ }
19
+
20
+ export function PostHeader({ post }: PostHeaderProps) {
21
+ return (
22
+ <div className="w-full" data-cy="post-header">
23
+ {/* Featured Image */}
24
+ {post.featuredImage && (
25
+ <div className="relative w-full h-[400px] md:h-[500px] mb-8" data-cy="post-featured-image-display">
26
+ <Image
27
+ src={post.featuredImage}
28
+ alt={post.title}
29
+ fill
30
+ className="object-cover rounded-lg"
31
+ priority
32
+ />
33
+ </div>
34
+ )}
35
+
36
+ {/* Post Meta */}
37
+ <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 mb-8">
38
+ {/* Categories */}
39
+ {post.categories && post.categories.length > 0 && (
40
+ <div className="flex gap-2 mb-4" data-cy="post-categories-display">
41
+ {post.categories.map((category) => (
42
+ <Badge
43
+ key={category.id}
44
+ variant="outline"
45
+ style={{
46
+ backgroundColor: category.color ? `${category.color}20` : undefined,
47
+ borderColor: category.color || undefined,
48
+ color: category.color || undefined,
49
+ }}
50
+ >
51
+ {category.name}
52
+ </Badge>
53
+ ))}
54
+ </div>
55
+ )}
56
+
57
+ {/* Title */}
58
+ <h1
59
+ className="text-4xl md:text-5xl font-bold text-foreground mb-4"
60
+ data-cy="post-title"
61
+ >
62
+ {post.title}
63
+ </h1>
64
+
65
+ {/* Excerpt */}
66
+ {post.excerpt && (
67
+ <p
68
+ className="text-xl text-muted-foreground mb-4"
69
+ data-cy="post-excerpt"
70
+ >
71
+ {post.excerpt}
72
+ </p>
73
+ )}
74
+
75
+ {/* Date - using suppressHydrationWarning to prevent server/client mismatch */}
76
+ <div className="flex items-center gap-4 text-sm text-muted-foreground">
77
+ <time
78
+ dateTime={new Date(post.createdAt).toISOString()}
79
+ suppressHydrationWarning
80
+ >
81
+ {new Date(post.createdAt).toLocaleDateString('en-US', {
82
+ year: 'numeric',
83
+ month: 'long',
84
+ day: 'numeric',
85
+ timeZone: 'UTC',
86
+ })}
87
+ </time>
88
+ </div>
89
+ </div>
90
+
91
+ {/* Separator */}
92
+ <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
93
+ <hr className="border-border mb-8" />
94
+ </div>
95
+ </div>
96
+ )
97
+ }