@fayz-ai/plugin-marketing 0.1.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/dist/MarketingContext.d.ts +35 -0
- package/dist/MarketingContext.d.ts.map +1 -0
- package/dist/MarketingPage.d.ts +11 -0
- package/dist/MarketingPage.d.ts.map +1 -0
- package/dist/components/MarketingBits.d.ts +29 -0
- package/dist/components/MarketingBits.d.ts.map +1 -0
- package/dist/components/icons.d.ts +6 -0
- package/dist/components/icons.d.ts.map +1 -0
- package/dist/data/mock.d.ts +10 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/types.d.ts +32 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/format.d.ts +17 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/index.cjs +1354 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1346 -0
- package/dist/index.js.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/presets.d.ts +20 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/store.d.ts +18 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/CampaignComposer.d.ts +6 -0
- package/dist/views/CampaignComposer.d.ts.map +1 -0
- package/dist/views/CampaignsView.d.ts +3 -0
- package/dist/views/CampaignsView.d.ts.map +1 -0
- package/dist/views/ChannelDetailView.d.ts +6 -0
- package/dist/views/ChannelDetailView.d.ts.map +1 -0
- package/dist/views/ChannelsView.d.ts +5 -0
- package/dist/views/ChannelsView.d.ts.map +1 -0
- package/dist/views/FunnelView.d.ts +3 -0
- package/dist/views/FunnelView.d.ts.map +1 -0
- package/dist/views/LandingPagesView.d.ts +3 -0
- package/dist/views/LandingPagesView.d.ts.map +1 -0
- package/dist/views/OverviewView.d.ts +9 -0
- package/dist/views/OverviewView.d.ts.map +1 -0
- package/dist/views/SettingsView.d.ts +3 -0
- package/dist/views/SettingsView.d.ts.map +1 -0
- package/dist/views/dashboardWidgets.d.ts +11 -0
- package/dist/views/dashboardWidgets.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/MarketingContext.tsx +39 -0
- package/src/MarketingPage.tsx +94 -0
- package/src/components/MarketingBits.tsx +131 -0
- package/src/components/icons.tsx +17 -0
- package/src/data/mock.ts +233 -0
- package/src/data/types.ts +55 -0
- package/src/format.ts +40 -0
- package/src/index.ts +225 -0
- package/src/locales/en.ts +74 -0
- package/src/locales/index.ts +7 -0
- package/src/locales/pt-BR.ts +74 -0
- package/src/presets.ts +115 -0
- package/src/store.ts +66 -0
- package/src/types.ts +114 -0
- package/src/views/CampaignComposer.tsx +89 -0
- package/src/views/CampaignsView.tsx +56 -0
- package/src/views/ChannelDetailView.tsx +59 -0
- package/src/views/ChannelsView.tsx +46 -0
- package/src/views/FunnelView.tsx +56 -0
- package/src/views/LandingPagesView.tsx +49 -0
- package/src/views/OverviewView.tsx +24 -0
- package/src/views/SettingsView.tsx +72 -0
- package/src/views/dashboardWidgets.tsx +173 -0
package/src/format.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface MarketingCurrency {
|
|
2
|
+
code: string
|
|
3
|
+
locale: string
|
|
4
|
+
symbol: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_CURRENCY: MarketingCurrency = { code: 'USD', locale: 'en-US', symbol: '$' }
|
|
8
|
+
|
|
9
|
+
export function formatCurrency(value: number, currency: MarketingCurrency): string {
|
|
10
|
+
try {
|
|
11
|
+
return new Intl.NumberFormat(currency.locale, {
|
|
12
|
+
style: 'currency',
|
|
13
|
+
currency: currency.code,
|
|
14
|
+
maximumFractionDigits: value >= 1000 ? 0 : 2,
|
|
15
|
+
}).format(value)
|
|
16
|
+
} catch {
|
|
17
|
+
return `${currency.symbol}${Math.round(value).toLocaleString()}`
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatNumber(value: number): string {
|
|
22
|
+
return Math.round(value).toLocaleString()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatPercent(value: number, digits = 1): string {
|
|
26
|
+
return `${value.toFixed(digits)}%`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Compact form for big reach numbers (12.4k). */
|
|
30
|
+
export function formatCompact(value: number): string {
|
|
31
|
+
if (value >= 1000) return `${(value / 1000).toFixed(value >= 10000 ? 0 : 1)}k`
|
|
32
|
+
return String(Math.round(value))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Percentage-point delta vs a previous value, signed. */
|
|
36
|
+
export function trendDelta(current: number, previous: number): { pct: number; dir: 'up' | 'down' | 'neutral' } {
|
|
37
|
+
if (previous === 0) return { pct: current > 0 ? 100 : 0, dir: current > 0 ? 'up' : 'neutral' }
|
|
38
|
+
const pct = ((current - previous) / previous) * 100
|
|
39
|
+
return { pct: Math.abs(pct), dir: pct > 0.5 ? 'up' : pct < -0.5 ? 'down' : 'neutral' }
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'
|
|
3
|
+
import { getSupabaseClientOptional, registerTranslations } from '@fayz-ai/core'
|
|
4
|
+
import { MarketingPage } from './MarketingPage'
|
|
5
|
+
import { SettingsView } from './views/SettingsView'
|
|
6
|
+
import { createMarketingDashboardWidgets } from './views/dashboardWidgets'
|
|
7
|
+
import {
|
|
8
|
+
MarketingContextProvider,
|
|
9
|
+
type ResolvedMarketingConfig,
|
|
10
|
+
type MarketingLabels,
|
|
11
|
+
} from './MarketingContext'
|
|
12
|
+
import type {
|
|
13
|
+
AttributionBridge,
|
|
14
|
+
MarketingDataProvider,
|
|
15
|
+
SitesPerformanceBridge,
|
|
16
|
+
} from './data/types'
|
|
17
|
+
import { createMockMarketingProvider } from './data/mock'
|
|
18
|
+
import { createMarketingStore } from './store'
|
|
19
|
+
import { DEFAULT_CURRENCY, type MarketingCurrency } from './format'
|
|
20
|
+
import { MARKETING_PRESETS, type MarketingDomain, type MarketingDomainModules } from './presets'
|
|
21
|
+
import type { AcquisitionChannel, ConversionModel } from './types'
|
|
22
|
+
import { marketingLocales } from './locales'
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// @fayz-ai/plugin-marketing — acquisition & conversion analytics. ONE plugin,
|
|
26
|
+
// every vertical: a `domain` preset (or explicit conversion + channels) adapts
|
|
27
|
+
// what a conversion is and where it's attributed from. Channels, campaigns,
|
|
28
|
+
// funnel and landing-page CVR are computed generically.
|
|
29
|
+
//
|
|
30
|
+
// Ships a vertical-flavored mock today; real attribution comes via the
|
|
31
|
+
// AttributionBridge / SitesPerformanceBridge seams (read from crm/agenda/
|
|
32
|
+
// orders/sites). Outbound broadcasts + journeys are reserved behind flags.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export interface MarketingPluginOptions {
|
|
36
|
+
/** Vertical preset for conversion model + channels + module defaults. */
|
|
37
|
+
domain?: MarketingDomain
|
|
38
|
+
/** Override the conversion model (else taken from the domain preset). */
|
|
39
|
+
conversion?: ConversionModel
|
|
40
|
+
/** Override the acquisition channels (else from the domain preset). */
|
|
41
|
+
channels?: AcquisitionChannel[]
|
|
42
|
+
modules?: Partial<MarketingDomainModules>
|
|
43
|
+
currency?: Partial<MarketingCurrency>
|
|
44
|
+
labels?: Partial<MarketingLabels>
|
|
45
|
+
navPosition?: number
|
|
46
|
+
navSection?: 'main' | 'secondary' | 'settings'
|
|
47
|
+
navLabel?: string
|
|
48
|
+
scope?: PluginScope
|
|
49
|
+
verticalId?: VerticalId
|
|
50
|
+
dataProvider?: MarketingDataProvider
|
|
51
|
+
/** Optional DI to read real conversions (mounted later). */
|
|
52
|
+
attributionBridge?: AttributionBridge
|
|
53
|
+
/** Optional DI to read real landing-page performance (mounted later). */
|
|
54
|
+
sitesBridge?: SitesPerformanceBridge
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DEFAULT_LABELS: MarketingLabels = {
|
|
58
|
+
pageTitle: 'Marketing',
|
|
59
|
+
pageSubtitle: 'Acquisition & conversion performance',
|
|
60
|
+
overview: 'Overview',
|
|
61
|
+
channels: 'Channels',
|
|
62
|
+
campaigns: 'Campaigns',
|
|
63
|
+
funnel: 'Funnel',
|
|
64
|
+
landingPages: 'Landing pages',
|
|
65
|
+
settings: 'Settings',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveConfig(options?: MarketingPluginOptions): { config: ResolvedMarketingConfig; domain: MarketingDomain } {
|
|
69
|
+
const domain = options?.domain ?? 'agency'
|
|
70
|
+
const preset = MARKETING_PRESETS[domain]
|
|
71
|
+
const config: ResolvedMarketingConfig = {
|
|
72
|
+
conversion: options?.conversion ?? preset.conversion,
|
|
73
|
+
channels: options?.channels ?? preset.channels,
|
|
74
|
+
modules: { ...preset.modules, ...options?.modules },
|
|
75
|
+
currency: { ...DEFAULT_CURRENCY, ...options?.currency },
|
|
76
|
+
labels: { ...DEFAULT_LABELS, ...options?.labels },
|
|
77
|
+
}
|
|
78
|
+
return { config, domain }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createSafeProvider(config: ResolvedMarketingConfig, domain: MarketingDomain): MarketingDataProvider {
|
|
82
|
+
// Supabase-backed + bridge-fed providers land later; mock powers it for now.
|
|
83
|
+
void getSupabaseClientOptional()
|
|
84
|
+
return createMockMarketingProvider({ channels: config.channels, conversion: config.conversion, domain })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createMarketingPlugin(options?: MarketingPluginOptions): PluginManifest {
|
|
88
|
+
registerTranslations(marketingLocales)
|
|
89
|
+
const { config, domain } = resolveConfig(options)
|
|
90
|
+
const provider = options?.dataProvider ?? createSafeProvider(config, domain)
|
|
91
|
+
const store = createMarketingStore(provider)
|
|
92
|
+
const dashboardWidgets = createMarketingDashboardWidgets({ config, provider, store })
|
|
93
|
+
|
|
94
|
+
const PageComponent: React.ComponentType<unknown> = () =>
|
|
95
|
+
React.createElement(MarketingContextProvider, { config, provider, store },
|
|
96
|
+
React.createElement(MarketingPage, { config, provider, store }))
|
|
97
|
+
PageComponent.displayName = 'MarketingPage'
|
|
98
|
+
|
|
99
|
+
// Settings lives in the SDK-core central Settings area (not a module tab).
|
|
100
|
+
const SettingsComponent: React.ComponentType<unknown> = () =>
|
|
101
|
+
React.createElement(MarketingContextProvider, { config, provider, store },
|
|
102
|
+
React.createElement(SettingsView))
|
|
103
|
+
SettingsComponent.displayName = 'MarketingSettings'
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
id: 'marketing',
|
|
107
|
+
name: options?.navLabel ?? config.labels.pageTitle,
|
|
108
|
+
icon: 'Megaphone',
|
|
109
|
+
version: '1.0.0',
|
|
110
|
+
scope: options?.scope ?? 'universal',
|
|
111
|
+
verticalId: options?.verticalId,
|
|
112
|
+
defaultEnabled: true,
|
|
113
|
+
dependencies: [],
|
|
114
|
+
declaredFeatures: [
|
|
115
|
+
{ id: 'marketing', label: config.labels.pageTitle, group: 'Engage' },
|
|
116
|
+
...(config.modules.landingPages ? [{ id: 'marketing.landing-pages', label: config.labels.landingPages, group: 'Engage' }] : []),
|
|
117
|
+
],
|
|
118
|
+
navigation: [
|
|
119
|
+
{
|
|
120
|
+
section: options?.navSection ?? 'main',
|
|
121
|
+
position: options?.navPosition ?? 5,
|
|
122
|
+
label: options?.navLabel ?? config.labels.pageTitle,
|
|
123
|
+
route: '/marketing',
|
|
124
|
+
icon: 'Megaphone',
|
|
125
|
+
permission: { feature: 'marketing', action: 'read' as const },
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
routes: [
|
|
129
|
+
{
|
|
130
|
+
path: '/marketing',
|
|
131
|
+
component: PageComponent,
|
|
132
|
+
permission: { feature: 'marketing', action: 'read' as const },
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
widgets: [],
|
|
136
|
+
dashboardWidgets,
|
|
137
|
+
events: [
|
|
138
|
+
{ name: 'marketing.conversion.tracked', description: 'A conversion was attributed to a channel/campaign' },
|
|
139
|
+
{ name: 'marketing.campaign.created', description: 'A campaign was created' },
|
|
140
|
+
{ name: 'marketing.campaign.updated', description: 'A campaign was updated' },
|
|
141
|
+
{ name: 'marketing.channel.synced', description: 'A channel pulled fresh attribution data' },
|
|
142
|
+
],
|
|
143
|
+
aiTools: [
|
|
144
|
+
{
|
|
145
|
+
id: 'marketing.channel-performance',
|
|
146
|
+
name: 'channelPerformance',
|
|
147
|
+
description: 'Returns acquisition performance per channel (reach, conversions, CVR, spend, CPA).',
|
|
148
|
+
icon: 'Radio',
|
|
149
|
+
mode: 'read' as const,
|
|
150
|
+
category: 'Marketing',
|
|
151
|
+
parameters: {
|
|
152
|
+
type: 'object' as const,
|
|
153
|
+
properties: { range: { type: 'string' as const, enum: ['7d', '30d', '90d'] } },
|
|
154
|
+
},
|
|
155
|
+
suggestions: [{ label: 'Which channel converts best?' }, { label: 'What is my cost per acquisition?' }],
|
|
156
|
+
permission: { feature: 'marketing', action: 'read' as const },
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'marketing.top-channels',
|
|
160
|
+
name: 'topChannels',
|
|
161
|
+
description: 'Returns the top acquisition channels ranked by conversions.',
|
|
162
|
+
icon: 'Trophy',
|
|
163
|
+
mode: 'read' as const,
|
|
164
|
+
category: 'Marketing',
|
|
165
|
+
permission: { feature: 'marketing', action: 'read' as const },
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'marketing.campaign-cvr',
|
|
169
|
+
name: 'campaignCvr',
|
|
170
|
+
description: 'Lists campaigns with their conversion rate for a date range.',
|
|
171
|
+
icon: 'Percent',
|
|
172
|
+
mode: 'read' as const,
|
|
173
|
+
category: 'Marketing',
|
|
174
|
+
parameters: {
|
|
175
|
+
type: 'object' as const,
|
|
176
|
+
properties: { range: { type: 'string' as const, enum: ['7d', '30d', '90d'] } },
|
|
177
|
+
},
|
|
178
|
+
permission: { feature: 'marketing', action: 'read' as const },
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: 'marketing.create-campaign',
|
|
182
|
+
name: 'createCampaign',
|
|
183
|
+
description: 'Creates an acquisition campaign on a channel.',
|
|
184
|
+
icon: 'Plus',
|
|
185
|
+
mode: 'persist' as const,
|
|
186
|
+
category: 'Marketing',
|
|
187
|
+
parameters: {
|
|
188
|
+
type: 'object' as const,
|
|
189
|
+
properties: {
|
|
190
|
+
name: { type: 'string' as const, description: 'Campaign name' },
|
|
191
|
+
channelId: { type: 'string' as const, description: 'Acquisition channel id' },
|
|
192
|
+
},
|
|
193
|
+
required: ['name', 'channelId'],
|
|
194
|
+
},
|
|
195
|
+
permission: { feature: 'marketing', action: 'create' as const },
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
settings: [
|
|
199
|
+
{
|
|
200
|
+
id: 'marketing',
|
|
201
|
+
label: config.labels.pageTitle,
|
|
202
|
+
icon: 'Megaphone',
|
|
203
|
+
component: SettingsComponent,
|
|
204
|
+
order: 20,
|
|
205
|
+
permission: { feature: 'marketing', action: 'read' as const },
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
locales: marketingLocales,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export type {
|
|
213
|
+
MarketingDataProvider,
|
|
214
|
+
AttributionBridge,
|
|
215
|
+
SitesPerformanceBridge,
|
|
216
|
+
} from './data/types'
|
|
217
|
+
export type {
|
|
218
|
+
AcquisitionChannel,
|
|
219
|
+
ConversionModel,
|
|
220
|
+
Campaign,
|
|
221
|
+
ChannelPerformance,
|
|
222
|
+
LandingPagePerf,
|
|
223
|
+
} from './types'
|
|
224
|
+
export { MARKETING_PRESETS, type MarketingDomain } from './presets'
|
|
225
|
+
export { createMockMarketingProvider } from './data/mock'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const en: Record<string, string> = {
|
|
2
|
+
// metrics
|
|
3
|
+
'marketing.metric.conversionRate': 'Conversion rate',
|
|
4
|
+
'marketing.metric.cpa': 'Cost per acquisition',
|
|
5
|
+
'marketing.metric.topChannel': 'Top channel',
|
|
6
|
+
'marketing.metric.spend': 'spend',
|
|
7
|
+
'marketing.metric.reach': 'Reach',
|
|
8
|
+
'marketing.metric.cvr': 'CVR',
|
|
9
|
+
'marketing.metric.totalVisits': 'Total visits',
|
|
10
|
+
'marketing.metric.pages': 'Pages',
|
|
11
|
+
'marketing.metric.avgConversion': 'Avg. conversion',
|
|
12
|
+
// columns
|
|
13
|
+
'marketing.col.campaign': 'Campaign',
|
|
14
|
+
'marketing.col.channel': 'Channel',
|
|
15
|
+
'marketing.col.reach': 'Reach',
|
|
16
|
+
'marketing.col.leads': 'Leads',
|
|
17
|
+
'marketing.col.cvr': 'CVR',
|
|
18
|
+
'marketing.col.spend': 'Spend',
|
|
19
|
+
'marketing.col.cpa': 'CPA',
|
|
20
|
+
'marketing.col.status': 'Status',
|
|
21
|
+
'marketing.col.visits': 'Visits',
|
|
22
|
+
'marketing.col.conversions': 'Conversions',
|
|
23
|
+
'marketing.col.type': 'Type',
|
|
24
|
+
'marketing.col.page': 'Page',
|
|
25
|
+
// overview
|
|
26
|
+
'marketing.overview.byChannelSuffix': 'by channel',
|
|
27
|
+
'marketing.overview.recent': 'Recent campaigns',
|
|
28
|
+
// channels
|
|
29
|
+
'marketing.channels.subtitle': 'Acquisition performance by channel',
|
|
30
|
+
'marketing.channels.campaignsOn': 'Campaigns on this channel',
|
|
31
|
+
'marketing.channels.noCampaigns': 'No campaigns yet on this channel.',
|
|
32
|
+
// campaigns
|
|
33
|
+
'marketing.campaigns.subtitle': 'All acquisition campaigns and their conversion performance',
|
|
34
|
+
'marketing.campaigns.new': 'New campaign',
|
|
35
|
+
'marketing.campaigns.empty': 'No campaigns yet — create your first one.',
|
|
36
|
+
'marketing.campaigns.delete': 'Delete campaign',
|
|
37
|
+
// composer
|
|
38
|
+
'marketing.composer.title': 'New campaign',
|
|
39
|
+
'marketing.composer.name': 'Campaign name',
|
|
40
|
+
'marketing.composer.namePlaceholder': 'e.g. Spring Promo — Paid Social',
|
|
41
|
+
'marketing.composer.channel': 'Channel',
|
|
42
|
+
'marketing.composer.channelPlaceholder': 'Select a channel',
|
|
43
|
+
'marketing.composer.status': 'Status',
|
|
44
|
+
'marketing.composer.budget': 'Budget / spend',
|
|
45
|
+
'marketing.composer.cancel': 'Cancel',
|
|
46
|
+
'marketing.composer.create': 'Create campaign',
|
|
47
|
+
'marketing.status.draft': 'draft',
|
|
48
|
+
'marketing.status.active': 'active',
|
|
49
|
+
'marketing.status.paused': 'paused',
|
|
50
|
+
'marketing.status.ended': 'ended',
|
|
51
|
+
// funnel
|
|
52
|
+
'marketing.funnel.subtitlePrefix': 'Attribution funnel — reach to',
|
|
53
|
+
'marketing.funnel.overall': 'Overall conversion rate',
|
|
54
|
+
'marketing.funnel.ofPrev': 'of prev',
|
|
55
|
+
// landing pages
|
|
56
|
+
'marketing.landing.subtitle': 'Per-page visits → conversions performance',
|
|
57
|
+
// settings
|
|
58
|
+
'marketing.settings.conversionModel': 'Conversion model',
|
|
59
|
+
'marketing.settings.conversionModelDesc': "What counts as a conversion in this workspace and where it's attributed from.",
|
|
60
|
+
'marketing.settings.conversion': 'Conversion',
|
|
61
|
+
'marketing.settings.valueMetric': 'Value metric',
|
|
62
|
+
'marketing.settings.attributedFrom': 'Attributed from',
|
|
63
|
+
'marketing.settings.channels': 'Acquisition channels',
|
|
64
|
+
'marketing.settings.channelsDesc': 'Sources tracked for this workspace.',
|
|
65
|
+
'marketing.settings.trackValue': 'Track monetary value',
|
|
66
|
+
'marketing.settings.trackValueDesc': 'Attribute revenue to channels and campaigns.',
|
|
67
|
+
'marketing.settings.attribution': 'Attribution',
|
|
68
|
+
'marketing.settings.attributionDesc': 'How conversions are matched to channels.',
|
|
69
|
+
'marketing.settings.autoSync': 'Auto-sync conversions',
|
|
70
|
+
'marketing.settings.autoSyncDesc': 'Pull conversions automatically from the source.',
|
|
71
|
+
'marketing.settings.assisted': 'Count assisted conversions',
|
|
72
|
+
'marketing.settings.assistedDesc': 'Credit channels that contributed to a conversion.',
|
|
73
|
+
'marketing.settings.channelTrackDesc': 'Track this acquisition source.',
|
|
74
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const ptBR: Record<string, string> = {
|
|
2
|
+
// metrics
|
|
3
|
+
'marketing.metric.conversionRate': 'Taxa de conversão',
|
|
4
|
+
'marketing.metric.cpa': 'Custo por aquisição',
|
|
5
|
+
'marketing.metric.topChannel': 'Principal canal',
|
|
6
|
+
'marketing.metric.spend': 'investido',
|
|
7
|
+
'marketing.metric.reach': 'Alcance',
|
|
8
|
+
'marketing.metric.cvr': 'CVR',
|
|
9
|
+
'marketing.metric.totalVisits': 'Total de visitas',
|
|
10
|
+
'marketing.metric.pages': 'Páginas',
|
|
11
|
+
'marketing.metric.avgConversion': 'Conversão média',
|
|
12
|
+
// columns
|
|
13
|
+
'marketing.col.campaign': 'Campanha',
|
|
14
|
+
'marketing.col.channel': 'Canal',
|
|
15
|
+
'marketing.col.reach': 'Alcance',
|
|
16
|
+
'marketing.col.leads': 'Leads',
|
|
17
|
+
'marketing.col.cvr': 'CVR',
|
|
18
|
+
'marketing.col.spend': 'Investimento',
|
|
19
|
+
'marketing.col.cpa': 'CPA',
|
|
20
|
+
'marketing.col.status': 'Status',
|
|
21
|
+
'marketing.col.visits': 'Visitas',
|
|
22
|
+
'marketing.col.conversions': 'Conversões',
|
|
23
|
+
'marketing.col.type': 'Tipo',
|
|
24
|
+
'marketing.col.page': 'Página',
|
|
25
|
+
// overview
|
|
26
|
+
'marketing.overview.byChannelSuffix': 'por canal',
|
|
27
|
+
'marketing.overview.recent': 'Campanhas recentes',
|
|
28
|
+
// channels
|
|
29
|
+
'marketing.channels.subtitle': 'Desempenho de aquisição por canal',
|
|
30
|
+
'marketing.channels.campaignsOn': 'Campanhas neste canal',
|
|
31
|
+
'marketing.channels.noCampaigns': 'Nenhuma campanha ainda neste canal.',
|
|
32
|
+
// campaigns
|
|
33
|
+
'marketing.campaigns.subtitle': 'Todas as campanhas de aquisição e seu desempenho de conversão',
|
|
34
|
+
'marketing.campaigns.new': 'Nova campanha',
|
|
35
|
+
'marketing.campaigns.empty': 'Nenhuma campanha ainda — crie a primeira.',
|
|
36
|
+
'marketing.campaigns.delete': 'Excluir campanha',
|
|
37
|
+
// composer
|
|
38
|
+
'marketing.composer.title': 'Nova campanha',
|
|
39
|
+
'marketing.composer.name': 'Nome da campanha',
|
|
40
|
+
'marketing.composer.namePlaceholder': 'ex.: Promo de Verão — Mídia Paga',
|
|
41
|
+
'marketing.composer.channel': 'Canal',
|
|
42
|
+
'marketing.composer.channelPlaceholder': 'Selecione um canal',
|
|
43
|
+
'marketing.composer.status': 'Status',
|
|
44
|
+
'marketing.composer.budget': 'Orçamento / investimento',
|
|
45
|
+
'marketing.composer.cancel': 'Cancelar',
|
|
46
|
+
'marketing.composer.create': 'Criar campanha',
|
|
47
|
+
'marketing.status.draft': 'rascunho',
|
|
48
|
+
'marketing.status.active': 'ativa',
|
|
49
|
+
'marketing.status.paused': 'pausada',
|
|
50
|
+
'marketing.status.ended': 'encerrada',
|
|
51
|
+
// funnel
|
|
52
|
+
'marketing.funnel.subtitlePrefix': 'Funil de atribuição — alcance até',
|
|
53
|
+
'marketing.funnel.overall': 'Taxa de conversão geral',
|
|
54
|
+
'marketing.funnel.ofPrev': 'do anterior',
|
|
55
|
+
// landing pages
|
|
56
|
+
'marketing.landing.subtitle': 'Desempenho de visitas → conversões por página',
|
|
57
|
+
// settings
|
|
58
|
+
'marketing.settings.conversionModel': 'Modelo de conversão',
|
|
59
|
+
'marketing.settings.conversionModelDesc': 'O que conta como conversão neste workspace e de onde é atribuída.',
|
|
60
|
+
'marketing.settings.conversion': 'Conversão',
|
|
61
|
+
'marketing.settings.valueMetric': 'Métrica de valor',
|
|
62
|
+
'marketing.settings.attributedFrom': 'Atribuída de',
|
|
63
|
+
'marketing.settings.channels': 'Canais de aquisição',
|
|
64
|
+
'marketing.settings.channelsDesc': 'Fontes monitoradas neste workspace.',
|
|
65
|
+
'marketing.settings.trackValue': 'Acompanhar valor monetário',
|
|
66
|
+
'marketing.settings.trackValueDesc': 'Atribuir receita a canais e campanhas.',
|
|
67
|
+
'marketing.settings.attribution': 'Atribuição',
|
|
68
|
+
'marketing.settings.attributionDesc': 'Como as conversões são associadas aos canais.',
|
|
69
|
+
'marketing.settings.autoSync': 'Sincronizar conversões automaticamente',
|
|
70
|
+
'marketing.settings.autoSyncDesc': 'Importar conversões automaticamente da fonte.',
|
|
71
|
+
'marketing.settings.assisted': 'Contar conversões assistidas',
|
|
72
|
+
'marketing.settings.assistedDesc': 'Creditar canais que contribuíram para uma conversão.',
|
|
73
|
+
'marketing.settings.channelTrackDesc': 'Monitorar esta fonte de aquisição.',
|
|
74
|
+
}
|
package/src/presets.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
/** Outbound — reserved, off by default, implemented in a later milestone. */
|
|
18
|
+
broadcasts: boolean
|
|
19
|
+
journeys: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MarketingDomainPreset {
|
|
23
|
+
conversion: ConversionModel
|
|
24
|
+
channels: AcquisitionChannel[]
|
|
25
|
+
modules: MarketingDomainModules
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const AGENCY: MarketingDomainPreset = {
|
|
29
|
+
conversion: {
|
|
30
|
+
id: 'won-deal',
|
|
31
|
+
label: 'Won deal',
|
|
32
|
+
labelPlural: 'Won deals',
|
|
33
|
+
valueLabel: 'Pipeline value',
|
|
34
|
+
source: 'crm',
|
|
35
|
+
avgValue: 3200,
|
|
36
|
+
},
|
|
37
|
+
channels: [
|
|
38
|
+
{ id: 'paid-search', label: 'Paid Search', icon: 'Search', kind: 'paid' },
|
|
39
|
+
{ id: 'paid-social', label: 'Paid Social', icon: 'Megaphone', kind: 'paid' },
|
|
40
|
+
{ id: 'organic', label: 'Organic / SEO', icon: 'Globe', kind: 'organic' },
|
|
41
|
+
{ id: 'email', label: 'Email', icon: 'Mail', kind: 'outbound' },
|
|
42
|
+
{ id: 'referral', label: 'Referral', icon: 'Users', kind: 'referral' },
|
|
43
|
+
{ id: 'direct', label: 'Direct', icon: 'MousePointerClick', kind: 'direct' },
|
|
44
|
+
],
|
|
45
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, broadcasts: false, journeys: false },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const BEAUTY: MarketingDomainPreset = {
|
|
49
|
+
conversion: {
|
|
50
|
+
id: 'booking',
|
|
51
|
+
label: 'Booking',
|
|
52
|
+
labelPlural: 'Bookings',
|
|
53
|
+
valueLabel: 'Booking revenue',
|
|
54
|
+
source: 'agenda',
|
|
55
|
+
avgValue: 85,
|
|
56
|
+
},
|
|
57
|
+
channels: [
|
|
58
|
+
{ id: 'instagram', label: 'Instagram', icon: 'Instagram', kind: 'social' },
|
|
59
|
+
{ id: 'google', label: 'Google', icon: 'Globe', kind: 'organic' },
|
|
60
|
+
{ id: 'referral', label: 'Referral', icon: 'Users', kind: 'referral' },
|
|
61
|
+
{ id: 'whatsapp', label: 'WhatsApp', icon: 'MessageCircle', kind: 'social' },
|
|
62
|
+
{ id: 'walkin', label: 'Walk-in', icon: 'DoorOpen', kind: 'direct' },
|
|
63
|
+
],
|
|
64
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, broadcasts: false, journeys: false },
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const RESTO: MarketingDomainPreset = {
|
|
68
|
+
conversion: {
|
|
69
|
+
id: 'order',
|
|
70
|
+
label: 'Order',
|
|
71
|
+
labelPlural: 'Orders',
|
|
72
|
+
valueLabel: 'Revenue',
|
|
73
|
+
source: 'orders',
|
|
74
|
+
avgValue: 42,
|
|
75
|
+
},
|
|
76
|
+
channels: [
|
|
77
|
+
{ id: 'ifood', label: 'iFood', icon: 'Utensils', kind: 'paid' },
|
|
78
|
+
{ id: 'delivery', label: 'Delivery apps', icon: 'Bike', kind: 'paid' },
|
|
79
|
+
{ id: 'google', label: 'Google', icon: 'Globe', kind: 'organic' },
|
|
80
|
+
{ id: 'instagram', label: 'Instagram', icon: 'Instagram', kind: 'social' },
|
|
81
|
+
{ id: 'walkin', label: 'Walk-in', icon: 'DoorOpen', kind: 'direct' },
|
|
82
|
+
],
|
|
83
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, broadcasts: false, journeys: false },
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const MARKETING_PRESETS: Record<MarketingDomain, MarketingDomainPreset> = {
|
|
87
|
+
agency: AGENCY,
|
|
88
|
+
beauty: BEAUTY,
|
|
89
|
+
resto: RESTO,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Vertical-flavored campaign name templates for the mock seed. */
|
|
93
|
+
export const CAMPAIGN_NAME_SEEDS: Record<MarketingDomain, string[]> = {
|
|
94
|
+
agency: [
|
|
95
|
+
'Q2 Lead-gen — Search',
|
|
96
|
+
'LinkedIn Retargeting',
|
|
97
|
+
'Webinar Funnel',
|
|
98
|
+
'Newsletter Nurture',
|
|
99
|
+
'Partner Referral Push',
|
|
100
|
+
],
|
|
101
|
+
beauty: [
|
|
102
|
+
'Summer Glow Promo',
|
|
103
|
+
'New Client — Instagram',
|
|
104
|
+
'Birthday Offer Blast',
|
|
105
|
+
'Win-back 90 days',
|
|
106
|
+
'Google Reviews Boost',
|
|
107
|
+
],
|
|
108
|
+
resto: [
|
|
109
|
+
'iFood Weekend Combo',
|
|
110
|
+
'Happy Hour Promo',
|
|
111
|
+
'New Menu Launch',
|
|
112
|
+
'Loyalty Double Points',
|
|
113
|
+
'Instagram Stories Drop',
|
|
114
|
+
],
|
|
115
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createStore, type StoreApi } from 'zustand/vanilla'
|
|
2
|
+
import type { MarketingDataProvider, SaveCampaignInput } from './data/types'
|
|
3
|
+
import type {
|
|
4
|
+
Campaign,
|
|
5
|
+
ChannelPerformance,
|
|
6
|
+
DateRangeKey,
|
|
7
|
+
FunnelStage,
|
|
8
|
+
LandingPagePerf,
|
|
9
|
+
MarketingOverview,
|
|
10
|
+
} from './types'
|
|
11
|
+
|
|
12
|
+
export interface MarketingUIState {
|
|
13
|
+
range: DateRangeKey
|
|
14
|
+
overview: MarketingOverview | null
|
|
15
|
+
channels: ChannelPerformance[]
|
|
16
|
+
campaigns: Campaign[]
|
|
17
|
+
funnel: FunnelStage[]
|
|
18
|
+
landingPages: LandingPagePerf[]
|
|
19
|
+
loading: boolean
|
|
20
|
+
|
|
21
|
+
load(): Promise<void>
|
|
22
|
+
setRange(range: DateRangeKey): Promise<void>
|
|
23
|
+
saveCampaign(input: SaveCampaignInput): Promise<Campaign>
|
|
24
|
+
deleteCampaign(id: string): Promise<void>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createMarketingStore(provider: MarketingDataProvider): StoreApi<MarketingUIState> {
|
|
28
|
+
return createStore<MarketingUIState>((set, get) => ({
|
|
29
|
+
range: '30d',
|
|
30
|
+
overview: null,
|
|
31
|
+
channels: [],
|
|
32
|
+
campaigns: [],
|
|
33
|
+
funnel: [],
|
|
34
|
+
landingPages: [],
|
|
35
|
+
loading: false,
|
|
36
|
+
|
|
37
|
+
async load() {
|
|
38
|
+
const range = get().range
|
|
39
|
+
set({ loading: true })
|
|
40
|
+
const [overview, channels, campaigns, funnel, landingPages] = await Promise.all([
|
|
41
|
+
provider.overview({ range }),
|
|
42
|
+
provider.channelPerformance({ range }),
|
|
43
|
+
provider.listCampaigns({ range }),
|
|
44
|
+
provider.funnel({ range }),
|
|
45
|
+
provider.landingPages({ range }),
|
|
46
|
+
])
|
|
47
|
+
set({ overview, channels, campaigns, funnel, landingPages, loading: false })
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async setRange(range) {
|
|
51
|
+
set({ range })
|
|
52
|
+
await get().load()
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async saveCampaign(input) {
|
|
56
|
+
const created = await provider.saveCampaign(input)
|
|
57
|
+
await get().load()
|
|
58
|
+
return created
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
async deleteCampaign(id) {
|
|
62
|
+
await provider.deleteCampaign(id)
|
|
63
|
+
await get().load()
|
|
64
|
+
},
|
|
65
|
+
}))
|
|
66
|
+
}
|