@fayz-ai/plugin-marketing 0.8.0 → 0.8.2
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/dist/MarketingContext.d.ts +2 -0
- package/dist/MarketingContext.d.ts.map +1 -1
- package/dist/MarketingPage.d.ts.map +1 -1
- package/dist/data/blogEntities.d.ts +28 -0
- package/dist/data/blogEntities.d.ts.map +1 -0
- package/dist/data/capability.test.d.ts +2 -0
- package/dist/data/capability.test.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +10 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/tables.d.ts +7 -0
- package/dist/data/tables.d.ts.map +1 -1
- package/dist/data/types.d.ts +2 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.cjs +1239 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1245 -94
- package/dist/index.js.map +1 -1
- package/dist/integrations/ranklayer/connectorDef.d.ts +3 -0
- package/dist/integrations/ranklayer/connectorDef.d.ts.map +1 -0
- package/dist/integrations/ranklayer/data/supabase.d.ts +16 -0
- package/dist/integrations/ranklayer/data/supabase.d.ts.map +1 -0
- package/dist/integrations/ranklayer/types.d.ts +16 -0
- package/dist/integrations/ranklayer/types.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +4 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/registries.d.ts +3 -3
- package/dist/registries.d.ts.map +1 -1
- package/dist/schema/index.d.ts +354 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema.cjs +30 -0
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.js +30 -2
- package/dist/schema.js.map +1 -1
- package/dist/store.d.ts +2 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/views/CampaignsView.d.ts.map +1 -1
- package/dist/views/blog/BlogAdminView.d.ts +5 -0
- package/dist/views/blog/BlogAdminView.d.ts.map +1 -0
- package/dist/views/blog/BlogEntityForm.d.ts +11 -0
- package/dist/views/blog/BlogEntityForm.d.ts.map +1 -0
- package/dist/views/blog/useEntityCrud.d.ts +29 -0
- package/dist/views/blog/useEntityCrud.d.ts.map +1 -0
- package/dist/views/content/ContentView.d.ts.map +1 -1
- package/package.json +8 -6
- package/src/MarketingContext.tsx +2 -0
- package/src/MarketingPage.tsx +27 -2
- package/src/data/blogEntities.tsx +91 -0
- package/src/data/capability.test.ts +92 -0
- package/src/data/supabase.ts +401 -0
- package/src/data/tables.ts +9 -0
- package/src/data/types.ts +2 -0
- package/src/index.ts +153 -10
- package/src/integrations/ranklayer/RANKLAYER.md +40 -0
- package/src/integrations/ranklayer/connectorDef.tsx +98 -0
- package/src/integrations/ranklayer/data/supabase.ts +106 -0
- package/src/integrations/ranklayer/types.ts +16 -0
- package/src/migrations/004_ranklayer.sql +50 -0
- package/src/migrations/005_analytics_base.sql +60 -0
- package/src/migrations/006_analytics_views.sql +68 -0
- package/src/migrations/007_agent_rpcs.sql +119 -0
- package/src/migrations/index.ts +310 -1
- package/src/presets.ts +5 -3
- package/src/registries.ts +18 -6
- package/src/schema/index.ts +38 -1
- package/src/store.ts +21 -8
- package/src/views/CampaignsView.tsx +4 -1
- package/src/views/blog/BlogAdminView.tsx +99 -0
- package/src/views/blog/BlogEntityForm.tsx +101 -0
- package/src/views/blog/useEntityCrud.ts +50 -0
- package/src/views/content/ContentView.tsx +8 -1
package/dist/schema.js
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
|
-
import { pgTable,
|
|
1
|
+
import { pgTable, timestamps, numeric, boolean, text, tenantId, uuid, index, uniqueIndex, timestamp, createdAt, integer, jsonb, date } from '@fayz-ai/db';
|
|
2
2
|
|
|
3
3
|
// src/schema/index.ts
|
|
4
|
+
var mktChannels = pgTable("plg_marketing_channels", {
|
|
5
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
6
|
+
tenantId: tenantId(),
|
|
7
|
+
channelKey: text("channel_key").notNull(),
|
|
8
|
+
label: text("label").notNull(),
|
|
9
|
+
icon: text("icon"),
|
|
10
|
+
kind: text("kind").notNull().default("organic"),
|
|
11
|
+
isActive: boolean("is_active").notNull().default(true),
|
|
12
|
+
monthlySpend: numeric("monthly_spend").notNull().default("0"),
|
|
13
|
+
...timestamps
|
|
14
|
+
}, (t) => [
|
|
15
|
+
index("idx_plg_marketing_channels_tenant").on(t.tenantId),
|
|
16
|
+
uniqueIndex("u_plg_marketing_channels_tenant_key").on(t.tenantId, t.channelKey)
|
|
17
|
+
]);
|
|
18
|
+
var mktCampaigns = pgTable("plg_marketing_campaigns", {
|
|
19
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
20
|
+
tenantId: tenantId(),
|
|
21
|
+
name: text("name").notNull(),
|
|
22
|
+
channelKey: text("channel_key").notNull(),
|
|
23
|
+
status: text("status").notNull().default("draft"),
|
|
24
|
+
startsAt: timestamp("starts_at", { withTimezone: true }),
|
|
25
|
+
endsAt: timestamp("ends_at", { withTimezone: true }),
|
|
26
|
+
spend: numeric("spend").notNull().default("0"),
|
|
27
|
+
...timestamps
|
|
28
|
+
}, (t) => [
|
|
29
|
+
index("idx_plg_marketing_campaigns_tenant").on(t.tenantId),
|
|
30
|
+
index("idx_plg_marketing_campaigns_channel").on(t.tenantId, t.channelKey)
|
|
31
|
+
]);
|
|
4
32
|
var mktSocialAccounts = pgTable("plg_marketing_social_accounts", {
|
|
5
33
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
6
34
|
tenantId: tenantId(),
|
|
@@ -56,6 +84,6 @@ var mktContentPosts = pgTable("plg_marketing_content_posts", {
|
|
|
56
84
|
index("idx_plg_marketing_content_posts_status").on(t.tenantId, t.status)
|
|
57
85
|
]);
|
|
58
86
|
|
|
59
|
-
export { mktContentPlans, mktContentPosts, mktSocialAccounts };
|
|
87
|
+
export { mktCampaigns, mktChannels, mktContentPlans, mktContentPosts, mktSocialAccounts };
|
|
60
88
|
//# sourceMappingURL=schema.js.map
|
|
61
89
|
//# sourceMappingURL=schema.js.map
|
package/dist/schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schema/index.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"sources":["../src/schema/index.ts"],"names":[],"mappings":"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","file":"schema.js","sourcesContent":["// Builders come from @fayz-ai/db (single drizzle-orm instance) — never import\n// pgTable/uuid directly from drizzle-orm/pg-core. See MIGRATION-ARCHITECTURE §6.\nimport {\n pgTable, uuid, text, boolean, integer, date, jsonb, index, uniqueIndex,\n numeric, timestamp,\n tenantId, timestamps, createdAt,\n} from '@fayz-ai/db'\n\n// ---------------------------------------------------------------------------\n// Marketing plugin — Ring-1 schema (schema-as-code), content planner tables.\n// RLS policies + grants are companion SQL (Drizzle doesn't diff those) —\n// see src/migrations/001_content_planner.sql.\n// ---------------------------------------------------------------------------\n\n// Acquisition channels — tenant-owned rows seeded from the domain preset.\n// `channel_key` is the stable preset/config id ('instagram', 'paid-search'…),\n// aligned with plugin-crm's lead-sources where they overlap.\nexport const mktChannels = pgTable('plg_marketing_channels', {\n id: uuid('id').primaryKey().defaultRandom(),\n tenantId: tenantId(),\n channelKey: text('channel_key').notNull(),\n label: text('label').notNull(),\n icon: text('icon'),\n kind: text('kind').notNull().default('organic'),\n isActive: boolean('is_active').notNull().default(true),\n monthlySpend: numeric('monthly_spend').notNull().default('0'),\n ...timestamps,\n}, (t) => [\n index('idx_plg_marketing_channels_tenant').on(t.tenantId),\n uniqueIndex('u_plg_marketing_channels_tenant_key').on(t.tenantId, t.channelKey),\n])\n\n// Acquisition campaigns — real CRUD; performance is DERIVED at read time from\n// v_marketing_attribution (channel match within the campaign window), only\n// spend is stored.\nexport const mktCampaigns = pgTable('plg_marketing_campaigns', {\n id: uuid('id').primaryKey().defaultRandom(),\n tenantId: tenantId(),\n name: text('name').notNull(),\n channelKey: text('channel_key').notNull(),\n status: text('status').notNull().default('draft'),\n startsAt: timestamp('starts_at', { withTimezone: true }),\n endsAt: timestamp('ends_at', { withTimezone: true }),\n spend: numeric('spend').notNull().default('0'),\n ...timestamps,\n}, (t) => [\n index('idx_plg_marketing_campaigns_tenant').on(t.tenantId),\n index('idx_plg_marketing_campaigns_channel').on(t.tenantId, t.channelKey),\n])\n\nexport const mktSocialAccounts = pgTable('plg_marketing_social_accounts', {\n id: uuid('id').primaryKey().defaultRandom(),\n tenantId: tenantId(),\n name: text('name').notNull(),\n handle: text('handle'),\n // One account, many platform connections (Instagram, YouTube, TikTok, ...)\n platforms: text('platforms').array().notNull().default(['instagram']),\n isActive: boolean('is_active').notNull().default(true),\n ...createdAt,\n}, (t) => [index('idx_plg_marketing_social_accounts_tenant').on(t.tenantId)])\n\nexport const mktContentPlans = pgTable('plg_marketing_content_plans', {\n id: uuid('id').primaryKey().defaultRandom(),\n tenantId: tenantId(),\n accountId: uuid('account_id').notNull().references(() => mktSocialAccounts.id, { onDelete: 'cascade' }),\n name: text('name').notNull(),\n status: text('status').notNull().default('active'),\n weeksCount: integer('weeks_count').notNull().default(8),\n startDate: date('start_date'),\n objective: text('objective'),\n tone: text('tone'),\n pillars: jsonb('pillars').notNull().default([]),\n formats: jsonb('formats').notNull().default([]),\n weeklyFrequency: integer('weekly_frequency').notNull().default(3),\n briefMd: text('brief_md').notNull().default(''),\n ...timestamps,\n}, (t) => [\n index('idx_plg_marketing_content_plans_tenant').on(t.tenantId),\n index('idx_plg_marketing_content_plans_account').on(t.accountId),\n])\n\nexport const mktContentPosts = pgTable('plg_marketing_content_posts', {\n id: uuid('id').primaryKey().defaultRandom(),\n tenantId: tenantId(),\n planId: uuid('plan_id').notNull().references(() => mktContentPlans.id, { onDelete: 'cascade' }),\n weekNumber: integer('week_number').notNull().default(1),\n position: integer('position').notNull().default(0),\n title: text('title').notNull(),\n format: text('format').notNull().default('reel'),\n status: text('status').notNull().default('idea'),\n scheduledDate: date('scheduled_date'),\n // Target platforms for this post; empty = inherit the account's platforms\n platforms: text('platforms').array().notNull().default([]),\n // Recording-day checklist [{ id, text, done }]\n checklist: jsonb('checklist').notNull().default([]),\n // Uploaded asset (static posts: caption + final art instead of a script)\n mediaUrl: text('media_url'),\n hook: text('hook'),\n cta: text('cta'),\n contentMd: text('content_md').notNull().default(''),\n ...timestamps,\n}, (t) => [\n index('idx_plg_marketing_content_posts_tenant').on(t.tenantId),\n index('idx_plg_marketing_content_posts_plan').on(t.planId, t.weekNumber, t.position),\n index('idx_plg_marketing_content_posts_status').on(t.tenantId, t.status),\n])\n"]}
|
package/dist/store.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface MarketingUIState {
|
|
|
10
10
|
landingPages: LandingPagePerf[];
|
|
11
11
|
loading: boolean;
|
|
12
12
|
load(): Promise<void>;
|
|
13
|
+
/** Invalidate the provider cache and reload (external writes, e.g. agent RPCs). */
|
|
14
|
+
refresh(): Promise<void>;
|
|
13
15
|
setRange(range: DateRangeKey): Promise<void>;
|
|
14
16
|
saveCampaign(input: SaveCampaignInput): Promise<Campaign>;
|
|
15
17
|
deleteCampaign(id: string): Promise<void>;
|
package/dist/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC5D,OAAO,KAAK,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAC5E,OAAO,KAAK,EACV,QAAQ,EACR,kBAAkB,EAClB,YAAY,EACZ,WAAW,EACX,eAAe,EACf,iBAAiB,EAClB,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,YAAY,CAAA;IACnB,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAA;IAClC,QAAQ,EAAE,kBAAkB,EAAE,CAAA;IAC9B,SAAS,EAAE,QAAQ,EAAE,CAAA;IACrB,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,YAAY,EAAE,eAAe,EAAE,CAAA;IAC/B,OAAO,EAAE,OAAO,CAAA;IAEhB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5C,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACzD,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1C;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,QAAQ,CAAC,gBAAgB,CAAC,
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC5D,OAAO,KAAK,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAC5E,OAAO,KAAK,EACV,QAAQ,EACR,kBAAkB,EAClB,YAAY,EACZ,WAAW,EACX,eAAe,EACf,iBAAiB,EAClB,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,YAAY,CAAA;IACnB,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAA;IAClC,QAAQ,EAAE,kBAAkB,EAAE,CAAA;IAC9B,SAAS,EAAE,QAAQ,EAAE,CAAA;IACrB,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,YAAY,EAAE,eAAe,EAAE,CAAA;IAC/B,OAAO,EAAE,OAAO,CAAA;IAEhB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,mFAAmF;IACnF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5C,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACzD,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1C;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAkDhG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CampaignsView.d.ts","sourceRoot":"","sources":["../../src/views/CampaignsView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"CampaignsView.d.ts","sourceRoot":"","sources":["../../src/views/CampaignsView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAYzB,wBAAgB,aAAa,sBA8C5B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BlogAdminView.d.ts","sourceRoot":"","sources":["../../../src/views/blog/BlogAdminView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAA;AAgF3D,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,qBAkB/E"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { EntityDef } from '@fayz-ai/core';
|
|
3
|
+
export declare function BlogEntityForm<T extends {
|
|
4
|
+
id: string;
|
|
5
|
+
}>({ entity, editId, parentLabel, onDone, }: {
|
|
6
|
+
entity: EntityDef<T>;
|
|
7
|
+
editId?: string;
|
|
8
|
+
parentLabel: string;
|
|
9
|
+
onDone: () => void;
|
|
10
|
+
}): React.JSX.Element;
|
|
11
|
+
//# sourceMappingURL=BlogEntityForm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BlogEntityForm.d.ts","sourceRoot":"","sources":["../../../src/views/blog/BlogEntityForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAW9C,wBAAgB,cAAc,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,EACvD,MAAM,EACN,MAAM,EACN,WAAW,EACX,MAAM,GACP,EAAE;IACD,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,IAAI,CAAA;CACnB,qBA4EA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EntityDef } from '@fayz-ai/core';
|
|
2
|
+
interface ListQuery {
|
|
3
|
+
search?: string;
|
|
4
|
+
filters?: Record<string, unknown>;
|
|
5
|
+
page?: number;
|
|
6
|
+
pageSize?: number;
|
|
7
|
+
sortBy?: string;
|
|
8
|
+
sortDir?: 'asc' | 'desc';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Minimal store-less CRUD wiring for a plugin that owns its data + routing.
|
|
12
|
+
* Resolves the generic Supabase provider from the EntityDef's `data.table`
|
|
13
|
+
* (mock fallback when Supabase is not configured) and exposes list/get/save/
|
|
14
|
+
* remove. Used by the blog backoffice list + form views — the heavy list/form
|
|
15
|
+
* rendering stays in the shared CrudListView / CrudFormPage.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useEntityCrud<T extends {
|
|
18
|
+
id: string;
|
|
19
|
+
}>(entity: EntityDef<T>): {
|
|
20
|
+
items: T[] | null;
|
|
21
|
+
total: number;
|
|
22
|
+
fetch: (query?: ListQuery) => Promise<void>;
|
|
23
|
+
getById: (id: string) => Promise<T | null>;
|
|
24
|
+
create: (data: Record<string, unknown>) => Promise<T>;
|
|
25
|
+
update: (id: string, data: Record<string, unknown>) => Promise<T>;
|
|
26
|
+
remove: (id: string) => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=useEntityCrud.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEntityCrud.d.ts","sourceRoot":"","sources":["../../../src/views/blog/useEntityCrud.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAE9C,UAAU,SAAS;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;;;oBAKlC,SAAS;kBAcV,MAAM,KAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;mBAK/B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;iBACzB,MAAM,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;iBACrC,MAAM;EAGvC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContentView.d.ts","sourceRoot":"","sources":["../../../src/views/content/ContentView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAwsBzB,wBAAgB,WAAW,CAAC,EAAE,UAAU,EAAE,EAAE;IAC1C,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC,
|
|
1
|
+
{"version":3,"file":"ContentView.d.ts","sourceRoot":"","sources":["../../../src/views/content/ContentView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAwsBzB,wBAAgB,WAAW,CAAC,EAAE,UAAU,EAAE,EAAE;IAC1C,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC,qBAiMA"}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"fayz": {
|
|
4
4
|
"status": "beta"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.8.
|
|
6
|
+
"version": "0.8.2",
|
|
7
7
|
"description": "Fayz SDK — marketing campaigns & broadcasts plugin",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./dist/index.cjs",
|
|
@@ -35,17 +35,18 @@
|
|
|
35
35
|
"@tanstack/react-table": "^8.20.0",
|
|
36
36
|
"lucide-react": "^0.400.0",
|
|
37
37
|
"zustand": "^4.5.0",
|
|
38
|
-
"@fayz-ai/ui": "^0.8.
|
|
39
|
-
"@fayz-ai/core": "^0.8.
|
|
40
|
-
"@fayz-ai/
|
|
41
|
-
"@fayz-ai/
|
|
38
|
+
"@fayz-ai/ui": "^0.8.4",
|
|
39
|
+
"@fayz-ai/core": "^0.8.4",
|
|
40
|
+
"@fayz-ai/db": "^0.8.4",
|
|
41
|
+
"@fayz-ai/saas": "^0.8.4"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/react": "^18.3.0",
|
|
45
45
|
"react": "^18.3.0",
|
|
46
46
|
"tsup": "^8.2.0",
|
|
47
47
|
"typescript": "^5.5.0",
|
|
48
|
-
"@types/react-dom": "^18.3.0"
|
|
48
|
+
"@types/react-dom": "^18.3.0",
|
|
49
|
+
"vitest": "^2.1.9"
|
|
49
50
|
},
|
|
50
51
|
"license": "MIT",
|
|
51
52
|
"keywords": [
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"build": "tsup && tsc --emitDeclarationOnly --declaration --declarationMap --noEmit false",
|
|
61
62
|
"dev": "tsup --watch",
|
|
62
63
|
"typecheck": "tsc --noEmit",
|
|
64
|
+
"test": "vitest run",
|
|
63
65
|
"clean": "rm -rf dist"
|
|
64
66
|
}
|
|
65
67
|
}
|
package/src/MarketingContext.tsx
CHANGED
package/src/MarketingPage.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import type { StoreApi } from 'zustand/vanilla'
|
|
3
3
|
import { ModulePage, type ModuleNavItem } from '@fayz-ai/ui'
|
|
4
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
4
|
+
import { useTranslation, useDataChanged } from '@fayz-ai/core'
|
|
5
5
|
import type { PluginQuickAction } from '@fayz-ai/core'
|
|
6
6
|
import { useModuleNavigation, ModuleActionBar, createViewRouter } from '@fayz-ai/saas'
|
|
7
7
|
import { MarketingContextProvider, type ResolvedMarketingConfig } from './MarketingContext'
|
|
@@ -19,6 +19,9 @@ import type { ContentPlannerUIState } from './views/content/contentStore'
|
|
|
19
19
|
import { ContentView } from './views/content/ContentView'
|
|
20
20
|
import { PostPage } from './views/content/PostPage'
|
|
21
21
|
import { PlanBriefView } from './views/content/PlanBriefView'
|
|
22
|
+
import { BlogAdminView } from './views/blog/BlogAdminView'
|
|
23
|
+
import { BlogEntityForm } from './views/blog/BlogEntityForm'
|
|
24
|
+
import { buildBlogPostEntity, buildBlogCategoryEntity } from './data/blogEntities'
|
|
22
25
|
|
|
23
26
|
function buildNav(
|
|
24
27
|
config: ResolvedMarketingConfig,
|
|
@@ -51,6 +54,14 @@ function buildNav(
|
|
|
51
54
|
onClick: () => navigate('content'),
|
|
52
55
|
})
|
|
53
56
|
}
|
|
57
|
+
if (config.modules.blog) {
|
|
58
|
+
items.push({
|
|
59
|
+
id: 'blog', label: config.labels.blog, icon: 'Newspaper',
|
|
60
|
+
active: view === 'blog' || view === 'blog-new' || view.startsWith('blog-post:')
|
|
61
|
+
|| view === 'blog-cat-new' || view.startsWith('blog-cat:'),
|
|
62
|
+
onClick: () => navigate('blog'),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
54
65
|
return items
|
|
55
66
|
}
|
|
56
67
|
|
|
@@ -63,15 +74,24 @@ export function MarketingPage({ config, provider, store, contentProvider, conten
|
|
|
63
74
|
}) {
|
|
64
75
|
const t = useTranslation()
|
|
65
76
|
const { view, direction, navigate } = useModuleNavigation('/marketing', {
|
|
66
|
-
overview: 0, channels: 0, campaigns: 0, funnel: 0, 'landing-pages': 0, content: 0,
|
|
77
|
+
overview: 0, channels: 0, campaigns: 0, funnel: 0, 'landing-pages': 0, content: 0, blog: 0,
|
|
67
78
|
'channel-detail': 1, 'content-post': 1, 'content-brief': 1,
|
|
79
|
+
'blog-new': 1, 'blog-post': 1, 'blog-cat-new': 1, 'blog-cat': 1,
|
|
68
80
|
}, 'overview')
|
|
69
81
|
|
|
70
82
|
React.useEffect(() => { void store.getState().load() }, [store])
|
|
71
83
|
|
|
84
|
+
// Agent RPC writes (createCampaign) land outside the provider — refresh past
|
|
85
|
+
// its read cache when any agent write is announced.
|
|
86
|
+
useDataChanged({ table: 'plg_marketing_campaigns' }, () => { void store.getState().refresh() }, [store])
|
|
87
|
+
|
|
72
88
|
const isOverview = view === 'overview'
|
|
73
89
|
const nav = buildNav(config, view, navigate)
|
|
74
90
|
|
|
91
|
+
// Blog backoffice entities — one shared EntityDef each drives the list + form.
|
|
92
|
+
const blogPostEntity = React.useMemo(() => buildBlogPostEntity(), [])
|
|
93
|
+
const blogCategoryEntity = React.useMemo(() => buildBlogCategoryEntity(), [])
|
|
94
|
+
|
|
75
95
|
const quickActions = React.useMemo<PluginQuickAction[]>(() => [
|
|
76
96
|
{ id: 'new-campaign', label: t('marketing.campaigns.new'), icon: 'Megaphone', description: config.labels.campaigns, action: () => navigate('campaigns') },
|
|
77
97
|
], [t, config.labels.campaigns])
|
|
@@ -85,6 +105,11 @@ export function MarketingPage({ config, provider, store, contentProvider, conten
|
|
|
85
105
|
{ id: 'content', render: () => <ContentView onOpenPost={(id) => navigate(`content-post:${id}`)} /> },
|
|
86
106
|
{ id: 'content-post', render: ({ id }) => <PostPage postId={id!} onBack={() => navigate('content')} /> },
|
|
87
107
|
{ id: 'content-brief', render: () => <PlanBriefView onBack={() => navigate('content')} /> },
|
|
108
|
+
{ id: 'blog', render: () => <BlogAdminView navigate={navigate} /> },
|
|
109
|
+
{ id: 'blog-new', render: () => <BlogEntityForm entity={blogPostEntity} parentLabel={config.labels.blog} onDone={() => navigate('blog')} /> },
|
|
110
|
+
{ id: 'blog-post', render: ({ id }) => <BlogEntityForm entity={blogPostEntity} editId={id!} parentLabel={config.labels.blog} onDone={() => navigate('blog')} /> },
|
|
111
|
+
{ id: 'blog-cat-new', render: () => <BlogEntityForm entity={blogCategoryEntity} parentLabel={config.labels.blogCategories} onDone={() => navigate('blog')} /> },
|
|
112
|
+
{ id: 'blog-cat', render: ({ id }) => <BlogEntityForm entity={blogCategoryEntity} editId={id!} parentLabel={config.labels.blogCategories} onDone={() => navigate('blog')} /> },
|
|
88
113
|
{ id: 'overview', render: () => <OverviewView /> },
|
|
89
114
|
], 'overview')
|
|
90
115
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { EntityDef, FieldDef } from '@fayz-ai/core'
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Blog backoffice entities — the single source of truth that drives BOTH the
|
|
5
|
+
// generic list (CrudListView columns + facets) and the create/edit form
|
|
6
|
+
// (CrudFormPage fields, incl. the markdown body + category relation).
|
|
7
|
+
//
|
|
8
|
+
// Persistence is the generic Supabase provider resolved from `data.table`
|
|
9
|
+
// (plg_blog_posts / plg_blog_categories, owned by @fayz-ai/plugin-blog's
|
|
10
|
+
// migrations). camelCase field keys map to snake_case columns automatically.
|
|
11
|
+
// The website reads the same rows through the anon v_public_blog_posts view.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export const BLOG_POSTS_TABLE = 'plg_blog_posts'
|
|
15
|
+
export const BLOG_CATEGORIES_TABLE = 'plg_blog_categories'
|
|
16
|
+
|
|
17
|
+
export interface BlogPostRow {
|
|
18
|
+
id: string
|
|
19
|
+
title: string
|
|
20
|
+
slug: string
|
|
21
|
+
excerpt: string
|
|
22
|
+
body: string
|
|
23
|
+
categoryId?: string
|
|
24
|
+
coverImage?: string
|
|
25
|
+
authorName?: string
|
|
26
|
+
authorRole?: string
|
|
27
|
+
authorAvatarUrl?: string
|
|
28
|
+
authorBio?: string
|
|
29
|
+
status: 'draft' | 'published'
|
|
30
|
+
readTime?: string
|
|
31
|
+
publishedAt?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BlogCategoryRow {
|
|
35
|
+
id: string
|
|
36
|
+
name: string
|
|
37
|
+
slug: string
|
|
38
|
+
description?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildBlogPostEntity(): EntityDef<BlogPostRow> {
|
|
42
|
+
const fields: FieldDef[] = [
|
|
43
|
+
{ key: 'title', label: 'Title', type: 'text', required: true, searchable: true, showInTable: true, sortable: true },
|
|
44
|
+
{ key: 'slug', label: 'Slug', type: 'text', required: true, searchable: true, showInTable: false, hint: 'URL identifier, e.g. "cannabis-and-oral-health"' },
|
|
45
|
+
{ key: 'status', label: 'Status', type: 'select', options: ['draft', 'published'], showInTable: true, defaultValue: 'draft' },
|
|
46
|
+
{ key: 'categoryId', label: 'Category', type: 'relation', relation: { table: BLOG_CATEGORIES_TABLE, valueField: 'id', labelField: 'name', tenantScoped: true }, showInTable: false },
|
|
47
|
+
{ key: 'excerpt', label: 'Excerpt', type: 'textarea', span: 2, showInTable: false, placeholder: 'Short summary shown on cards' },
|
|
48
|
+
{ key: 'body', label: 'Body', type: 'markdown', span: 2, showInTable: false },
|
|
49
|
+
{ key: 'coverImage', label: 'Cover image', type: 'image', showInTable: false, placeholder: 'https://…' },
|
|
50
|
+
{ key: 'readTime', label: 'Read time', type: 'text', showInTable: false, placeholder: '5 min' },
|
|
51
|
+
{ key: 'publishedAt', label: 'Published at', type: 'date', showInTable: true, sortable: true },
|
|
52
|
+
{ key: 'authorName', label: 'Author name', type: 'text', showInTable: false },
|
|
53
|
+
{ key: 'authorRole', label: 'Author role', type: 'text', showInTable: false, placeholder: 'e.g. Cirurgião-Dentista' },
|
|
54
|
+
{ key: 'authorAvatarUrl', label: 'Author avatar', type: 'image', showInTable: false, placeholder: 'https://…' },
|
|
55
|
+
{ key: 'authorBio', label: 'Author bio', type: 'textarea', span: 2, showInTable: false },
|
|
56
|
+
]
|
|
57
|
+
return {
|
|
58
|
+
name: 'Post',
|
|
59
|
+
namePlural: 'Posts',
|
|
60
|
+
icon: 'Newspaper',
|
|
61
|
+
fields,
|
|
62
|
+
displayField: 'title',
|
|
63
|
+
subtitleField: 'slug',
|
|
64
|
+
facets: [{ field: 'status', allLabel: 'All' }],
|
|
65
|
+
data: {
|
|
66
|
+
table: BLOG_POSTS_TABLE,
|
|
67
|
+
tenantScoped: true,
|
|
68
|
+
searchColumns: ['title', 'slug', 'excerpt'],
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildBlogCategoryEntity(): EntityDef<BlogCategoryRow> {
|
|
74
|
+
const fields: FieldDef[] = [
|
|
75
|
+
{ key: 'name', label: 'Name', type: 'text', required: true, searchable: true, showInTable: true, sortable: true },
|
|
76
|
+
{ key: 'slug', label: 'Slug', type: 'text', required: true, searchable: true, showInTable: true, hint: 'URL identifier' },
|
|
77
|
+
{ key: 'description', label: 'Description', type: 'textarea', span: 2, showInTable: false },
|
|
78
|
+
]
|
|
79
|
+
return {
|
|
80
|
+
name: 'Category',
|
|
81
|
+
namePlural: 'Categories',
|
|
82
|
+
icon: 'Tags',
|
|
83
|
+
fields,
|
|
84
|
+
displayField: 'name',
|
|
85
|
+
data: {
|
|
86
|
+
table: BLOG_CATEGORIES_TABLE,
|
|
87
|
+
tenantScoped: true,
|
|
88
|
+
searchColumns: ['name', 'slug'],
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Capability test for the marketing analytics data slice (mock provider) —
|
|
2
|
+
// template: plugin-tasks/src/data/capability.test.ts. The Supabase provider
|
|
3
|
+
// implements the same MarketingDataProvider interface against
|
|
4
|
+
// plg_marketing_channels/_campaigns + v_marketing_attribution.
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
import { createMockMarketingProvider } from './mock'
|
|
7
|
+
import { MARKETING_PRESETS } from '../presets'
|
|
8
|
+
|
|
9
|
+
function makeProvider() {
|
|
10
|
+
const preset = MARKETING_PRESETS.beauty
|
|
11
|
+
return createMockMarketingProvider({
|
|
12
|
+
channels: preset.channels,
|
|
13
|
+
conversion: preset.conversion,
|
|
14
|
+
domain: 'beauty',
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('plugin-marketing capability · analytics slice (mock provider)', () => {
|
|
19
|
+
it('reports per-channel performance for every configured channel', async () => {
|
|
20
|
+
const provider = makeProvider()
|
|
21
|
+
const perf = await provider.channelPerformance({ range: '30d' })
|
|
22
|
+
expect(perf.map((c) => c.channelId).sort()).toEqual(
|
|
23
|
+
MARKETING_PRESETS.beauty.channels.map((c) => c.id).sort(),
|
|
24
|
+
)
|
|
25
|
+
for (const c of perf) {
|
|
26
|
+
expect(c.conversions).toBeGreaterThanOrEqual(0)
|
|
27
|
+
expect(c.cvr).toBeGreaterThanOrEqual(0)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('overview aggregates the same channels the mix reports', async () => {
|
|
32
|
+
const provider = makeProvider()
|
|
33
|
+
const overview = await provider.overview({ range: '30d' })
|
|
34
|
+
expect(overview.channelMix.length).toBe(MARKETING_PRESETS.beauty.channels.length)
|
|
35
|
+
expect(overview.conversions).toBe(
|
|
36
|
+
overview.channelMix.reduce((s, c) => s + c.conversions, 0),
|
|
37
|
+
)
|
|
38
|
+
expect(overview.topChannelId).toBeTruthy()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('persists a created campaign: it surfaces in list and get', async () => {
|
|
42
|
+
const provider = makeProvider()
|
|
43
|
+
const created = await provider.saveCampaign({
|
|
44
|
+
name: 'Campanha Teste',
|
|
45
|
+
channelId: 'instagram',
|
|
46
|
+
status: 'active',
|
|
47
|
+
start: new Date('2026-07-01').toISOString(),
|
|
48
|
+
spend: 150,
|
|
49
|
+
})
|
|
50
|
+
expect(created.id).toBeTruthy()
|
|
51
|
+
|
|
52
|
+
const listed = await provider.listCampaigns({ range: '30d' })
|
|
53
|
+
expect(listed.some((c) => c.id === created.id)).toBe(true)
|
|
54
|
+
|
|
55
|
+
const fetched = await provider.getCampaign(created.id)
|
|
56
|
+
expect(fetched?.name).toBe('Campanha Teste')
|
|
57
|
+
expect(fetched?.channelId).toBe('instagram')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('round-trips update and delete', async () => {
|
|
61
|
+
const provider = makeProvider()
|
|
62
|
+
const created = await provider.saveCampaign({
|
|
63
|
+
name: 'temp',
|
|
64
|
+
channelId: 'google',
|
|
65
|
+
status: 'draft',
|
|
66
|
+
start: new Date('2026-07-01').toISOString(),
|
|
67
|
+
spend: 0,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const updated = await provider.saveCampaign({
|
|
71
|
+
id: created.id,
|
|
72
|
+
name: 'temp',
|
|
73
|
+
channelId: 'google',
|
|
74
|
+
status: 'paused',
|
|
75
|
+
start: created.start,
|
|
76
|
+
spend: 80,
|
|
77
|
+
})
|
|
78
|
+
expect(updated.status).toBe('paused')
|
|
79
|
+
|
|
80
|
+
await provider.deleteCampaign(created.id)
|
|
81
|
+
expect(await provider.getCampaign(created.id)).toBeNull()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('funnel stages end on the vertical conversion label', async () => {
|
|
85
|
+
const provider = makeProvider()
|
|
86
|
+
const funnel = await provider.funnel({ range: '30d' })
|
|
87
|
+
expect(funnel.length).toBeGreaterThanOrEqual(2)
|
|
88
|
+
expect(funnel[funnel.length - 1].label).toBe(
|
|
89
|
+
MARKETING_PRESETS.beauty.conversion.labelPlural,
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
})
|