@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,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
|
+
}
|