@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,376 @@
1
+ /**
2
+ * Posts Service
3
+ *
4
+ * Provides data access methods for posts, separating public queries
5
+ * (without RLS) from authenticated queries (with RLS).
6
+ *
7
+ * @module PostsService
8
+ */
9
+
10
+ import { query, queryOne, queryOneWithRLS } from '@nextsparkjs/core/lib/db'
11
+ import type {
12
+ PostPublic,
13
+ PostMetadata,
14
+ PostListOptions,
15
+ PostListResult,
16
+ Block,
17
+ PostCategory,
18
+ } from './posts.types'
19
+
20
+ // Database row type for post with categories
21
+ interface DbPostWithCategories {
22
+ id: string
23
+ slug: string
24
+ title: string
25
+ excerpt: string | null
26
+ featuredImage: string | null
27
+ blocks: Block[]
28
+ status: string
29
+ createdAt: string
30
+ categories: PostCategory[]
31
+ }
32
+
33
+ // Database row type for metadata query
34
+ interface DbPostMetadata {
35
+ title: string
36
+ excerpt: string | null
37
+ seoTitle: string | null
38
+ seoDescription: string | null
39
+ ogImage: string | null
40
+ featuredImage: string | null
41
+ }
42
+
43
+ export class PostsService {
44
+ // ============================================
45
+ // PUBLIC METHODS (sin RLS)
46
+ // ============================================
47
+
48
+ /**
49
+ * Get a published post by slug with categories
50
+ *
51
+ * Fetches the full post data including blocks and taxonomy relations.
52
+ * Only returns published posts.
53
+ *
54
+ * @param slug - Post slug
55
+ * @returns Post data or null if not found/not published
56
+ *
57
+ * @example
58
+ * const post = await PostsService.getPublishedBySlug('my-post-slug')
59
+ * if (!post) notFound()
60
+ */
61
+ static async getPublishedBySlug(slug: string): Promise<PostPublic | null> {
62
+ try {
63
+ if (!slug || slug.trim() === '') {
64
+ throw new Error('Post slug is required')
65
+ }
66
+
67
+ const post = await queryOne<DbPostWithCategories>(
68
+ `
69
+ SELECT
70
+ p.id,
71
+ p.slug,
72
+ p.title,
73
+ p.excerpt,
74
+ p."featuredImage",
75
+ p.blocks,
76
+ p.status,
77
+ p."createdAt",
78
+ COALESCE(
79
+ json_agg(
80
+ json_build_object(
81
+ 'id', t.id,
82
+ 'name', t.name,
83
+ 'slug', t.slug,
84
+ 'color', t.color
85
+ )
86
+ ) FILTER (WHERE t.id IS NOT NULL),
87
+ '[]'::json
88
+ ) as categories
89
+ FROM posts p
90
+ LEFT JOIN entity_taxonomy_relations etr ON p.id::text = etr."entityId" AND etr."entityType" = 'posts'
91
+ LEFT JOIN taxonomies t ON etr."taxonomyId" = t.id AND t."isActive" = true AND t."deletedAt" IS NULL
92
+ WHERE p.slug = $1 AND p.status = 'published'
93
+ GROUP BY p.id, p.slug, p.title, p.excerpt, p."featuredImage", p.blocks, p.status, p."createdAt"
94
+ `,
95
+ [slug]
96
+ )
97
+
98
+ if (!post) {
99
+ return null
100
+ }
101
+
102
+ return {
103
+ id: post.id,
104
+ slug: post.slug,
105
+ title: post.title,
106
+ excerpt: post.excerpt ?? undefined,
107
+ featuredImage: post.featuredImage ?? undefined,
108
+ blocks: post.blocks,
109
+ createdAt: post.createdAt,
110
+ categories: post.categories,
111
+ }
112
+ } catch (error) {
113
+ console.error('PostsService.getPublishedBySlug error:', error)
114
+ throw new Error(
115
+ error instanceof Error ? error.message : 'Failed to fetch post'
116
+ )
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Get post metadata for SEO/head tags
122
+ *
123
+ * Lightweight query that only fetches SEO-related fields.
124
+ * Used in generateMetadata for better performance.
125
+ *
126
+ * @param slug - Post slug
127
+ * @returns Metadata or null if not found/not published
128
+ *
129
+ * @example
130
+ * const metadata = await PostsService.getPublishedMetadata('my-post')
131
+ * return { title: metadata?.seoTitle || metadata?.title }
132
+ */
133
+ static async getPublishedMetadata(slug: string): Promise<PostMetadata | null> {
134
+ try {
135
+ if (!slug || slug.trim() === '') {
136
+ throw new Error('Post slug is required')
137
+ }
138
+
139
+ const result = await query<DbPostMetadata>(
140
+ `
141
+ SELECT
142
+ title,
143
+ excerpt,
144
+ "seoTitle",
145
+ "seoDescription",
146
+ "ogImage",
147
+ "featuredImage"
148
+ FROM posts
149
+ WHERE slug = $1 AND status = 'published'
150
+ `,
151
+ [slug]
152
+ )
153
+
154
+ if (result.rows.length === 0) {
155
+ return null
156
+ }
157
+
158
+ const post = result.rows[0]
159
+
160
+ return {
161
+ title: post.title,
162
+ excerpt: post.excerpt ?? undefined,
163
+ seoTitle: post.seoTitle ?? undefined,
164
+ seoDescription: post.seoDescription ?? undefined,
165
+ ogImage: post.ogImage ?? undefined,
166
+ featuredImage: post.featuredImage ?? undefined,
167
+ }
168
+ } catch (error) {
169
+ console.error('PostsService.getPublishedMetadata error:', error)
170
+ throw new Error(
171
+ error instanceof Error ? error.message : 'Failed to fetch post metadata'
172
+ )
173
+ }
174
+ }
175
+
176
+ /**
177
+ * List published posts with pagination and filtering
178
+ *
179
+ * Used for archive pages like /blog.
180
+ *
181
+ * @param options - List options (limit, offset, categorySlug, orderBy, orderDir)
182
+ * @returns Object with posts array and total count
183
+ *
184
+ * @example
185
+ * const { posts, total } = await PostsService.listPublished({
186
+ * limit: 10,
187
+ * offset: 0,
188
+ * categorySlug: 'news'
189
+ * })
190
+ */
191
+ static async listPublished(
192
+ options: PostListOptions = {}
193
+ ): Promise<PostListResult> {
194
+ try {
195
+ const {
196
+ limit = 10,
197
+ offset = 0,
198
+ categorySlug,
199
+ orderBy = 'createdAt',
200
+ orderDir = 'desc',
201
+ } = options
202
+
203
+ // Build WHERE clause
204
+ const conditions = ['p.status = $1']
205
+ const params: unknown[] = ['published']
206
+ let paramIndex = 2
207
+
208
+ // Category filter with subquery
209
+ if (categorySlug) {
210
+ conditions.push(`
211
+ EXISTS (
212
+ SELECT 1 FROM entity_taxonomy_relations etr
213
+ JOIN taxonomies t ON etr."taxonomyId" = t.id
214
+ WHERE etr."entityId" = p.id::text
215
+ AND etr."entityType" = 'posts'
216
+ AND t.slug = $${paramIndex}
217
+ AND t."isActive" = true
218
+ AND t."deletedAt" IS NULL
219
+ )
220
+ `)
221
+ params.push(categorySlug)
222
+ paramIndex++
223
+ }
224
+
225
+ const whereClause = conditions.join(' AND ')
226
+
227
+ // Validate orderBy to prevent SQL injection
228
+ const validOrderBy = ['createdAt', 'title'].includes(orderBy) ? orderBy : 'createdAt'
229
+ const validOrderDir = orderDir === 'asc' ? 'ASC' : 'DESC'
230
+ const orderColumn = validOrderBy === 'createdAt' ? '"createdAt"' : 'title'
231
+
232
+ // Get total count
233
+ const countResult = await query<{ count: string }>(
234
+ `SELECT COUNT(*)::text as count FROM posts p WHERE ${whereClause}`,
235
+ params
236
+ )
237
+ const total = parseInt(countResult.rows[0]?.count || '0', 10)
238
+
239
+ // Get posts with categories
240
+ params.push(limit, offset)
241
+ const postsResult = await query<DbPostWithCategories>(
242
+ `
243
+ SELECT
244
+ p.id,
245
+ p.slug,
246
+ p.title,
247
+ p.excerpt,
248
+ p."featuredImage",
249
+ p.blocks,
250
+ p.status,
251
+ p."createdAt",
252
+ COALESCE(
253
+ json_agg(
254
+ json_build_object(
255
+ 'id', t.id,
256
+ 'name', t.name,
257
+ 'slug', t.slug,
258
+ 'color', t.color
259
+ )
260
+ ) FILTER (WHERE t.id IS NOT NULL),
261
+ '[]'::json
262
+ ) as categories
263
+ FROM posts p
264
+ LEFT JOIN entity_taxonomy_relations etr ON p.id::text = etr."entityId" AND etr."entityType" = 'posts'
265
+ LEFT JOIN taxonomies t ON etr."taxonomyId" = t.id AND t."isActive" = true AND t."deletedAt" IS NULL
266
+ WHERE ${whereClause}
267
+ GROUP BY p.id, p.slug, p.title, p.excerpt, p."featuredImage", p.blocks, p.status, p."createdAt"
268
+ ORDER BY p.${orderColumn} ${validOrderDir}
269
+ LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
270
+ `,
271
+ params
272
+ )
273
+
274
+ const posts: PostPublic[] = postsResult.rows.map((post) => ({
275
+ id: post.id,
276
+ slug: post.slug,
277
+ title: post.title,
278
+ excerpt: post.excerpt ?? undefined,
279
+ featuredImage: post.featuredImage ?? undefined,
280
+ blocks: post.blocks,
281
+ createdAt: post.createdAt,
282
+ categories: post.categories,
283
+ }))
284
+
285
+ return { posts, total }
286
+ } catch (error) {
287
+ console.error('PostsService.listPublished error:', error)
288
+ throw new Error(
289
+ error instanceof Error ? error.message : 'Failed to list posts'
290
+ )
291
+ }
292
+ }
293
+
294
+ // ============================================
295
+ // AUTHENTICATED METHODS (con RLS)
296
+ // ============================================
297
+
298
+ /**
299
+ * Get a post by ID (authenticated)
300
+ *
301
+ * For dashboard/admin views. Respects RLS policies.
302
+ *
303
+ * @param id - Post ID
304
+ * @param userId - Current user ID for RLS
305
+ * @returns Post data or null if not found/not authorized
306
+ *
307
+ * @example
308
+ * const post = await PostsService.getById('post-uuid', currentUserId)
309
+ */
310
+ static async getById(
311
+ id: string,
312
+ userId: string
313
+ ): Promise<PostPublic | null> {
314
+ try {
315
+ if (!id || id.trim() === '') {
316
+ throw new Error('Post ID is required')
317
+ }
318
+
319
+ if (!userId || userId.trim() === '') {
320
+ throw new Error('User ID is required for authentication')
321
+ }
322
+
323
+ const post = await queryOneWithRLS<DbPostWithCategories>(
324
+ `
325
+ SELECT
326
+ p.id,
327
+ p.slug,
328
+ p.title,
329
+ p.excerpt,
330
+ p."featuredImage",
331
+ p.blocks,
332
+ p.status,
333
+ p."createdAt",
334
+ COALESCE(
335
+ json_agg(
336
+ json_build_object(
337
+ 'id', t.id,
338
+ 'name', t.name,
339
+ 'slug', t.slug,
340
+ 'color', t.color
341
+ )
342
+ ) FILTER (WHERE t.id IS NOT NULL),
343
+ '[]'::json
344
+ ) as categories
345
+ FROM posts p
346
+ LEFT JOIN entity_taxonomy_relations etr ON p.id::text = etr."entityId" AND etr."entityType" = 'posts'
347
+ LEFT JOIN taxonomies t ON etr."taxonomyId" = t.id AND t."isActive" = true AND t."deletedAt" IS NULL
348
+ WHERE p.id = $1
349
+ GROUP BY p.id, p.slug, p.title, p.excerpt, p."featuredImage", p.blocks, p.status, p."createdAt"
350
+ `,
351
+ [id],
352
+ userId
353
+ )
354
+
355
+ if (!post) {
356
+ return null
357
+ }
358
+
359
+ return {
360
+ id: post.id,
361
+ slug: post.slug,
362
+ title: post.title,
363
+ excerpt: post.excerpt ?? undefined,
364
+ featuredImage: post.featuredImage ?? undefined,
365
+ blocks: post.blocks,
366
+ createdAt: post.createdAt,
367
+ categories: post.categories,
368
+ }
369
+ } catch (error) {
370
+ console.error('PostsService.getById error:', error)
371
+ throw new Error(
372
+ error instanceof Error ? error.message : 'Failed to fetch post'
373
+ )
374
+ }
375
+ }
376
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Posts Service Types
3
+ *
4
+ * Type definitions for the PostsService.
5
+ * Separates public types (for rendering) from internal types.
6
+ *
7
+ * @module PostsTypes
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
+ * Category attached to a post (from taxonomy relations)
21
+ */
22
+ export interface PostCategory {
23
+ id: string
24
+ name: string
25
+ slug: string
26
+ color?: string
27
+ }
28
+
29
+ /**
30
+ * Post data for public rendering
31
+ * Includes blocks and categories for full page display
32
+ */
33
+ export interface PostPublic {
34
+ id: string
35
+ slug: string
36
+ title: string
37
+ excerpt?: string
38
+ featuredImage?: string
39
+ blocks: Block[]
40
+ createdAt: string
41
+ categories?: PostCategory[]
42
+ }
43
+
44
+ /**
45
+ * Lightweight post metadata for SEO/head tags
46
+ * Does not include blocks or categories
47
+ */
48
+ export interface PostMetadata {
49
+ title: string
50
+ excerpt?: string
51
+ seoTitle?: string
52
+ seoDescription?: string
53
+ ogImage?: string
54
+ featuredImage?: string
55
+ }
56
+
57
+ /**
58
+ * Options for listing published posts
59
+ */
60
+ export interface PostListOptions {
61
+ limit?: number
62
+ offset?: number
63
+ categorySlug?: string
64
+ orderBy?: 'createdAt' | 'title'
65
+ orderDir?: 'asc' | 'desc'
66
+ }
67
+
68
+ /**
69
+ * Result of listing posts with pagination
70
+ */
71
+ export interface PostListResult {
72
+ posts: PostPublic[]
73
+ total: number
74
+ }
@@ -0,0 +1,204 @@
1
+ {
2
+ "title": "My Tasks",
3
+ "subtitle": "Organize your work and keep control of your projects.",
4
+ "description": "Organize your work and keep control of your projects.",
5
+ "stats": {
6
+ "active": "Active",
7
+ "completed": "Completed",
8
+ "total": "Total Tasks",
9
+ "totalDescription": "All tasks in the system",
10
+ "pending": "Pending",
11
+ "pendingDescription": "Tasks waiting to be completed",
12
+ "overdue": "Overdue"
13
+ },
14
+ "form": {
15
+ "addTitle": "Add New Task",
16
+ "addDescription": "Create a new task to keep track of what needs to be done",
17
+ "titleLabel": "Task title",
18
+ "titlePlaceholder": "What needs to be done?",
19
+ "titleDescription": "Enter the title of the new task. This field is required.",
20
+ "descriptionLabel": "Task description (optional)",
21
+ "descriptionPlaceholder": "Add a description (optional)",
22
+ "descriptionHelp": "Optional description to provide more details about the task.",
23
+ "addButton": "Add Task",
24
+ "adding": "Adding...",
25
+ "enterTitle": "Enter a title to enable the create task button",
26
+ "creating": "Creating task, please wait",
27
+ "pressToCreate": "Press to create the new task",
28
+ "addSuccess": "Task added successfully!",
29
+ "addError": "Failed to add task",
30
+ "tryAgain": "Please try again later",
31
+ "taskCreated": "Task \"{title}\" created successfully",
32
+ "createError": "Error creating task \"{title}\""
33
+ },
34
+ "task": {
35
+ "completed": "completed",
36
+ "active": "active",
37
+ "deleteConfirm": "Are you sure you want to delete this task?",
38
+ "deleteSuccess": "Task deleted successfully",
39
+ "toggleSuccess": "Task status updated"
40
+ },
41
+ "overview": {
42
+ "title": "Tasks Overview",
43
+ "label": "Tasks summary",
44
+ "activeTasks": "{count} active tasks",
45
+ "completedTasks": "{count} completed tasks",
46
+ "totalTasks": "{count} total tasks"
47
+ },
48
+ "actions": {
49
+ "completed": "Task completed!",
50
+ "reactivated": "Task reactivated!",
51
+ "deleted": "Task deleted",
52
+ "markCompleted": "completed",
53
+ "markPending": "pending",
54
+ "taskStatusChanged": "Task \"{title}\" {action}",
55
+ "taskDeleted": "Task \"{title}\" deleted",
56
+ "toggleError": "Error changing status of task \"{title}\"",
57
+ "deleteError": "Error deleting task",
58
+ "updateError": "Error updating task"
59
+ },
60
+ "status": {
61
+ "completed": "Completed",
62
+ "pending": "Pending",
63
+ "active": "Active",
64
+ "todo": "To Do",
65
+ "in-progress": "In Progress",
66
+ "review": "In Review",
67
+ "done": "Done",
68
+ "blocked": "Blocked"
69
+ },
70
+ "priority": {
71
+ "low": "Low",
72
+ "medium": "Medium",
73
+ "high": "High",
74
+ "urgent": "Urgent"
75
+ },
76
+ "fields": {
77
+ "status": "Status",
78
+ "priority": "Priority",
79
+ "tags": "Tags",
80
+ "dueDate": "Due Date",
81
+ "estimatedHours": "Estimated Hours",
82
+ "tagsPlaceholder": "Add tags...",
83
+ "dueDatePlaceholder": "Select due date...",
84
+ "estimatedHoursPlaceholder": "0",
85
+ "statusDescription": "Current task status",
86
+ "priorityDescription": "Task priority level",
87
+ "tagsDescription": "Task tags for categorization",
88
+ "dueDateDescription": "Task deadline",
89
+ "estimatedHoursDescription": "Estimated time to complete (in hours)"
90
+ },
91
+ "sections": {
92
+ "activeTasks": "Active Tasks",
93
+ "completedTasks": "Completed Tasks"
94
+ },
95
+ "empty": {
96
+ "title": "No tasks yet",
97
+ "description": "Add your first task above to get started!"
98
+ },
99
+ "accessibility": {
100
+ "taskCard": "Task: {title}. {status}. Press Enter to view details.",
101
+ "completedTaskCard": "Completed task: {title}. Press Enter to view details.",
102
+ "toggleTask": "Mark task \"{title}\" as {action}",
103
+ "reactivateTask": "Reactivate completed task \"{title}\"",
104
+ "deleteTask": "Delete task \"{title}\"",
105
+ "deleteCompletedTask": "Delete completed task \"{title}\"",
106
+ "createdDate": "Created on {date}",
107
+ "completedDate": "Completed on {date}"
108
+ },
109
+ "time": {
110
+ "daysAgo": "{count} day{count, plural, one {} other {s}} ago",
111
+ "hoursAgo": "{count} hour{count, plural, one {} other {s}} ago",
112
+ "minutesAgo": "{count} minute{count, plural, one {} other {s}} ago",
113
+ "justNow": "Just now"
114
+ },
115
+ "page": {
116
+ "statsAriaLabel": "Task statistics",
117
+ "listTitle": "Tasks List",
118
+ "listDescription": "Manage your pending tasks and get better organized",
119
+ "today": "Today"
120
+ },
121
+ "list": {
122
+ "title": "Tasks List",
123
+ "description": "Manage all your tasks in one place",
124
+ "allTasks": "All Tasks"
125
+ },
126
+ "detail": {
127
+ "loading": {
128
+ "message": "Loading task details",
129
+ "ariaLabel": "Loading task details",
130
+ "srText": "Loading task details..."
131
+ },
132
+ "navigation": {
133
+ "backToTasks": "Back to Tasks",
134
+ "backAriaLabel": "Back to task list",
135
+ "returnNavigation": "Return navigation",
136
+ "viewAllTasks": "View all tasks",
137
+ "viewAllDescription": "Navigate back to the complete task list"
138
+ },
139
+ "error": {
140
+ "notFound": "Could not find the requested task. It may have been deleted or you don't have permissions to view it.",
141
+ "title": "Task not found"
142
+ },
143
+ "status": {
144
+ "completed": "Completed",
145
+ "pending": "Pending",
146
+ "active": "In progress",
147
+ "completedAriaLabel": "Task completed",
148
+ "pendingAriaLabel": "Task pending"
149
+ },
150
+ "priority": {
151
+ "high": "High",
152
+ "medium": "Medium",
153
+ "low": "Low",
154
+ "label": "Priority"
155
+ },
156
+ "categories": {
157
+ "design": "Design",
158
+ "development": "Development",
159
+ "meetings": "Meetings",
160
+ "documentation": "Documentation",
161
+ "testing": "Testing",
162
+ "general": "General"
163
+ },
164
+ "sections": {
165
+ "description": "Description",
166
+ "actions": "Actions",
167
+ "information": "Information",
168
+ "classification": "Classification",
169
+ "metadata": "Task metadata"
170
+ },
171
+ "actions": {
172
+ "title": "Actions",
173
+ "description": "Manage the status and options for this task. Keyboard shortcuts: Cmd+Enter to complete, Cmd+Backspace to delete.",
174
+ "markPending": "Mark Pending",
175
+ "markCompleted": "Mark Completed",
176
+ "delete": "Delete",
177
+ "markPendingAriaLabel": "Mark task \"{title}\" as pending. Shortcut: Cmd+Enter",
178
+ "markCompletedAriaLabel": "Mark task \"{title}\" as completed. Shortcut: Cmd+Enter",
179
+ "deleteAriaLabel": "Permanently delete task \"{title}\". Shortcut: Cmd+Backspace"
180
+ },
181
+ "info": {
182
+ "created": "Created",
183
+ "lastUpdate": "Last update",
184
+ "status": "Status",
185
+ "priority": "Priority",
186
+ "category": "Category",
187
+ "taskId": "Task ID",
188
+ "noDescription": "No description available for this task.",
189
+ "inferredInfo": "Automatically inferred information"
190
+ },
191
+ "messages": {
192
+ "taskCompleted": "Task \"{title}\" completed successfully",
193
+ "taskReactivated": "Task \"{title}\" reactivated successfully",
194
+ "taskDeleted": "Task \"{title}\" deleted successfully",
195
+ "toggleError": "Error changing status of task \"{title}\"",
196
+ "deleteError": "Error deleting task \"{title}\"",
197
+ "completedToast": "Task completed",
198
+ "reactivatedToast": "Task reactivated",
199
+ "deletedToast": "Task deleted",
200
+ "updateErrorToast": "Error updating task",
201
+ "deleteErrorToast": "Error deleting task"
202
+ }
203
+ }
204
+ }