@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,50 @@
|
|
|
1
|
+
-- Migration: 024_add_status_to_pages.sql
|
|
2
|
+
-- Description: Add status column to pages + enable RLS (was missing)
|
|
3
|
+
-- Date: 2025-12-17
|
|
4
|
+
-- Session: builder-entities-unification-v2
|
|
5
|
+
|
|
6
|
+
-- ============================================
|
|
7
|
+
-- STEP 1: Add status column (replaces published boolean)
|
|
8
|
+
-- ============================================
|
|
9
|
+
ALTER TABLE public."pages"
|
|
10
|
+
ADD COLUMN IF NOT EXISTS "status" VARCHAR(50) DEFAULT 'draft';
|
|
11
|
+
|
|
12
|
+
-- Migrate existing data: published = true -> 'published', false -> 'draft'
|
|
13
|
+
UPDATE public."pages"
|
|
14
|
+
SET "status" = CASE
|
|
15
|
+
WHEN "published" = true THEN 'published'
|
|
16
|
+
ELSE 'draft'
|
|
17
|
+
END
|
|
18
|
+
WHERE "status" = 'draft' OR "status" IS NULL;
|
|
19
|
+
|
|
20
|
+
-- Create index for status filtering
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_pages_status ON public."pages"("status");
|
|
22
|
+
|
|
23
|
+
-- Add check constraint for valid status values
|
|
24
|
+
-- First drop if exists to avoid errors on re-run
|
|
25
|
+
ALTER TABLE public."pages" DROP CONSTRAINT IF EXISTS pages_status_check;
|
|
26
|
+
ALTER TABLE public."pages"
|
|
27
|
+
ADD CONSTRAINT pages_status_check
|
|
28
|
+
CHECK ("status" IN ('draft', 'published', 'scheduled', 'archived'));
|
|
29
|
+
|
|
30
|
+
-- ============================================
|
|
31
|
+
-- STEP 2: Update RLS public policy to use status field
|
|
32
|
+
-- ============================================
|
|
33
|
+
-- Note: RLS and team isolation policy already created in 001_pages_table.sql
|
|
34
|
+
-- Only update the public select policy to use status instead of published
|
|
35
|
+
|
|
36
|
+
DROP POLICY IF EXISTS "pages public can select" ON public."pages";
|
|
37
|
+
|
|
38
|
+
-- Public can read published pages (using NEW status field)
|
|
39
|
+
CREATE POLICY "pages public can select"
|
|
40
|
+
ON public."pages"
|
|
41
|
+
FOR SELECT TO anon
|
|
42
|
+
USING ("status" = 'published');
|
|
43
|
+
|
|
44
|
+
-- ============================================
|
|
45
|
+
-- COMMENTS
|
|
46
|
+
-- ============================================
|
|
47
|
+
COMMENT ON COLUMN public."pages"."status" IS 'Publication status: draft, published, scheduled, archived';
|
|
48
|
+
|
|
49
|
+
-- Note: We keep 'published' column temporarily for rollback safety
|
|
50
|
+
-- DROP COLUMN published will be in a future migration after validation
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pages Management Service
|
|
3
|
+
*
|
|
4
|
+
* Provides authenticated CRUD operations for pages including block management.
|
|
5
|
+
* All methods require authentication and use RLS.
|
|
6
|
+
*
|
|
7
|
+
* @module PagesManagementService
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from '@nextsparkjs/core/lib/db'
|
|
11
|
+
import type {
|
|
12
|
+
Block,
|
|
13
|
+
PageFull,
|
|
14
|
+
PageCreateData,
|
|
15
|
+
PageUpdateData,
|
|
16
|
+
PageManagementListOptions,
|
|
17
|
+
PageManagementListResult,
|
|
18
|
+
} from './pages.types'
|
|
19
|
+
|
|
20
|
+
// Database row type for page
|
|
21
|
+
interface DbPage {
|
|
22
|
+
id: string
|
|
23
|
+
slug: string
|
|
24
|
+
title: string
|
|
25
|
+
blocks: Block[]
|
|
26
|
+
locale: string
|
|
27
|
+
status: string
|
|
28
|
+
seoTitle: string | null
|
|
29
|
+
seoDescription: string | null
|
|
30
|
+
ogImage: string | null
|
|
31
|
+
createdAt: string
|
|
32
|
+
updatedAt: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Convert database row to PageFull
|
|
37
|
+
*/
|
|
38
|
+
function toPageFull(row: DbPage): PageFull {
|
|
39
|
+
return {
|
|
40
|
+
id: row.id,
|
|
41
|
+
slug: row.slug,
|
|
42
|
+
title: row.title,
|
|
43
|
+
blocks: row.blocks || [],
|
|
44
|
+
locale: row.locale,
|
|
45
|
+
status: row.status as 'draft' | 'published',
|
|
46
|
+
seoTitle: row.seoTitle ?? undefined,
|
|
47
|
+
seoDescription: row.seoDescription ?? undefined,
|
|
48
|
+
ogImage: row.ogImage ?? undefined,
|
|
49
|
+
createdAt: row.createdAt,
|
|
50
|
+
updatedAt: row.updatedAt,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class PagesManagementService {
|
|
55
|
+
// ============================================
|
|
56
|
+
// CRUD OPERATIONS
|
|
57
|
+
// ============================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* List pages with optional filters
|
|
61
|
+
*/
|
|
62
|
+
static async list(
|
|
63
|
+
userId: string,
|
|
64
|
+
options: PageManagementListOptions = {}
|
|
65
|
+
): Promise<PageManagementListResult> {
|
|
66
|
+
try {
|
|
67
|
+
if (!userId || userId.trim() === '') {
|
|
68
|
+
throw new Error('User ID is required for authentication')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const {
|
|
72
|
+
limit = 20,
|
|
73
|
+
offset = 0,
|
|
74
|
+
locale,
|
|
75
|
+
status = 'all',
|
|
76
|
+
orderBy = 'createdAt',
|
|
77
|
+
orderDir = 'desc',
|
|
78
|
+
} = options
|
|
79
|
+
|
|
80
|
+
// Build WHERE clause
|
|
81
|
+
const conditions: string[] = []
|
|
82
|
+
const params: unknown[] = []
|
|
83
|
+
let paramIndex = 1
|
|
84
|
+
|
|
85
|
+
if (locale) {
|
|
86
|
+
conditions.push(`locale = $${paramIndex++}`)
|
|
87
|
+
params.push(locale)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (status !== 'all') {
|
|
91
|
+
conditions.push(`status = $${paramIndex++}`)
|
|
92
|
+
params.push(status)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const whereClause = conditions.length > 0
|
|
96
|
+
? `WHERE ${conditions.join(' AND ')}`
|
|
97
|
+
: ''
|
|
98
|
+
|
|
99
|
+
// Validate orderBy
|
|
100
|
+
const validOrderBy = ['createdAt', 'updatedAt', 'title'].includes(orderBy) ? orderBy : 'createdAt'
|
|
101
|
+
const validOrderDir = orderDir === 'asc' ? 'ASC' : 'DESC'
|
|
102
|
+
const orderColumn = validOrderBy === 'title' ? 'title' : `"${validOrderBy}"`
|
|
103
|
+
|
|
104
|
+
// Get total count
|
|
105
|
+
const countResult = await queryWithRLS<{ count: string }>(
|
|
106
|
+
`SELECT COUNT(*)::text as count FROM pages ${whereClause}`,
|
|
107
|
+
params,
|
|
108
|
+
userId
|
|
109
|
+
)
|
|
110
|
+
const total = parseInt(countResult[0]?.count || '0', 10)
|
|
111
|
+
|
|
112
|
+
// Get pages
|
|
113
|
+
params.push(limit, offset)
|
|
114
|
+
const pages = await queryWithRLS<DbPage>(
|
|
115
|
+
`
|
|
116
|
+
SELECT
|
|
117
|
+
id, slug, title, blocks, locale, status,
|
|
118
|
+
"seoTitle", "seoDescription", "ogImage",
|
|
119
|
+
"createdAt", "updatedAt"
|
|
120
|
+
FROM pages
|
|
121
|
+
${whereClause}
|
|
122
|
+
ORDER BY ${orderColumn} ${validOrderDir}
|
|
123
|
+
LIMIT $${paramIndex++} OFFSET $${paramIndex}
|
|
124
|
+
`,
|
|
125
|
+
params,
|
|
126
|
+
userId
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
pages: pages.map(toPageFull),
|
|
131
|
+
total,
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('PagesManagementService.list error:', error)
|
|
135
|
+
throw new Error(
|
|
136
|
+
error instanceof Error ? error.message : 'Failed to list pages'
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get a page by ID
|
|
143
|
+
*/
|
|
144
|
+
static async getById(
|
|
145
|
+
userId: string,
|
|
146
|
+
id: string
|
|
147
|
+
): Promise<PageFull | null> {
|
|
148
|
+
try {
|
|
149
|
+
if (!userId || userId.trim() === '') {
|
|
150
|
+
throw new Error('User ID is required for authentication')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!id || id.trim() === '') {
|
|
154
|
+
throw new Error('Page ID is required')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const page = await queryOneWithRLS<DbPage>(
|
|
158
|
+
`
|
|
159
|
+
SELECT
|
|
160
|
+
id, slug, title, blocks, locale, status,
|
|
161
|
+
"seoTitle", "seoDescription", "ogImage",
|
|
162
|
+
"createdAt", "updatedAt"
|
|
163
|
+
FROM pages
|
|
164
|
+
WHERE id = $1
|
|
165
|
+
`,
|
|
166
|
+
[id],
|
|
167
|
+
userId
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return page ? toPageFull(page) : null
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error('PagesManagementService.getById error:', error)
|
|
173
|
+
throw new Error(
|
|
174
|
+
error instanceof Error ? error.message : 'Failed to get page'
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Create a new page
|
|
181
|
+
*/
|
|
182
|
+
static async create(
|
|
183
|
+
userId: string,
|
|
184
|
+
data: PageCreateData
|
|
185
|
+
): Promise<PageFull> {
|
|
186
|
+
try {
|
|
187
|
+
if (!userId || userId.trim() === '') {
|
|
188
|
+
throw new Error('User ID is required for authentication')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!data.slug || !data.title) {
|
|
192
|
+
throw new Error('Slug and title are required')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const id = crypto.randomUUID()
|
|
196
|
+
const now = new Date().toISOString()
|
|
197
|
+
|
|
198
|
+
const result = await mutateWithRLS<DbPage>(
|
|
199
|
+
`
|
|
200
|
+
INSERT INTO pages (
|
|
201
|
+
id, "userId", "teamId", slug, title, blocks,
|
|
202
|
+
locale, status, "seoTitle", "seoDescription", "ogImage",
|
|
203
|
+
"createdAt", "updatedAt"
|
|
204
|
+
)
|
|
205
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
206
|
+
RETURNING
|
|
207
|
+
id, slug, title, blocks, locale, status,
|
|
208
|
+
"seoTitle", "seoDescription", "ogImage",
|
|
209
|
+
"createdAt", "updatedAt"
|
|
210
|
+
`,
|
|
211
|
+
[
|
|
212
|
+
id,
|
|
213
|
+
userId,
|
|
214
|
+
data.teamId,
|
|
215
|
+
data.slug,
|
|
216
|
+
data.title,
|
|
217
|
+
JSON.stringify(data.blocks || []),
|
|
218
|
+
data.locale || 'en',
|
|
219
|
+
data.status || 'draft',
|
|
220
|
+
data.seoTitle || null,
|
|
221
|
+
data.seoDescription || null,
|
|
222
|
+
data.ogImage || null,
|
|
223
|
+
now,
|
|
224
|
+
now,
|
|
225
|
+
],
|
|
226
|
+
userId
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if (!result.rows[0]) {
|
|
230
|
+
throw new Error('Failed to create page')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return toPageFull(result.rows[0])
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('PagesManagementService.create error:', error)
|
|
236
|
+
throw new Error(
|
|
237
|
+
error instanceof Error ? error.message : 'Failed to create page'
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Update page metadata (not blocks)
|
|
244
|
+
*/
|
|
245
|
+
static async update(
|
|
246
|
+
userId: string,
|
|
247
|
+
id: string,
|
|
248
|
+
data: PageUpdateData
|
|
249
|
+
): Promise<PageFull> {
|
|
250
|
+
try {
|
|
251
|
+
if (!userId || userId.trim() === '') {
|
|
252
|
+
throw new Error('User ID is required for authentication')
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!id || id.trim() === '') {
|
|
256
|
+
throw new Error('Page ID is required')
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Build dynamic update query
|
|
260
|
+
const updates: string[] = []
|
|
261
|
+
const values: unknown[] = []
|
|
262
|
+
let paramIndex = 1
|
|
263
|
+
|
|
264
|
+
if (data.slug !== undefined) {
|
|
265
|
+
updates.push(`slug = $${paramIndex++}`)
|
|
266
|
+
values.push(data.slug)
|
|
267
|
+
}
|
|
268
|
+
if (data.title !== undefined) {
|
|
269
|
+
updates.push(`title = $${paramIndex++}`)
|
|
270
|
+
values.push(data.title)
|
|
271
|
+
}
|
|
272
|
+
if (data.status !== undefined) {
|
|
273
|
+
updates.push(`status = $${paramIndex++}`)
|
|
274
|
+
values.push(data.status)
|
|
275
|
+
}
|
|
276
|
+
if (data.seoTitle !== undefined) {
|
|
277
|
+
updates.push(`"seoTitle" = $${paramIndex++}`)
|
|
278
|
+
values.push(data.seoTitle || null)
|
|
279
|
+
}
|
|
280
|
+
if (data.seoDescription !== undefined) {
|
|
281
|
+
updates.push(`"seoDescription" = $${paramIndex++}`)
|
|
282
|
+
values.push(data.seoDescription || null)
|
|
283
|
+
}
|
|
284
|
+
if (data.ogImage !== undefined) {
|
|
285
|
+
updates.push(`"ogImage" = $${paramIndex++}`)
|
|
286
|
+
values.push(data.ogImage || null)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (updates.length === 0) {
|
|
290
|
+
throw new Error('No fields to update')
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
updates.push(`"updatedAt" = $${paramIndex++}`)
|
|
294
|
+
values.push(new Date().toISOString())
|
|
295
|
+
|
|
296
|
+
values.push(id)
|
|
297
|
+
|
|
298
|
+
const result = await mutateWithRLS<DbPage>(
|
|
299
|
+
`
|
|
300
|
+
UPDATE pages
|
|
301
|
+
SET ${updates.join(', ')}
|
|
302
|
+
WHERE id = $${paramIndex}
|
|
303
|
+
RETURNING
|
|
304
|
+
id, slug, title, blocks, locale, status,
|
|
305
|
+
"seoTitle", "seoDescription", "ogImage",
|
|
306
|
+
"createdAt", "updatedAt"
|
|
307
|
+
`,
|
|
308
|
+
values,
|
|
309
|
+
userId
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if (!result.rows[0]) {
|
|
313
|
+
throw new Error('Page not found or update failed')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return toPageFull(result.rows[0])
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.error('PagesManagementService.update error:', error)
|
|
319
|
+
throw new Error(
|
|
320
|
+
error instanceof Error ? error.message : 'Failed to update page'
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Delete a page
|
|
327
|
+
*/
|
|
328
|
+
static async delete(
|
|
329
|
+
userId: string,
|
|
330
|
+
id: string
|
|
331
|
+
): Promise<boolean> {
|
|
332
|
+
try {
|
|
333
|
+
if (!userId || userId.trim() === '') {
|
|
334
|
+
throw new Error('User ID is required for authentication')
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!id || id.trim() === '') {
|
|
338
|
+
throw new Error('Page ID is required')
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const result = await mutateWithRLS(
|
|
342
|
+
`DELETE FROM pages WHERE id = $1`,
|
|
343
|
+
[id],
|
|
344
|
+
userId
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return result.rowCount > 0
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.error('PagesManagementService.delete error:', error)
|
|
350
|
+
throw new Error(
|
|
351
|
+
error instanceof Error ? error.message : 'Failed to delete page'
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ============================================
|
|
357
|
+
// BLOCK OPERATIONS
|
|
358
|
+
// ============================================
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Add a block to a page
|
|
362
|
+
*/
|
|
363
|
+
static async addBlock(
|
|
364
|
+
userId: string,
|
|
365
|
+
pageId: string,
|
|
366
|
+
blockSlug: string,
|
|
367
|
+
props: Record<string, unknown>,
|
|
368
|
+
position?: number
|
|
369
|
+
): Promise<PageFull> {
|
|
370
|
+
try {
|
|
371
|
+
// Get current page
|
|
372
|
+
const page = await this.getById(userId, pageId)
|
|
373
|
+
if (!page) {
|
|
374
|
+
throw new Error('Page not found')
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Create new block
|
|
378
|
+
const newBlock: Block = {
|
|
379
|
+
id: crypto.randomUUID(),
|
|
380
|
+
blockSlug,
|
|
381
|
+
props,
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Add block at position or end
|
|
385
|
+
const blocks = [...page.blocks]
|
|
386
|
+
if (position !== undefined && position >= 0 && position <= blocks.length) {
|
|
387
|
+
blocks.splice(position, 0, newBlock)
|
|
388
|
+
} else {
|
|
389
|
+
blocks.push(newBlock)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Update page with new blocks
|
|
393
|
+
const result = await mutateWithRLS<DbPage>(
|
|
394
|
+
`
|
|
395
|
+
UPDATE pages
|
|
396
|
+
SET blocks = $1, "updatedAt" = $2
|
|
397
|
+
WHERE id = $3
|
|
398
|
+
RETURNING
|
|
399
|
+
id, slug, title, blocks, locale, status,
|
|
400
|
+
"seoTitle", "seoDescription", "ogImage",
|
|
401
|
+
"createdAt", "updatedAt"
|
|
402
|
+
`,
|
|
403
|
+
[JSON.stringify(blocks), new Date().toISOString(), pageId],
|
|
404
|
+
userId
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
if (!result.rows[0]) {
|
|
408
|
+
throw new Error('Failed to add block')
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return toPageFull(result.rows[0])
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.error('PagesManagementService.addBlock error:', error)
|
|
414
|
+
throw new Error(
|
|
415
|
+
error instanceof Error ? error.message : 'Failed to add block'
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Update a block's properties
|
|
422
|
+
*/
|
|
423
|
+
static async updateBlock(
|
|
424
|
+
userId: string,
|
|
425
|
+
pageId: string,
|
|
426
|
+
blockId: string,
|
|
427
|
+
props: Record<string, unknown>
|
|
428
|
+
): Promise<PageFull> {
|
|
429
|
+
try {
|
|
430
|
+
// Get current page
|
|
431
|
+
const page = await this.getById(userId, pageId)
|
|
432
|
+
if (!page) {
|
|
433
|
+
throw new Error('Page not found')
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Find and update block
|
|
437
|
+
const blockIndex = page.blocks.findIndex((b: Block) => b.id === blockId)
|
|
438
|
+
if (blockIndex === -1) {
|
|
439
|
+
throw new Error('Block not found')
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const blocks = [...page.blocks]
|
|
443
|
+
blocks[blockIndex] = {
|
|
444
|
+
...blocks[blockIndex],
|
|
445
|
+
props: { ...blocks[blockIndex].props, ...props },
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Update page with modified blocks
|
|
449
|
+
const result = await mutateWithRLS<DbPage>(
|
|
450
|
+
`
|
|
451
|
+
UPDATE pages
|
|
452
|
+
SET blocks = $1, "updatedAt" = $2
|
|
453
|
+
WHERE id = $3
|
|
454
|
+
RETURNING
|
|
455
|
+
id, slug, title, blocks, locale, status,
|
|
456
|
+
"seoTitle", "seoDescription", "ogImage",
|
|
457
|
+
"createdAt", "updatedAt"
|
|
458
|
+
`,
|
|
459
|
+
[JSON.stringify(blocks), new Date().toISOString(), pageId],
|
|
460
|
+
userId
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
if (!result.rows[0]) {
|
|
464
|
+
throw new Error('Failed to update block')
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return toPageFull(result.rows[0])
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error('PagesManagementService.updateBlock error:', error)
|
|
470
|
+
throw new Error(
|
|
471
|
+
error instanceof Error ? error.message : 'Failed to update block'
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Remove a block from a page
|
|
478
|
+
*/
|
|
479
|
+
static async removeBlock(
|
|
480
|
+
userId: string,
|
|
481
|
+
pageId: string,
|
|
482
|
+
blockId: string
|
|
483
|
+
): Promise<PageFull> {
|
|
484
|
+
try {
|
|
485
|
+
// Get current page
|
|
486
|
+
const page = await this.getById(userId, pageId)
|
|
487
|
+
if (!page) {
|
|
488
|
+
throw new Error('Page not found')
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Filter out the block
|
|
492
|
+
const blocks = page.blocks.filter((b: Block) => b.id !== blockId)
|
|
493
|
+
|
|
494
|
+
if (blocks.length === page.blocks.length) {
|
|
495
|
+
throw new Error('Block not found')
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Update page with remaining blocks
|
|
499
|
+
const result = await mutateWithRLS<DbPage>(
|
|
500
|
+
`
|
|
501
|
+
UPDATE pages
|
|
502
|
+
SET blocks = $1, "updatedAt" = $2
|
|
503
|
+
WHERE id = $3
|
|
504
|
+
RETURNING
|
|
505
|
+
id, slug, title, blocks, locale, status,
|
|
506
|
+
"seoTitle", "seoDescription", "ogImage",
|
|
507
|
+
"createdAt", "updatedAt"
|
|
508
|
+
`,
|
|
509
|
+
[JSON.stringify(blocks), new Date().toISOString(), pageId],
|
|
510
|
+
userId
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
if (!result.rows[0]) {
|
|
514
|
+
throw new Error('Failed to remove block')
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return toPageFull(result.rows[0])
|
|
518
|
+
} catch (error) {
|
|
519
|
+
console.error('PagesManagementService.removeBlock error:', error)
|
|
520
|
+
throw new Error(
|
|
521
|
+
error instanceof Error ? error.message : 'Failed to remove block'
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Reorder blocks in a page
|
|
528
|
+
*/
|
|
529
|
+
static async reorderBlocks(
|
|
530
|
+
userId: string,
|
|
531
|
+
pageId: string,
|
|
532
|
+
blockIds: string[]
|
|
533
|
+
): Promise<PageFull> {
|
|
534
|
+
try {
|
|
535
|
+
// Get current page
|
|
536
|
+
const page = await this.getById(userId, pageId)
|
|
537
|
+
if (!page) {
|
|
538
|
+
throw new Error('Page not found')
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Create a map of blocks by ID
|
|
542
|
+
const blockMap = new Map(page.blocks.map(b => [b.id, b]))
|
|
543
|
+
|
|
544
|
+
// Reorder based on provided IDs
|
|
545
|
+
const reorderedBlocks: Block[] = []
|
|
546
|
+
for (const id of blockIds) {
|
|
547
|
+
const block = blockMap.get(id)
|
|
548
|
+
if (block) {
|
|
549
|
+
reorderedBlocks.push(block)
|
|
550
|
+
blockMap.delete(id)
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Add any remaining blocks (in case some IDs were missing)
|
|
555
|
+
for (const block of blockMap.values()) {
|
|
556
|
+
reorderedBlocks.push(block)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Update page with reordered blocks
|
|
560
|
+
const result = await mutateWithRLS<DbPage>(
|
|
561
|
+
`
|
|
562
|
+
UPDATE pages
|
|
563
|
+
SET blocks = $1, "updatedAt" = $2
|
|
564
|
+
WHERE id = $3
|
|
565
|
+
RETURNING
|
|
566
|
+
id, slug, title, blocks, locale, status,
|
|
567
|
+
"seoTitle", "seoDescription", "ogImage",
|
|
568
|
+
"createdAt", "updatedAt"
|
|
569
|
+
`,
|
|
570
|
+
[JSON.stringify(reorderedBlocks), new Date().toISOString(), pageId],
|
|
571
|
+
userId
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
if (!result.rows[0]) {
|
|
575
|
+
throw new Error('Failed to reorder blocks')
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return toPageFull(result.rows[0])
|
|
579
|
+
} catch (error) {
|
|
580
|
+
console.error('PagesManagementService.reorderBlocks error:', error)
|
|
581
|
+
throw new Error(
|
|
582
|
+
error instanceof Error ? error.message : 'Failed to reorder blocks'
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ============================================
|
|
588
|
+
// PUBLICATION
|
|
589
|
+
// ============================================
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Publish a page
|
|
593
|
+
*/
|
|
594
|
+
static async publish(
|
|
595
|
+
userId: string,
|
|
596
|
+
id: string
|
|
597
|
+
): Promise<PageFull> {
|
|
598
|
+
return this.update(userId, id, { status: 'published' })
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Unpublish a page (set to draft)
|
|
603
|
+
*/
|
|
604
|
+
static async unpublish(
|
|
605
|
+
userId: string,
|
|
606
|
+
id: string
|
|
607
|
+
): Promise<PageFull> {
|
|
608
|
+
return this.update(userId, id, { status: 'draft' })
|
|
609
|
+
}
|
|
610
|
+
}
|