@fayz-ai/plugin-marketing 0.9.0-next.1 → 0.9.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/README.md +3 -3
- package/dist/{MarketingContext.d.ts → context.d.ts} +5 -3
- package/dist/context.d.ts.map +1 -0
- package/dist/data/mock.d.ts +1 -1
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/{registries.d.ts → data/registries.d.ts} +1 -1
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -22
- package/dist/index.js.map +1 -1
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/{presets.d.ts → lib/presets.d.ts} +1 -1
- package/dist/lib/presets.d.ts.map +1 -0
- package/dist/{MarketingPage.d.ts → views/MarketingPage.d.ts} +5 -5
- package/dist/views/MarketingPage.d.ts.map +1 -0
- package/dist/views/blog/BlogAdminView.d.ts.map +1 -1
- package/dist/views/content/ContentPlannerContext.d.ts +1 -1
- package/dist/views/content/ContentPlannerContext.d.ts.map +1 -1
- package/dist/views/dashboardWidgets.d.ts +1 -1
- package/dist/views/dashboardWidgets.d.ts.map +1 -1
- package/package.json +6 -9
- package/dist/MarketingContext.d.ts.map +0 -1
- package/dist/MarketingPage.d.ts.map +0 -1
- package/dist/format.d.ts.map +0 -1
- package/dist/presets.d.ts.map +0 -1
- package/dist/registries.d.ts.map +0 -1
- package/src/MarketingContext.tsx +0 -42
- package/src/MarketingPage.tsx +0 -139
- package/src/components/MarketingBits.tsx +0 -131
- package/src/components/icons.tsx +0 -36
- package/src/data/blogEntities.tsx +0 -91
- package/src/data/capability.test.ts +0 -92
- package/src/data/contentMedia.ts +0 -25
- package/src/data/contentMock.ts +0 -253
- package/src/data/contentSupabase.ts +0 -263
- package/src/data/contentTypes.ts +0 -138
- package/src/data/mock.ts +0 -308
- package/src/data/supabase.ts +0 -613
- package/src/data/tables.ts +0 -33
- package/src/data/types.ts +0 -97
- package/src/format.ts +0 -40
- package/src/index.ts +0 -457
- package/src/integrations/ranklayer/RANKLAYER.md +0 -40
- package/src/integrations/ranklayer/connectorDef.tsx +0 -102
- package/src/integrations/ranklayer/data/supabase.ts +0 -106
- package/src/integrations/ranklayer/types.ts +0 -16
- package/src/locales/en.ts +0 -192
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -192
- package/src/migrations/000_plg_rename.sql +0 -15
- package/src/migrations/001_content_planner.sql +0 -94
- package/src/migrations/002_multi_platform.sql +0 -25
- package/src/migrations/003_recording_ops.sql +0 -25
- package/src/migrations/004_ranklayer.sql +0 -50
- package/src/migrations/005_analytics_base.sql +0 -60
- package/src/migrations/006_analytics_views.sql +0 -68
- package/src/migrations/007_agent_rpcs.sql +0 -119
- package/src/migrations/008_landing_pages.sql +0 -112
- package/src/migrations/index.ts +0 -601
- package/src/presets.ts +0 -119
- package/src/registries.ts +0 -62
- package/src/schema/index.ts +0 -106
- package/src/store.ts +0 -102
- package/src/types.ts +0 -153
- package/src/views/CampaignComposer.tsx +0 -89
- package/src/views/CampaignsView.tsx +0 -59
- package/src/views/ChannelDetailView.tsx +0 -59
- package/src/views/ChannelsView.tsx +0 -46
- package/src/views/FunnelView.tsx +0 -56
- package/src/views/LandingPageComposer.tsx +0 -201
- package/src/views/LandingPagesView.tsx +0 -172
- package/src/views/OverviewView.tsx +0 -24
- package/src/views/SettingsView.tsx +0 -72
- package/src/views/blog/BlogAdminView.tsx +0 -99
- package/src/views/blog/BlogEntityForm.tsx +0 -101
- package/src/views/blog/useEntityCrud.ts +0 -50
- package/src/views/content/ContentPlannerContext.tsx +0 -13
- package/src/views/content/ContentView.tsx +0 -908
- package/src/views/content/PlanBriefView.tsx +0 -268
- package/src/views/content/PostPage.tsx +0 -370
- package/src/views/content/PostSocialPreview.tsx +0 -212
- package/src/views/content/RecordingChecklist.tsx +0 -145
- package/src/views/content/contentBits.tsx +0 -114
- package/src/views/content/contentStore.ts +0 -160
- package/src/views/content/templates.ts +0 -172
- package/src/views/dashboardWidgets.tsx +0 -173
- /package/dist/{format.d.ts → lib/format.d.ts} +0 -0
package/src/presets.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import type { AcquisitionChannel, ConversionModel } from './types'
|
|
2
|
-
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Domain presets — the abstraction that lets ONE plugin feed every vertical.
|
|
5
|
-
// Each preset defines what a "conversion" is (and where it's attributed from)
|
|
6
|
-
// plus the acquisition channels that vertical actually uses. Channel ids align
|
|
7
|
-
// with plugin-crm's `lead-sources` registry where they overlap.
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
export type MarketingDomain = 'agency' | 'beauty' | 'resto'
|
|
11
|
-
|
|
12
|
-
export interface MarketingDomainModules {
|
|
13
|
-
channels: boolean
|
|
14
|
-
campaigns: boolean
|
|
15
|
-
funnel: boolean
|
|
16
|
-
landingPages: boolean
|
|
17
|
-
/** Social content planner — accounts, week board, markdown post pages. */
|
|
18
|
-
contentPlanner: boolean
|
|
19
|
-
/** Blog backoffice — posts + categories CMS feeding @fayz-ai/plugin-blog. Off by default. */
|
|
20
|
-
blog: boolean
|
|
21
|
-
/** Outbound — reserved, off by default, implemented in a later milestone. */
|
|
22
|
-
broadcasts: boolean
|
|
23
|
-
journeys: boolean
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface MarketingDomainPreset {
|
|
27
|
-
conversion: ConversionModel
|
|
28
|
-
channels: AcquisitionChannel[]
|
|
29
|
-
modules: MarketingDomainModules
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const AGENCY: MarketingDomainPreset = {
|
|
33
|
-
conversion: {
|
|
34
|
-
id: 'won-deal',
|
|
35
|
-
label: 'Won deal',
|
|
36
|
-
labelPlural: 'Won deals',
|
|
37
|
-
valueLabel: 'Pipeline value',
|
|
38
|
-
source: 'crm',
|
|
39
|
-
avgValue: 3200,
|
|
40
|
-
},
|
|
41
|
-
channels: [
|
|
42
|
-
{ id: 'paid-search', label: 'Paid Search', icon: 'Search', kind: 'paid' },
|
|
43
|
-
{ id: 'paid-social', label: 'Paid Social', icon: 'Megaphone', kind: 'paid' },
|
|
44
|
-
{ id: 'organic', label: 'Organic / SEO', icon: 'Globe', kind: 'organic' },
|
|
45
|
-
{ id: 'email', label: 'Email', icon: 'Mail', kind: 'outbound' },
|
|
46
|
-
{ id: 'referral', label: 'Referral', icon: 'Users', kind: 'referral' },
|
|
47
|
-
{ id: 'direct', label: 'Direct', icon: 'MousePointerClick', kind: 'direct' },
|
|
48
|
-
],
|
|
49
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, blog: false, broadcasts: false, journeys: false },
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const BEAUTY: MarketingDomainPreset = {
|
|
53
|
-
conversion: {
|
|
54
|
-
id: 'booking',
|
|
55
|
-
label: 'Booking',
|
|
56
|
-
labelPlural: 'Bookings',
|
|
57
|
-
valueLabel: 'Booking revenue',
|
|
58
|
-
source: 'agenda',
|
|
59
|
-
avgValue: 85,
|
|
60
|
-
},
|
|
61
|
-
channels: [
|
|
62
|
-
{ id: 'instagram', label: 'Instagram', icon: 'Instagram', kind: 'social' },
|
|
63
|
-
{ id: 'google', label: 'Google', icon: 'Globe', kind: 'organic' },
|
|
64
|
-
{ id: 'referral', label: 'Referral', icon: 'Users', kind: 'referral' },
|
|
65
|
-
{ id: 'whatsapp', label: 'WhatsApp', icon: 'MessageCircle', kind: 'social' },
|
|
66
|
-
{ id: 'walkin', label: 'Walk-in', icon: 'DoorOpen', kind: 'direct' },
|
|
67
|
-
],
|
|
68
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, blog: false, broadcasts: false, journeys: false },
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const RESTO: MarketingDomainPreset = {
|
|
72
|
-
conversion: {
|
|
73
|
-
id: 'order',
|
|
74
|
-
label: 'Order',
|
|
75
|
-
labelPlural: 'Orders',
|
|
76
|
-
valueLabel: 'Revenue',
|
|
77
|
-
source: 'orders',
|
|
78
|
-
avgValue: 42,
|
|
79
|
-
},
|
|
80
|
-
channels: [
|
|
81
|
-
{ id: 'ifood', label: 'iFood', icon: 'Utensils', kind: 'paid' },
|
|
82
|
-
{ id: 'delivery', label: 'Delivery apps', icon: 'Bike', kind: 'paid' },
|
|
83
|
-
{ id: 'google', label: 'Google', icon: 'Globe', kind: 'organic' },
|
|
84
|
-
{ id: 'instagram', label: 'Instagram', icon: 'Instagram', kind: 'social' },
|
|
85
|
-
{ id: 'walkin', label: 'Walk-in', icon: 'DoorOpen', kind: 'direct' },
|
|
86
|
-
],
|
|
87
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, blog: false, broadcasts: false, journeys: false },
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export const MARKETING_PRESETS: Record<MarketingDomain, MarketingDomainPreset> = {
|
|
91
|
-
agency: AGENCY,
|
|
92
|
-
beauty: BEAUTY,
|
|
93
|
-
resto: RESTO,
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Vertical-flavored campaign name templates for the mock seed. */
|
|
97
|
-
export const CAMPAIGN_NAME_SEEDS: Record<MarketingDomain, string[]> = {
|
|
98
|
-
agency: [
|
|
99
|
-
'Q2 Lead-gen — Search',
|
|
100
|
-
'LinkedIn Retargeting',
|
|
101
|
-
'Webinar Funnel',
|
|
102
|
-
'Newsletter Nurture',
|
|
103
|
-
'Partner Referral Push',
|
|
104
|
-
],
|
|
105
|
-
beauty: [
|
|
106
|
-
'Summer Glow Promo',
|
|
107
|
-
'New Client — Instagram',
|
|
108
|
-
'Birthday Offer Blast',
|
|
109
|
-
'Win-back 90 days',
|
|
110
|
-
'Google Reviews Boost',
|
|
111
|
-
],
|
|
112
|
-
resto: [
|
|
113
|
-
'iFood Weekend Combo',
|
|
114
|
-
'Happy Hour Promo',
|
|
115
|
-
'New Menu Launch',
|
|
116
|
-
'Loyalty Double Points',
|
|
117
|
-
'Instagram Stories Drop',
|
|
118
|
-
],
|
|
119
|
-
}
|
package/src/registries.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import type { PluginRegistryDef, EntityDef } from '@fayz-ai/core'
|
|
2
|
-
import type { AcquisitionChannel } from './types'
|
|
3
|
-
import { T } from './data/tables'
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Settings registries — surfaced as CRUD tabs in the central Settings area
|
|
7
|
-
// (mirrors how plugin-menu exposes Allergens / Dietary Tags). "Channels" lists
|
|
8
|
-
// the acquisition channels resolved from the domain preset.
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
const KIND_OPTIONS = [
|
|
12
|
-
{ value: 'paid', label: 'Paid' },
|
|
13
|
-
{ value: 'organic', label: 'Organic' },
|
|
14
|
-
{ value: 'social', label: 'Social' },
|
|
15
|
-
{ value: 'referral', label: 'Referral' },
|
|
16
|
-
{ value: 'direct', label: 'Direct' },
|
|
17
|
-
{ value: 'outbound', label: 'Outbound' },
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
const channelEntity: EntityDef = {
|
|
21
|
-
name: 'Channel',
|
|
22
|
-
namePlural: 'Channels',
|
|
23
|
-
icon: 'Radio',
|
|
24
|
-
displayField: 'label',
|
|
25
|
-
defaultSort: 'label',
|
|
26
|
-
fields: [
|
|
27
|
-
{ key: 'label', label: 'Channel', type: 'text', required: true, showInTable: true, searchable: true },
|
|
28
|
-
{ key: 'channelKey', label: 'Key', type: 'text', required: true, showInTable: true },
|
|
29
|
-
{ key: 'kind', label: 'Type', type: 'select', options: KIND_OPTIONS, showInTable: true },
|
|
30
|
-
{ key: 'icon', label: 'Icon', type: 'text' },
|
|
31
|
-
{ key: 'monthlySpend', label: 'Monthly spend', type: 'number', showInTable: true, defaultValue: 0 },
|
|
32
|
-
{ key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true, inlineToggle: true },
|
|
33
|
-
],
|
|
34
|
-
facets: [{ field: 'kind' }],
|
|
35
|
-
data: {
|
|
36
|
-
table: T.channels,
|
|
37
|
-
tenantScoped: true,
|
|
38
|
-
searchColumns: ['label', 'channel_key'],
|
|
39
|
-
},
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Build the marketing settings registries. Channels are tenant-editable rows
|
|
43
|
-
* in plg_marketing_channels; the Supabase provider lazily seeds them from the
|
|
44
|
-
* resolved domain preset (seedData doubles as walkable mock data). Sites are
|
|
45
|
-
* NOT here on purpose: they are the tenant's fayz projects, managed by the
|
|
46
|
-
* shell in Settings → Sites, and marketing only points a page at one. */
|
|
47
|
-
export function buildMarketingRegistries(channels: AcquisitionChannel[]): PluginRegistryDef[] {
|
|
48
|
-
return [
|
|
49
|
-
{
|
|
50
|
-
id: 'channels',
|
|
51
|
-
entity: channelEntity,
|
|
52
|
-
icon: 'Radio',
|
|
53
|
-
description: 'Acquisition channels tracked for this workspace',
|
|
54
|
-
display: 'table',
|
|
55
|
-
seedData: [
|
|
56
|
-
...channels.map((c) => ({
|
|
57
|
-
id: c.id, channelKey: c.id, label: c.label, kind: c.kind, icon: c.icon, isActive: true, monthlySpend: 0,
|
|
58
|
-
})),
|
|
59
|
-
],
|
|
60
|
-
},
|
|
61
|
-
]
|
|
62
|
-
}
|
package/src/schema/index.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// Builders come from @fayz-ai/db (single drizzle-orm instance) — never import
|
|
2
|
-
// pgTable/uuid directly from drizzle-orm/pg-core. See MIGRATION-ARCHITECTURE §6.
|
|
3
|
-
import {
|
|
4
|
-
pgTable, uuid, text, boolean, integer, date, jsonb, index, uniqueIndex,
|
|
5
|
-
numeric, timestamp,
|
|
6
|
-
tenantId, timestamps, createdAt,
|
|
7
|
-
} from '@fayz-ai/db'
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Marketing plugin — Ring-1 schema (schema-as-code), content planner tables.
|
|
11
|
-
// RLS policies + grants are companion SQL (Drizzle doesn't diff those) —
|
|
12
|
-
// see src/migrations/001_content_planner.sql.
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
// Acquisition channels — tenant-owned rows seeded from the domain preset.
|
|
16
|
-
// `channel_key` is the stable preset/config id ('instagram', 'paid-search'…),
|
|
17
|
-
// aligned with plugin-crm's lead-sources where they overlap.
|
|
18
|
-
export const mktChannels = pgTable('plg_marketing_channels', {
|
|
19
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
20
|
-
tenantId: tenantId(),
|
|
21
|
-
channelKey: text('channel_key').notNull(),
|
|
22
|
-
label: text('label').notNull(),
|
|
23
|
-
icon: text('icon'),
|
|
24
|
-
kind: text('kind').notNull().default('organic'),
|
|
25
|
-
isActive: boolean('is_active').notNull().default(true),
|
|
26
|
-
monthlySpend: numeric('monthly_spend').notNull().default('0'),
|
|
27
|
-
...timestamps,
|
|
28
|
-
}, (t) => [
|
|
29
|
-
index('idx_plg_marketing_channels_tenant').on(t.tenantId),
|
|
30
|
-
uniqueIndex('u_plg_marketing_channels_tenant_key').on(t.tenantId, t.channelKey),
|
|
31
|
-
])
|
|
32
|
-
|
|
33
|
-
// Acquisition campaigns — real CRUD; performance is DERIVED at read time from
|
|
34
|
-
// v_marketing_attribution (channel match within the campaign window), only
|
|
35
|
-
// spend is stored.
|
|
36
|
-
export const mktCampaigns = pgTable('plg_marketing_campaigns', {
|
|
37
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
38
|
-
tenantId: tenantId(),
|
|
39
|
-
name: text('name').notNull(),
|
|
40
|
-
channelKey: text('channel_key').notNull(),
|
|
41
|
-
status: text('status').notNull().default('draft'),
|
|
42
|
-
startsAt: timestamp('starts_at', { withTimezone: true }),
|
|
43
|
-
endsAt: timestamp('ends_at', { withTimezone: true }),
|
|
44
|
-
spend: numeric('spend').notNull().default('0'),
|
|
45
|
-
...timestamps,
|
|
46
|
-
}, (t) => [
|
|
47
|
-
index('idx_plg_marketing_campaigns_tenant').on(t.tenantId),
|
|
48
|
-
index('idx_plg_marketing_campaigns_channel').on(t.tenantId, t.channelKey),
|
|
49
|
-
])
|
|
50
|
-
|
|
51
|
-
export const mktSocialAccounts = pgTable('plg_marketing_social_accounts', {
|
|
52
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
53
|
-
tenantId: tenantId(),
|
|
54
|
-
name: text('name').notNull(),
|
|
55
|
-
handle: text('handle'),
|
|
56
|
-
// One account, many platform connections (Instagram, YouTube, TikTok, ...)
|
|
57
|
-
platforms: text('platforms').array().notNull().default(['instagram']),
|
|
58
|
-
isActive: boolean('is_active').notNull().default(true),
|
|
59
|
-
...createdAt,
|
|
60
|
-
}, (t) => [index('idx_plg_marketing_social_accounts_tenant').on(t.tenantId)])
|
|
61
|
-
|
|
62
|
-
export const mktContentPlans = pgTable('plg_marketing_content_plans', {
|
|
63
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
64
|
-
tenantId: tenantId(),
|
|
65
|
-
accountId: uuid('account_id').notNull().references(() => mktSocialAccounts.id, { onDelete: 'cascade' }),
|
|
66
|
-
name: text('name').notNull(),
|
|
67
|
-
status: text('status').notNull().default('active'),
|
|
68
|
-
weeksCount: integer('weeks_count').notNull().default(8),
|
|
69
|
-
startDate: date('start_date'),
|
|
70
|
-
objective: text('objective'),
|
|
71
|
-
tone: text('tone'),
|
|
72
|
-
pillars: jsonb('pillars').notNull().default([]),
|
|
73
|
-
formats: jsonb('formats').notNull().default([]),
|
|
74
|
-
weeklyFrequency: integer('weekly_frequency').notNull().default(3),
|
|
75
|
-
briefMd: text('brief_md').notNull().default(''),
|
|
76
|
-
...timestamps,
|
|
77
|
-
}, (t) => [
|
|
78
|
-
index('idx_plg_marketing_content_plans_tenant').on(t.tenantId),
|
|
79
|
-
index('idx_plg_marketing_content_plans_account').on(t.accountId),
|
|
80
|
-
])
|
|
81
|
-
|
|
82
|
-
export const mktContentPosts = pgTable('plg_marketing_content_posts', {
|
|
83
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
84
|
-
tenantId: tenantId(),
|
|
85
|
-
planId: uuid('plan_id').notNull().references(() => mktContentPlans.id, { onDelete: 'cascade' }),
|
|
86
|
-
weekNumber: integer('week_number').notNull().default(1),
|
|
87
|
-
position: integer('position').notNull().default(0),
|
|
88
|
-
title: text('title').notNull(),
|
|
89
|
-
format: text('format').notNull().default('reel'),
|
|
90
|
-
status: text('status').notNull().default('idea'),
|
|
91
|
-
scheduledDate: date('scheduled_date'),
|
|
92
|
-
// Target platforms for this post; empty = inherit the account's platforms
|
|
93
|
-
platforms: text('platforms').array().notNull().default([]),
|
|
94
|
-
// Recording-day checklist [{ id, text, done }]
|
|
95
|
-
checklist: jsonb('checklist').notNull().default([]),
|
|
96
|
-
// Uploaded asset (static posts: caption + final art instead of a script)
|
|
97
|
-
mediaUrl: text('media_url'),
|
|
98
|
-
hook: text('hook'),
|
|
99
|
-
cta: text('cta'),
|
|
100
|
-
contentMd: text('content_md').notNull().default(''),
|
|
101
|
-
...timestamps,
|
|
102
|
-
}, (t) => [
|
|
103
|
-
index('idx_plg_marketing_content_posts_tenant').on(t.tenantId),
|
|
104
|
-
index('idx_plg_marketing_content_posts_plan').on(t.planId, t.weekNumber, t.position),
|
|
105
|
-
index('idx_plg_marketing_content_posts_status').on(t.tenantId, t.status),
|
|
106
|
-
])
|
package/src/store.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { createStore, type StoreApi } from 'zustand/vanilla'
|
|
2
|
-
import type { MarketingDataProvider, SaveCampaignInput, SaveLandingPageInput } from './data/types'
|
|
3
|
-
import type {
|
|
4
|
-
Campaign,
|
|
5
|
-
ChannelPerformance,
|
|
6
|
-
DateRangeKey,
|
|
7
|
-
FunnelStage,
|
|
8
|
-
LandingPage,
|
|
9
|
-
LandingPagePerf,
|
|
10
|
-
MarketingOverview,
|
|
11
|
-
MarketingSite,
|
|
12
|
-
} from './types'
|
|
13
|
-
|
|
14
|
-
export interface MarketingUIState {
|
|
15
|
-
range: DateRangeKey
|
|
16
|
-
overview: MarketingOverview | null
|
|
17
|
-
channels: ChannelPerformance[]
|
|
18
|
-
campaigns: Campaign[]
|
|
19
|
-
funnel: FunnelStage[]
|
|
20
|
-
landingPages: LandingPagePerf[]
|
|
21
|
-
sites: MarketingSite[]
|
|
22
|
-
leadTags: Array<{ slug: string; name: string }>
|
|
23
|
-
loading: boolean
|
|
24
|
-
|
|
25
|
-
load(): Promise<void>
|
|
26
|
-
/** Invalidate the provider cache and reload (external writes, e.g. agent RPCs). */
|
|
27
|
-
refresh(): Promise<void>
|
|
28
|
-
setRange(range: DateRangeKey): Promise<void>
|
|
29
|
-
saveCampaign(input: SaveCampaignInput): Promise<Campaign>
|
|
30
|
-
deleteCampaign(id: string): Promise<void>
|
|
31
|
-
saveLandingPage(input: SaveLandingPageInput): Promise<LandingPage>
|
|
32
|
-
deleteLandingPage(id: string): Promise<void>
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function createMarketingStore(provider: MarketingDataProvider): StoreApi<MarketingUIState> {
|
|
36
|
-
return createStore<MarketingUIState>((set, get) => ({
|
|
37
|
-
range: '30d',
|
|
38
|
-
overview: null,
|
|
39
|
-
channels: [],
|
|
40
|
-
campaigns: [],
|
|
41
|
-
funnel: [],
|
|
42
|
-
landingPages: [],
|
|
43
|
-
sites: [],
|
|
44
|
-
leadTags: [],
|
|
45
|
-
loading: false,
|
|
46
|
-
|
|
47
|
-
async load() {
|
|
48
|
-
const range = get().range
|
|
49
|
-
set({ loading: true })
|
|
50
|
-
try {
|
|
51
|
-
const [overview, channels, campaigns, funnel, landingPages, sites, leadTags] = await Promise.all([
|
|
52
|
-
provider.overview({ range }),
|
|
53
|
-
provider.channelPerformance({ range }),
|
|
54
|
-
provider.listCampaigns({ range }),
|
|
55
|
-
provider.funnel({ range }),
|
|
56
|
-
provider.landingPages({ range }),
|
|
57
|
-
provider.listSites(),
|
|
58
|
-
provider.listLeadTags(),
|
|
59
|
-
])
|
|
60
|
-
set({ overview, channels, campaigns, funnel, landingPages, sites, leadTags, loading: false })
|
|
61
|
-
} catch (err) {
|
|
62
|
-
// A failed load must never wedge the surface in `loading` forever.
|
|
63
|
-
set({ loading: false })
|
|
64
|
-
throw err
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
async refresh() {
|
|
69
|
-
provider.invalidateCache?.()
|
|
70
|
-
await get().load()
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
async setRange(range) {
|
|
74
|
-
set({ range })
|
|
75
|
-
await get().load()
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
async saveCampaign(input) {
|
|
79
|
-
const created = await provider.saveCampaign(input)
|
|
80
|
-
await get().load()
|
|
81
|
-
return created
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
async deleteCampaign(id) {
|
|
85
|
-
await provider.deleteCampaign(id)
|
|
86
|
-
await get().load()
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
async saveLandingPage(input) {
|
|
90
|
-
const saved = await provider.saveLandingPage(input)
|
|
91
|
-
provider.invalidateCache?.()
|
|
92
|
-
await get().load()
|
|
93
|
-
return saved
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
async deleteLandingPage(id) {
|
|
97
|
-
await provider.deleteLandingPage(id)
|
|
98
|
-
provider.invalidateCache?.()
|
|
99
|
-
await get().load()
|
|
100
|
-
},
|
|
101
|
-
}))
|
|
102
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// @fayz-ai/plugin-marketing — domain model for ACQUISITION & CONVERSION
|
|
3
|
-
// analytics. The plugin is vertical-agnostic: each app supplies a
|
|
4
|
-
// `ConversionModel` (what a conversion is + where it's read from) and a set of
|
|
5
|
-
// `AcquisitionChannel`s, and the same analytics engine adapts.
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
export type AcquisitionChannelKind =
|
|
9
|
-
| 'paid'
|
|
10
|
-
| 'organic'
|
|
11
|
-
| 'social'
|
|
12
|
-
| 'referral'
|
|
13
|
-
| 'direct'
|
|
14
|
-
| 'outbound'
|
|
15
|
-
|
|
16
|
-
export interface AcquisitionChannel {
|
|
17
|
-
id: string
|
|
18
|
-
label: string
|
|
19
|
-
/** lucide-react icon name (resolved in components/icons.ts) */
|
|
20
|
-
icon: string
|
|
21
|
-
kind: AcquisitionChannelKind
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Where attribution reads conversions from (per vertical). */
|
|
25
|
-
export type ConversionSource = 'crm' | 'agenda' | 'orders' | 'custom'
|
|
26
|
-
|
|
27
|
-
export interface ConversionModel {
|
|
28
|
-
id: string
|
|
29
|
-
/** singular noun for one conversion, e.g. 'Won deal' | 'Booking' | 'Order' */
|
|
30
|
-
label: string
|
|
31
|
-
labelPlural: string
|
|
32
|
-
/** label for the monetary value, e.g. 'Pipeline value' | 'Revenue' */
|
|
33
|
-
valueLabel: string
|
|
34
|
-
source: ConversionSource
|
|
35
|
-
/** average monetary value of a single conversion (mock + estimates) */
|
|
36
|
-
avgValue: number
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type CampaignStatus = 'active' | 'paused' | 'ended' | 'draft'
|
|
40
|
-
|
|
41
|
-
/** An acquisition campaign running on a channel, with attributed performance. */
|
|
42
|
-
export interface Campaign {
|
|
43
|
-
id: string
|
|
44
|
-
name: string
|
|
45
|
-
channelId: string
|
|
46
|
-
status: CampaignStatus
|
|
47
|
-
start: string
|
|
48
|
-
end?: string
|
|
49
|
-
spend: number
|
|
50
|
-
/** visits / sessions / sends reached (NOT raw impressions) */
|
|
51
|
-
reach: number
|
|
52
|
-
leads: number
|
|
53
|
-
conversions: number
|
|
54
|
-
/** attributed monetary value */
|
|
55
|
-
value: number
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Per-channel rollup used by the Channels view + Overview. */
|
|
59
|
-
export interface ChannelPerformance {
|
|
60
|
-
channelId: string
|
|
61
|
-
reach: number
|
|
62
|
-
leads: number
|
|
63
|
-
conversions: number
|
|
64
|
-
spend: number
|
|
65
|
-
value: number
|
|
66
|
-
/** conversions / reach * 100 */
|
|
67
|
-
cvr: number
|
|
68
|
-
/** spend / conversions (0 when no spend) */
|
|
69
|
-
cpa: number
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface FunnelStage {
|
|
73
|
-
id: string
|
|
74
|
-
label: string
|
|
75
|
-
count: number
|
|
76
|
-
color: string
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** A site the tenant owns — a row of public.fayz_projects, managed in
|
|
80
|
-
* Settings → Sites. `appId`/`fayzProjectId` are the deploy's ids on the
|
|
81
|
-
* hosting platform: the keys the visit-analytics call needs (app + route +
|
|
82
|
-
* period). Marketing reads these; it does not own them. */
|
|
83
|
-
export interface MarketingSite {
|
|
84
|
-
id: string
|
|
85
|
-
name: string
|
|
86
|
-
/** live site URL, e.g. 'https://espacorenovario.com.br' */
|
|
87
|
-
url?: string
|
|
88
|
-
appId?: string
|
|
89
|
-
fayzProjectId?: string
|
|
90
|
-
/** Fayz editor for this project — where a planned page gets built. */
|
|
91
|
-
editorUrl?: string
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** 'live' = the route already exists on the site. 'planned' = it does not yet;
|
|
95
|
-
* `prompt` is the brief for building it with Fayz. */
|
|
96
|
-
export type LandingPageStatus = 'live' | 'planned'
|
|
97
|
-
|
|
98
|
-
/** A landing page IS a site + a route. `leadTags` is the CRM cross: leads that
|
|
99
|
-
* come in through the page carry those tag slugs. */
|
|
100
|
-
export interface LandingPage {
|
|
101
|
-
id: string
|
|
102
|
-
siteId: string
|
|
103
|
-
name: string
|
|
104
|
-
/** path within the site, always leading-slashed, e.g. '/lp-capilar' */
|
|
105
|
-
route: string
|
|
106
|
-
leadTags: string[]
|
|
107
|
-
status: LandingPageStatus
|
|
108
|
-
/** what the page should be — only meaningful while status is 'planned' */
|
|
109
|
-
prompt?: string
|
|
110
|
-
isActive: boolean
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface LandingPagePerf extends LandingPage {
|
|
114
|
-
siteName: string
|
|
115
|
-
siteUrl?: string
|
|
116
|
-
appId?: string
|
|
117
|
-
editorUrl?: string
|
|
118
|
-
/** full public URL (site + route); the bare route when the site has no URL */
|
|
119
|
-
url: string
|
|
120
|
-
/** null while no visit source is connected — renders as "—", not as zero. */
|
|
121
|
-
visits: number | null
|
|
122
|
-
/** leads carrying `leadTag` within the queried period */
|
|
123
|
-
conversions: number
|
|
124
|
-
/** conversions / visits * 100 — null when visits are unknown */
|
|
125
|
-
cvr: number | null
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export interface MarketingOverview {
|
|
129
|
-
conversions: number
|
|
130
|
-
conversionsPrev: number
|
|
131
|
-
cvr: number
|
|
132
|
-
cvrPrev: number
|
|
133
|
-
spend: number
|
|
134
|
-
cpa: number
|
|
135
|
-
value: number
|
|
136
|
-
topChannelId: string | null
|
|
137
|
-
/** conversions per channel for the mix bars */
|
|
138
|
-
channelMix: Array<{ channelId: string; conversions: number }>
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export type DateRangeKey = '7d' | '30d' | '90d'
|
|
142
|
-
|
|
143
|
-
export interface MarketingQuery {
|
|
144
|
-
range: DateRangeKey
|
|
145
|
-
/** optionally scope to a single channel (channel detail) */
|
|
146
|
-
channelId?: string
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export const DATE_RANGE_LABELS: Record<DateRangeKey, string> = {
|
|
150
|
-
'7d': 'Last 7 days',
|
|
151
|
-
'30d': 'Last 30 days',
|
|
152
|
-
'90d': 'Last 90 days',
|
|
153
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import {
|
|
3
|
-
Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter,
|
|
4
|
-
Button, Input,
|
|
5
|
-
Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
|
|
6
|
-
} from '@fayz-ai/ui'
|
|
7
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
8
|
-
import { useMarketingConfig, useMarketingStore } from '../MarketingContext'
|
|
9
|
-
import type { CampaignStatus } from '../types'
|
|
10
|
-
|
|
11
|
-
const STATUSES: CampaignStatus[] = ['draft', 'active', 'paused']
|
|
12
|
-
|
|
13
|
-
export function CampaignComposer({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
14
|
-
const t = useTranslation()
|
|
15
|
-
const { channels } = useMarketingConfig()
|
|
16
|
-
const saveCampaign = useMarketingStore((s) => s.saveCampaign)
|
|
17
|
-
|
|
18
|
-
const [name, setName] = React.useState('')
|
|
19
|
-
const [channelId, setChannelId] = React.useState(channels[0]?.id ?? '')
|
|
20
|
-
const [status, setStatus] = React.useState<CampaignStatus>('active')
|
|
21
|
-
const [spend, setSpend] = React.useState('')
|
|
22
|
-
const [saving, setSaving] = React.useState(false)
|
|
23
|
-
|
|
24
|
-
React.useEffect(() => {
|
|
25
|
-
if (open) {
|
|
26
|
-
setName(''); setChannelId(channels[0]?.id ?? ''); setStatus('active'); setSpend('')
|
|
27
|
-
}
|
|
28
|
-
}, [open, channels])
|
|
29
|
-
|
|
30
|
-
async function handleSave() {
|
|
31
|
-
if (!name.trim() || !channelId) return
|
|
32
|
-
setSaving(true)
|
|
33
|
-
await saveCampaign({
|
|
34
|
-
name: name.trim(),
|
|
35
|
-
channelId,
|
|
36
|
-
status,
|
|
37
|
-
start: new Date().toISOString(),
|
|
38
|
-
spend: Number(spend) || 0,
|
|
39
|
-
})
|
|
40
|
-
setSaving(false)
|
|
41
|
-
onClose()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<Modal open={open} onOpenChange={(o) => { if (!o) onClose() }}>
|
|
46
|
-
<ModalContent size="md">
|
|
47
|
-
<ModalHeader>
|
|
48
|
-
<ModalTitle>{t('marketing.composer.title')}</ModalTitle>
|
|
49
|
-
</ModalHeader>
|
|
50
|
-
<ModalBody className="space-y-4">
|
|
51
|
-
<label className="block space-y-1.5">
|
|
52
|
-
<span className="text-sm font-medium text-foreground">{t('marketing.composer.name')}</span>
|
|
53
|
-
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder={t('marketing.composer.namePlaceholder')} />
|
|
54
|
-
</label>
|
|
55
|
-
|
|
56
|
-
<label className="block space-y-1.5">
|
|
57
|
-
<span className="text-sm font-medium text-foreground">{t('marketing.composer.channel')}</span>
|
|
58
|
-
<Select value={channelId} onValueChange={setChannelId}>
|
|
59
|
-
<SelectTrigger><SelectValue placeholder={t('marketing.composer.channelPlaceholder')} /></SelectTrigger>
|
|
60
|
-
<SelectContent>
|
|
61
|
-
{channels.map((c) => <SelectItem key={c.id} value={c.id}>{c.label}</SelectItem>)}
|
|
62
|
-
</SelectContent>
|
|
63
|
-
</Select>
|
|
64
|
-
</label>
|
|
65
|
-
|
|
66
|
-
<div className="grid grid-cols-2 gap-3">
|
|
67
|
-
<label className="block space-y-1.5">
|
|
68
|
-
<span className="text-sm font-medium text-foreground">{t('marketing.composer.status')}</span>
|
|
69
|
-
<Select value={status} onValueChange={(v) => setStatus(v as CampaignStatus)}>
|
|
70
|
-
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
71
|
-
<SelectContent>
|
|
72
|
-
{STATUSES.map((s) => <SelectItem key={s} value={s} className="capitalize">{t(`marketing.status.${s}`)}</SelectItem>)}
|
|
73
|
-
</SelectContent>
|
|
74
|
-
</Select>
|
|
75
|
-
</label>
|
|
76
|
-
<label className="block space-y-1.5">
|
|
77
|
-
<span className="text-sm font-medium text-foreground">{t('marketing.composer.budget')}</span>
|
|
78
|
-
<Input type="number" min={0} value={spend} onChange={(e) => setSpend(e.target.value)} placeholder="0" />
|
|
79
|
-
</label>
|
|
80
|
-
</div>
|
|
81
|
-
</ModalBody>
|
|
82
|
-
<ModalFooter>
|
|
83
|
-
<Button variant="outline" onClick={onClose}>{t('marketing.composer.cancel')}</Button>
|
|
84
|
-
<Button onClick={handleSave} disabled={saving || !name.trim()}>{t('marketing.composer.create')}</Button>
|
|
85
|
-
</ModalFooter>
|
|
86
|
-
</ModalContent>
|
|
87
|
-
</Modal>
|
|
88
|
-
)
|
|
89
|
-
}
|