@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.
- package/about/business.md +49 -0
- package/about/features.json +302 -0
- package/about/team.md +79 -0
- package/api/ai/chat/stream/route.ts +212 -0
- package/api/ai/orchestrator/route.ts +226 -0
- package/api/ai/single-agent/route.ts +291 -0
- package/api/ai/usage/route.ts +122 -0
- package/blocks/benefits/component.tsx +100 -0
- package/blocks/benefits/config.ts +11 -0
- package/blocks/benefits/examples.ts +85 -0
- package/blocks/benefits/fields.ts +156 -0
- package/blocks/benefits/schema.ts +33 -0
- package/blocks/cta-section/component.tsx +100 -0
- package/blocks/cta-section/config.ts +11 -0
- package/blocks/cta-section/examples.ts +41 -0
- package/blocks/cta-section/fields.ts +89 -0
- package/blocks/cta-section/index.ts +6 -0
- package/blocks/cta-section/schema.ts +32 -0
- package/blocks/cta-section/thumbnail.png +1 -0
- package/blocks/faq-accordion/component.tsx +156 -0
- package/blocks/faq-accordion/config.ts +11 -0
- package/blocks/faq-accordion/examples.ts +77 -0
- package/blocks/faq-accordion/fields.ts +119 -0
- package/blocks/faq-accordion/index.ts +6 -0
- package/blocks/faq-accordion/schema.ts +45 -0
- package/blocks/features-grid/component.tsx +112 -0
- package/blocks/features-grid/config.ts +11 -0
- package/blocks/features-grid/examples.ts +63 -0
- package/blocks/features-grid/fields.ts +97 -0
- package/blocks/features-grid/index.ts +6 -0
- package/blocks/features-grid/schema.ts +40 -0
- package/blocks/features-grid/thumbnail.png +1 -0
- package/blocks/hero/component.tsx +100 -0
- package/blocks/hero/config.ts +11 -0
- package/blocks/hero/examples.ts +35 -0
- package/blocks/hero/fields.ts +60 -0
- package/blocks/hero/index.ts +6 -0
- package/blocks/hero/schema.ts +32 -0
- package/blocks/hero/thumbnail.png +1 -0
- package/blocks/hero/thumbnail.png.txt +6 -0
- package/blocks/hero-with-form/component.tsx +232 -0
- package/blocks/hero-with-form/config.ts +11 -0
- package/blocks/hero-with-form/examples.ts +16 -0
- package/blocks/hero-with-form/fields.ts +207 -0
- package/blocks/hero-with-form/index.ts +6 -0
- package/blocks/hero-with-form/schema.ts +54 -0
- package/blocks/jumbotron/component.tsx +136 -0
- package/blocks/jumbotron/config.ts +11 -0
- package/blocks/jumbotron/examples.ts +36 -0
- package/blocks/jumbotron/fields.ts +202 -0
- package/blocks/jumbotron/index.ts +6 -0
- package/blocks/jumbotron/schema.ts +55 -0
- package/blocks/logo-cloud/component.tsx +154 -0
- package/blocks/logo-cloud/config.ts +11 -0
- package/blocks/logo-cloud/examples.ts +34 -0
- package/blocks/logo-cloud/fields.ts +133 -0
- package/blocks/logo-cloud/index.ts +6 -0
- package/blocks/logo-cloud/schema.ts +46 -0
- package/blocks/post-content/component.tsx +197 -0
- package/blocks/post-content/config.ts +11 -0
- package/blocks/post-content/examples.ts +33 -0
- package/blocks/post-content/fields.ts +165 -0
- package/blocks/post-content/index.ts +4 -0
- package/blocks/post-content/schema.ts +46 -0
- package/blocks/pricing-table/component.tsx +154 -0
- package/blocks/pricing-table/config.ts +11 -0
- package/blocks/pricing-table/examples.ts +96 -0
- package/blocks/pricing-table/fields.ts +161 -0
- package/blocks/pricing-table/index.ts +4 -0
- package/blocks/pricing-table/schema.ts +50 -0
- package/blocks/split-content/component.tsx +135 -0
- package/blocks/split-content/config.ts +11 -0
- package/blocks/split-content/examples.ts +38 -0
- package/blocks/split-content/fields.ts +198 -0
- package/blocks/split-content/index.ts +6 -0
- package/blocks/split-content/schema.ts +67 -0
- package/blocks/stats-counter/component.tsx +124 -0
- package/blocks/stats-counter/config.ts +11 -0
- package/blocks/stats-counter/examples.ts +61 -0
- package/blocks/stats-counter/fields.ts +134 -0
- package/blocks/stats-counter/index.ts +6 -0
- package/blocks/stats-counter/schema.ts +47 -0
- package/blocks/testimonials/component.tsx +114 -0
- package/blocks/testimonials/config.ts +11 -0
- package/blocks/testimonials/examples.ts +65 -0
- package/blocks/testimonials/fields.ts +105 -0
- package/blocks/testimonials/index.ts +6 -0
- package/blocks/testimonials/schema.ts +41 -0
- package/blocks/testimonials/thumbnail.png +1 -0
- package/blocks/text-content/component.tsx +97 -0
- package/blocks/text-content/config.ts +11 -0
- package/blocks/text-content/examples.ts +30 -0
- package/blocks/text-content/fields.ts +88 -0
- package/blocks/text-content/index.ts +6 -0
- package/blocks/text-content/schema.ts +30 -0
- package/blocks/text-content/thumbnail.png +1 -0
- package/blocks/timeline/component.tsx +267 -0
- package/blocks/timeline/config.ts +11 -0
- package/blocks/timeline/examples.ts +68 -0
- package/blocks/timeline/fields.ts +147 -0
- package/blocks/timeline/index.ts +6 -0
- package/blocks/timeline/schema.ts +49 -0
- package/blocks/video-hero/component.tsx +270 -0
- package/blocks/video-hero/config.ts +11 -0
- package/blocks/video-hero/examples.ts +24 -0
- package/blocks/video-hero/fields.ts +98 -0
- package/blocks/video-hero/index.ts +6 -0
- package/blocks/video-hero/schema.ts +39 -0
- package/components/ai-chat/ChatPanel.tsx +575 -0
- package/components/ai-chat/ConversationItem.tsx +266 -0
- package/components/ai-chat/ConversationSidebar.tsx +99 -0
- package/components/ai-chat/MarkdownRenderer.tsx +15 -0
- package/components/ai-chat/Message.tsx +42 -0
- package/components/ai-chat/MessageInput.tsx +49 -0
- package/components/ai-chat/MessageList.tsx +46 -0
- package/components/ai-chat/TypingIndicator.tsx +11 -0
- package/config/app.config.ts +367 -0
- package/config/billing.config.ts +349 -0
- package/config/dashboard.config.ts +506 -0
- package/config/dev.config.ts +104 -0
- package/config/features.config.ts +203 -0
- package/config/flows.config.ts +129 -0
- package/config/permissions.config.ts +245 -0
- package/config/theme.config.ts +74 -0
- package/docs/01-overview/01-introduction.md +335 -0
- package/docs/01-overview/02-customization.md +671 -0
- package/docs/02-features/01-components.md +155 -0
- package/docs/02-features/02-styling.md +139 -0
- package/docs/02-features/03-tasks-entity.md +407 -0
- package/docs/03-ai/01-overview.md +211 -0
- package/docs/03-ai/02-customization.md +436 -0
- package/entities/customers/customers.config.ts +75 -0
- package/entities/customers/customers.fields.ts +165 -0
- package/entities/customers/customers.service.ts +516 -0
- package/entities/customers/customers.types.ts +83 -0
- package/entities/customers/messages/en.json +66 -0
- package/entities/customers/messages/es.json +66 -0
- package/entities/customers/migrations/001_customers_table.sql +102 -0
- package/entities/customers/migrations/002_customers_metas.sql +92 -0
- package/entities/pages/messages/en.json +41 -0
- package/entities/pages/messages/es.json +41 -0
- package/entities/pages/migrations/001_pages_table.sql +112 -0
- package/entities/pages/migrations/002_pages_metas.sql +56 -0
- package/entities/pages/migrations/003_add_status.sql +50 -0
- package/entities/pages/pages-management.service.ts +610 -0
- package/entities/pages/pages.config.ts +94 -0
- package/entities/pages/pages.fields.ts +101 -0
- package/entities/pages/pages.service.ts +290 -0
- package/entities/pages/pages.types.ts +124 -0
- package/entities/posts/components/post-header.tsx +97 -0
- package/entities/posts/messages/en.json +55 -0
- package/entities/posts/messages/es.json +55 -0
- package/entities/posts/migrations/001_posts_table.sql +115 -0
- package/entities/posts/migrations/003_add_status.sql +44 -0
- package/entities/posts/migrations/004_entity_taxonomy_relations.sql +129 -0
- package/entities/posts/migrations/006_posts_metas.sql +56 -0
- package/entities/posts/posts.config.ts +101 -0
- package/entities/posts/posts.fields.ts +116 -0
- package/entities/posts/posts.service.ts +376 -0
- package/entities/posts/posts.types.ts +74 -0
- package/entities/tasks/messages/en.json +204 -0
- package/entities/tasks/messages/es.json +204 -0
- package/entities/tasks/migrations/001_tasks_table.sql +105 -0
- package/entities/tasks/migrations/002_task_metas.sql +85 -0
- package/entities/tasks/migrations/sample_data.json +77 -0
- package/entities/tasks/tasks.config.ts +79 -0
- package/entities/tasks/tasks.fields.ts +196 -0
- package/entities/tasks/tasks.service.ts +541 -0
- package/entities/tasks/tasks.types.ts +56 -0
- package/lib/hooks/useAiChat.ts +114 -0
- package/lib/hooks/useConversations.ts +376 -0
- package/lib/hooks/useOrchestratorChat.ts +122 -0
- package/lib/hooks/usePersistentChat.ts +315 -0
- package/lib/hooks/useStreamingChat.ts +127 -0
- package/lib/hooks/useTokenUsage.ts +63 -0
- package/lib/langchain/agents/customer-assistant.md +69 -0
- package/lib/langchain/agents/index.ts +61 -0
- package/lib/langchain/agents/orchestrator.md +59 -0
- package/lib/langchain/agents/page-assistant.md +85 -0
- package/lib/langchain/agents/single-agent.md +46 -0
- package/lib/langchain/agents/task-assistant.md +55 -0
- package/lib/langchain/config.ts +45 -0
- package/lib/langchain/handlers/customer-handler.ts +338 -0
- package/lib/langchain/handlers/page-handler.ts +232 -0
- package/lib/langchain/handlers/task-handler.ts +323 -0
- package/lib/langchain/langchain.config.ts +223 -0
- package/lib/langchain/observability.config.ts +30 -0
- package/lib/langchain/orchestrator.ts +562 -0
- package/lib/langchain/tools/customers.ts +176 -0
- package/lib/langchain/tools/index.ts +10 -0
- package/lib/langchain/tools/orchestrator.ts +92 -0
- package/lib/langchain/tools/pages.ts +289 -0
- package/lib/langchain/tools/tasks.ts +167 -0
- package/lib/scheduled-actions/billing.ts +149 -0
- package/lib/scheduled-actions/index.ts +170 -0
- package/lib/scheduled-actions/webhook.ts +231 -0
- package/lib/selectors.ts +197 -0
- package/messages/de/admin.json +219 -0
- package/messages/de/aiUsage.json +36 -0
- package/messages/de/buttons.json +19 -0
- package/messages/de/categories.json +35 -0
- package/messages/de/common.json +16 -0
- package/messages/de/dev.json +101 -0
- package/messages/de/docs.json +27 -0
- package/messages/de/entities.json +7 -0
- package/messages/de/features.json +119 -0
- package/messages/de/footer.json +22 -0
- package/messages/de/home.json +57 -0
- package/messages/de/index.ts +39 -0
- package/messages/de/mobileNav.json +13 -0
- package/messages/de/navigation.json +8 -0
- package/messages/de/observability.json +74 -0
- package/messages/de/posts.json +54 -0
- package/messages/de/pricing.json +102 -0
- package/messages/de/support.json +9 -0
- package/messages/de/teams.json +8 -0
- package/messages/en/admin.json +219 -0
- package/messages/en/aiUsage.json +36 -0
- package/messages/en/buttons.json +19 -0
- package/messages/en/categories.json +35 -0
- package/messages/en/common.json +16 -0
- package/messages/en/dev.json +106 -0
- package/messages/en/docs.json +27 -0
- package/messages/en/entities.json +7 -0
- package/messages/en/features.json +119 -0
- package/messages/en/footer.json +22 -0
- package/messages/en/home.json +57 -0
- package/messages/en/index.ts +39 -0
- package/messages/en/mobileNav.json +13 -0
- package/messages/en/navigation.json +8 -0
- package/messages/en/observability.json +74 -0
- package/messages/en/posts.json +54 -0
- package/messages/en/pricing.json +102 -0
- package/messages/en/support.json +9 -0
- package/messages/en/teams.json +8 -0
- package/messages/es/admin.json +219 -0
- package/messages/es/aiUsage.json +36 -0
- package/messages/es/buttons.json +19 -0
- package/messages/es/categories.json +35 -0
- package/messages/es/common.json +16 -0
- package/messages/es/dev.json +101 -0
- package/messages/es/docs.json +27 -0
- package/messages/es/entities.json +7 -0
- package/messages/es/features.json +119 -0
- package/messages/es/footer.json +22 -0
- package/messages/es/home.json +57 -0
- package/messages/es/index.ts +39 -0
- package/messages/es/mobileNav.json +13 -0
- package/messages/es/navigation.json +8 -0
- package/messages/es/observability.json +74 -0
- package/messages/es/posts.json +54 -0
- package/messages/es/pricing.json +102 -0
- package/messages/es/support.json +9 -0
- package/messages/es/teams.json +8 -0
- package/messages/fr/admin.json +219 -0
- package/messages/fr/aiUsage.json +36 -0
- package/messages/fr/buttons.json +19 -0
- package/messages/fr/categories.json +35 -0
- package/messages/fr/common.json +16 -0
- package/messages/fr/dev.json +101 -0
- package/messages/fr/docs.json +27 -0
- package/messages/fr/entities.json +7 -0
- package/messages/fr/features.json +119 -0
- package/messages/fr/footer.json +22 -0
- package/messages/fr/home.json +57 -0
- package/messages/fr/index.ts +39 -0
- package/messages/fr/mobileNav.json +13 -0
- package/messages/fr/navigation.json +8 -0
- package/messages/fr/observability.json +74 -0
- package/messages/fr/posts.json +54 -0
- package/messages/fr/pricing.json +102 -0
- package/messages/fr/support.json +9 -0
- package/messages/fr/teams.json +8 -0
- package/messages/it/admin.json +219 -0
- package/messages/it/aiUsage.json +36 -0
- package/messages/it/buttons.json +19 -0
- package/messages/it/categories.json +35 -0
- package/messages/it/common.json +16 -0
- package/messages/it/dev.json +101 -0
- package/messages/it/docs.json +27 -0
- package/messages/it/entities.json +7 -0
- package/messages/it/features.json +119 -0
- package/messages/it/footer.json +22 -0
- package/messages/it/home.json +57 -0
- package/messages/it/index.ts +39 -0
- package/messages/it/mobileNav.json +13 -0
- package/messages/it/navigation.json +8 -0
- package/messages/it/observability.json +74 -0
- package/messages/it/posts.json +54 -0
- package/messages/it/pricing.json +102 -0
- package/messages/it/support.json +9 -0
- package/messages/it/teams.json +8 -0
- package/messages/pt/admin.json +219 -0
- package/messages/pt/aiUsage.json +36 -0
- package/messages/pt/buttons.json +19 -0
- package/messages/pt/categories.json +35 -0
- package/messages/pt/common.json +16 -0
- package/messages/pt/dev.json +101 -0
- package/messages/pt/docs.json +27 -0
- package/messages/pt/entities.json +7 -0
- package/messages/pt/features.json +119 -0
- package/messages/pt/footer.json +22 -0
- package/messages/pt/home.json +57 -0
- package/messages/pt/index.ts +39 -0
- package/messages/pt/mobileNav.json +13 -0
- package/messages/pt/navigation.json +8 -0
- package/messages/pt/observability.json +74 -0
- package/messages/pt/posts.json +54 -0
- package/messages/pt/pricing.json +102 -0
- package/messages/pt/support.json +9 -0
- package/messages/pt/teams.json +8 -0
- package/migrations/089_add_editor_team_role.sql +39 -0
- package/migrations/090_demo_users_teams.sql +540 -0
- package/migrations/091_greek_teams_billing.sql +523 -0
- package/migrations/092_billing_sample_data.sql +774 -0
- package/migrations/093_pages_sample_data.sql +1158 -0
- package/migrations/094_posts_sample_data.sql +278 -0
- package/migrations/095_tasks_sample_data.sql +440 -0
- package/migrations/096_customers_sample_data.sql +358 -0
- package/migrations/097_scheduled_actions_sample_data.sql +111 -0
- package/package.json +22 -0
- package/public/docs/desktop-layout-example.png +0 -0
- package/styles/components.css +11 -0
- package/styles/globals.css +179 -0
- package/templates/(public)/blog/[slug]/page.tsx +65 -0
- package/templates/(public)/layout.tsx +25 -0
- package/templates/(public)/page.tsx +200 -0
- package/templates/(public)/support/page.tsx +321 -0
- package/templates/dashboard/(main)/agent-multi/page.tsx +63 -0
- package/templates/dashboard/(main)/agent-single/page.tsx +142 -0
- package/templates/dashboard/(main)/settings/ai-usage/page.tsx +157 -0
- package/templates/superadmin/ai-observability/[traceId]/page.tsx +27 -0
- 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
|
+
}
|