@fayz-ai/plugin-marketing 0.9.0 → 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.
Files changed (88) hide show
  1. package/README.md +3 -3
  2. package/dist/{MarketingContext.d.ts → context.d.ts} +5 -3
  3. package/dist/context.d.ts.map +1 -0
  4. package/dist/data/mock.d.ts +1 -1
  5. package/dist/data/mock.d.ts.map +1 -1
  6. package/dist/{registries.d.ts → data/registries.d.ts} +1 -1
  7. package/dist/data/registries.d.ts.map +1 -0
  8. package/dist/index.d.ts +4 -4
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +17 -22
  11. package/dist/index.js.map +1 -1
  12. package/dist/lib/format.d.ts.map +1 -0
  13. package/dist/{presets.d.ts → lib/presets.d.ts} +1 -1
  14. package/dist/lib/presets.d.ts.map +1 -0
  15. package/dist/{MarketingPage.d.ts → views/MarketingPage.d.ts} +5 -5
  16. package/dist/views/MarketingPage.d.ts.map +1 -0
  17. package/dist/views/blog/BlogAdminView.d.ts.map +1 -1
  18. package/dist/views/content/ContentPlannerContext.d.ts +1 -1
  19. package/dist/views/content/ContentPlannerContext.d.ts.map +1 -1
  20. package/dist/views/dashboardWidgets.d.ts +1 -1
  21. package/dist/views/dashboardWidgets.d.ts.map +1 -1
  22. package/package.json +6 -9
  23. package/dist/MarketingContext.d.ts.map +0 -1
  24. package/dist/MarketingPage.d.ts.map +0 -1
  25. package/dist/format.d.ts.map +0 -1
  26. package/dist/presets.d.ts.map +0 -1
  27. package/dist/registries.d.ts.map +0 -1
  28. package/src/MarketingContext.tsx +0 -42
  29. package/src/MarketingPage.tsx +0 -139
  30. package/src/components/MarketingBits.tsx +0 -131
  31. package/src/components/icons.tsx +0 -36
  32. package/src/data/blogEntities.tsx +0 -91
  33. package/src/data/capability.test.ts +0 -92
  34. package/src/data/contentMedia.ts +0 -25
  35. package/src/data/contentMock.ts +0 -253
  36. package/src/data/contentSupabase.ts +0 -263
  37. package/src/data/contentTypes.ts +0 -138
  38. package/src/data/mock.ts +0 -308
  39. package/src/data/supabase.ts +0 -613
  40. package/src/data/tables.ts +0 -33
  41. package/src/data/types.ts +0 -97
  42. package/src/format.ts +0 -40
  43. package/src/index.ts +0 -457
  44. package/src/integrations/ranklayer/RANKLAYER.md +0 -40
  45. package/src/integrations/ranklayer/connectorDef.tsx +0 -102
  46. package/src/integrations/ranklayer/data/supabase.ts +0 -106
  47. package/src/integrations/ranklayer/types.ts +0 -16
  48. package/src/locales/en.ts +0 -192
  49. package/src/locales/index.ts +0 -7
  50. package/src/locales/pt-BR.ts +0 -192
  51. package/src/migrations/000_plg_rename.sql +0 -15
  52. package/src/migrations/001_content_planner.sql +0 -94
  53. package/src/migrations/002_multi_platform.sql +0 -25
  54. package/src/migrations/003_recording_ops.sql +0 -25
  55. package/src/migrations/004_ranklayer.sql +0 -50
  56. package/src/migrations/005_analytics_base.sql +0 -60
  57. package/src/migrations/006_analytics_views.sql +0 -68
  58. package/src/migrations/007_agent_rpcs.sql +0 -119
  59. package/src/migrations/008_landing_pages.sql +0 -112
  60. package/src/migrations/index.ts +0 -601
  61. package/src/presets.ts +0 -119
  62. package/src/registries.ts +0 -62
  63. package/src/schema/index.ts +0 -106
  64. package/src/store.ts +0 -102
  65. package/src/types.ts +0 -153
  66. package/src/views/CampaignComposer.tsx +0 -89
  67. package/src/views/CampaignsView.tsx +0 -59
  68. package/src/views/ChannelDetailView.tsx +0 -59
  69. package/src/views/ChannelsView.tsx +0 -46
  70. package/src/views/FunnelView.tsx +0 -56
  71. package/src/views/LandingPageComposer.tsx +0 -201
  72. package/src/views/LandingPagesView.tsx +0 -172
  73. package/src/views/OverviewView.tsx +0 -24
  74. package/src/views/SettingsView.tsx +0 -72
  75. package/src/views/blog/BlogAdminView.tsx +0 -99
  76. package/src/views/blog/BlogEntityForm.tsx +0 -101
  77. package/src/views/blog/useEntityCrud.ts +0 -50
  78. package/src/views/content/ContentPlannerContext.tsx +0 -13
  79. package/src/views/content/ContentView.tsx +0 -908
  80. package/src/views/content/PlanBriefView.tsx +0 -268
  81. package/src/views/content/PostPage.tsx +0 -370
  82. package/src/views/content/PostSocialPreview.tsx +0 -212
  83. package/src/views/content/RecordingChecklist.tsx +0 -145
  84. package/src/views/content/contentBits.tsx +0 -114
  85. package/src/views/content/contentStore.ts +0 -160
  86. package/src/views/content/templates.ts +0 -172
  87. package/src/views/dashboardWidgets.tsx +0 -173
  88. /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
- }
@@ -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
- }