@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.
- 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/data/contentTypes.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Content planner — domain types + provider seam. Deliberately separate from
|
|
3
|
-
// MarketingDataProvider: analytics is mock-until-bridges, while the planner is
|
|
4
|
-
// a real CRUD surface (Supabase-backed via createSafeDataProvider). The
|
|
5
|
-
// interface must stay methods-only (the safe-provider Proxy turns every
|
|
6
|
-
// property access into a call).
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
export type PostFormat = 'reel' | 'static' | 'carousel' | 'story' | 'video' | 'live' | 'article'
|
|
10
|
-
|
|
11
|
-
/** Platforms an account can publish to. UI-driven list (the DB stores plain
|
|
12
|
-
* text[]), so growing it is a one-line change here. */
|
|
13
|
-
export const PLATFORMS = [
|
|
14
|
-
'instagram',
|
|
15
|
-
'tiktok',
|
|
16
|
-
'youtube',
|
|
17
|
-
'facebook',
|
|
18
|
-
'linkedin',
|
|
19
|
-
'x',
|
|
20
|
-
'pinterest',
|
|
21
|
-
'threads',
|
|
22
|
-
] as const
|
|
23
|
-
export type Platform = (typeof PLATFORMS)[number]
|
|
24
|
-
|
|
25
|
-
export type PostStatus = 'idea' | 'script' | 'recording' | 'editing' | 'scheduled' | 'published'
|
|
26
|
-
|
|
27
|
-
export const POST_STATUS_ORDER: PostStatus[] = ['idea', 'script', 'recording', 'editing', 'scheduled', 'published']
|
|
28
|
-
|
|
29
|
-
export interface SocialAccount {
|
|
30
|
-
id: string
|
|
31
|
-
name: string
|
|
32
|
-
handle?: string
|
|
33
|
-
/** Platforms this account/brand publishes to (one account, many connections). */
|
|
34
|
-
platforms: string[]
|
|
35
|
-
isActive: boolean
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface ContentPlan {
|
|
39
|
-
id: string
|
|
40
|
-
accountId: string
|
|
41
|
-
name: string
|
|
42
|
-
status: 'draft' | 'active' | 'archived'
|
|
43
|
-
weeksCount: number
|
|
44
|
-
startDate?: string
|
|
45
|
-
/** Structured "what we want" config — feeds chips + future AI drafting. */
|
|
46
|
-
objective?: string
|
|
47
|
-
tone?: string
|
|
48
|
-
pillars: string[]
|
|
49
|
-
formats: PostFormat[]
|
|
50
|
-
weeklyFrequency: number
|
|
51
|
-
/** Free-form brief/strategy document (markdown). */
|
|
52
|
-
briefMd: string
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Recording-day checklist item (on-set companion). */
|
|
56
|
-
export interface ChecklistItem {
|
|
57
|
-
id: string
|
|
58
|
-
text: string
|
|
59
|
-
done: boolean
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface ContentPost {
|
|
63
|
-
id: string
|
|
64
|
-
planId: string
|
|
65
|
-
weekNumber: number
|
|
66
|
-
position: number
|
|
67
|
-
title: string
|
|
68
|
-
format: PostFormat
|
|
69
|
-
status: PostStatus
|
|
70
|
-
scheduledDate?: string
|
|
71
|
-
/** Target platforms for this post; empty = inherit the account's platforms. */
|
|
72
|
-
platforms: string[]
|
|
73
|
-
/** Recording-day checklist — persisted as the crew checks items off. */
|
|
74
|
-
checklist: ChecklistItem[]
|
|
75
|
-
/** Uploaded asset URL (static posts: caption + final art instead of a script). */
|
|
76
|
-
mediaUrl?: string
|
|
77
|
-
hook?: string
|
|
78
|
-
cta?: string
|
|
79
|
-
/** The Notion-style page body — the script/plan in markdown. */
|
|
80
|
-
contentMd: string
|
|
81
|
-
updatedAt?: string
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export interface SaveSocialAccountInput {
|
|
85
|
-
id?: string
|
|
86
|
-
name: string
|
|
87
|
-
handle?: string
|
|
88
|
-
platforms?: string[]
|
|
89
|
-
isActive?: boolean
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface SaveContentPlanInput {
|
|
93
|
-
id?: string
|
|
94
|
-
accountId?: string
|
|
95
|
-
name?: string
|
|
96
|
-
status?: ContentPlan['status']
|
|
97
|
-
weeksCount?: number
|
|
98
|
-
startDate?: string | null
|
|
99
|
-
objective?: string
|
|
100
|
-
tone?: string
|
|
101
|
-
pillars?: string[]
|
|
102
|
-
formats?: PostFormat[]
|
|
103
|
-
weeklyFrequency?: number
|
|
104
|
-
briefMd?: string
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export interface SaveContentPostInput {
|
|
108
|
-
id?: string
|
|
109
|
-
planId?: string
|
|
110
|
-
weekNumber?: number
|
|
111
|
-
position?: number
|
|
112
|
-
title?: string
|
|
113
|
-
format?: PostFormat
|
|
114
|
-
status?: PostStatus
|
|
115
|
-
scheduledDate?: string | null
|
|
116
|
-
platforms?: string[]
|
|
117
|
-
checklist?: ChecklistItem[]
|
|
118
|
-
mediaUrl?: string | null
|
|
119
|
-
hook?: string
|
|
120
|
-
cta?: string
|
|
121
|
-
contentMd?: string
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export interface ContentPlannerProvider {
|
|
125
|
-
listAccounts(): Promise<SocialAccount[]>
|
|
126
|
-
saveAccount(input: SaveSocialAccountInput): Promise<SocialAccount>
|
|
127
|
-
/** Hard delete — plans and posts cascade with the account. */
|
|
128
|
-
deleteAccount(id: string): Promise<void>
|
|
129
|
-
listPlans(accountId: string): Promise<ContentPlan[]>
|
|
130
|
-
getPlan(id: string): Promise<ContentPlan | null>
|
|
131
|
-
savePlan(input: SaveContentPlanInput): Promise<ContentPlan>
|
|
132
|
-
deletePlan(id: string): Promise<void>
|
|
133
|
-
/** Ordered by week_number then position. */
|
|
134
|
-
listPosts(planId: string): Promise<ContentPost[]>
|
|
135
|
-
getPost(id: string): Promise<ContentPost | null>
|
|
136
|
-
savePost(input: SaveContentPostInput): Promise<ContentPost>
|
|
137
|
-
deletePost(id: string): Promise<void>
|
|
138
|
-
}
|
package/src/data/mock.ts
DELETED
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
import type { MarketingDataProvider, SaveCampaignInput, SaveLandingPageInput } from './types'
|
|
2
|
-
import type {
|
|
3
|
-
AcquisitionChannel,
|
|
4
|
-
AcquisitionChannelKind,
|
|
5
|
-
Campaign,
|
|
6
|
-
ChannelPerformance,
|
|
7
|
-
ConversionModel,
|
|
8
|
-
DateRangeKey,
|
|
9
|
-
FunnelStage,
|
|
10
|
-
LandingPage,
|
|
11
|
-
LandingPagePerf,
|
|
12
|
-
MarketingOverview,
|
|
13
|
-
MarketingQuery,
|
|
14
|
-
MarketingSite,
|
|
15
|
-
} from '../types'
|
|
16
|
-
import { CAMPAIGN_NAME_SEEDS, type MarketingDomain } from '../presets'
|
|
17
|
-
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// Deterministic seed knobs (no Date.now at module init for stable analytics).
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
const RANGE_FACTOR: Record<DateRangeKey, number> = { '7d': 0.25, '30d': 1, '90d': 3.05 }
|
|
23
|
-
const BASE_REACH = [4200, 3100, 2600, 1900, 1500, 1200, 1000]
|
|
24
|
-
|
|
25
|
-
const KIND_LEAD_RATE: Record<AcquisitionChannelKind, number> = {
|
|
26
|
-
paid: 0.18, social: 0.22, organic: 0.14, referral: 0.3, direct: 0.1, outbound: 0.16,
|
|
27
|
-
}
|
|
28
|
-
const KIND_CONV_RATE: Record<AcquisitionChannelKind, number> = {
|
|
29
|
-
paid: 0.28, social: 0.25, organic: 0.32, referral: 0.45, direct: 0.22, outbound: 0.24,
|
|
30
|
-
}
|
|
31
|
-
const KIND_SPEND30: Record<AcquisitionChannelKind, number> = {
|
|
32
|
-
paid: 2800, social: 900, outbound: 600, organic: 0, referral: 0, direct: 0,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface ChannelBase {
|
|
36
|
-
channelId: string
|
|
37
|
-
kind: AcquisitionChannelKind
|
|
38
|
-
reach: number
|
|
39
|
-
leads: number
|
|
40
|
-
conversions: number
|
|
41
|
-
spend: number
|
|
42
|
-
value: number
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface MockMarketingConfig {
|
|
46
|
-
channels: AcquisitionChannel[]
|
|
47
|
-
conversion: ConversionModel
|
|
48
|
-
domain: MarketingDomain
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function channelBase30(channels: AcquisitionChannel[], conversion: ConversionModel): ChannelBase[] {
|
|
52
|
-
return channels.map((ch, i) => {
|
|
53
|
-
const reach = BASE_REACH[i % BASE_REACH.length]
|
|
54
|
-
const leads = Math.round(reach * KIND_LEAD_RATE[ch.kind])
|
|
55
|
-
const conversions = Math.round(leads * KIND_CONV_RATE[ch.kind])
|
|
56
|
-
const spend = KIND_SPEND30[ch.kind]
|
|
57
|
-
const value = Math.round(conversions * conversion.avgValue)
|
|
58
|
-
return { channelId: ch.id, kind: ch.kind, reach, leads, conversions, spend, value }
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function scale(n: number, range: DateRangeKey): number {
|
|
63
|
-
return Math.round(n * RANGE_FACTOR[range])
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function toPerformance(base: ChannelBase, range: DateRangeKey): ChannelPerformance {
|
|
67
|
-
const reach = scale(base.reach, range)
|
|
68
|
-
const leads = scale(base.leads, range)
|
|
69
|
-
const conversions = scale(base.conversions, range)
|
|
70
|
-
const spend = scale(base.spend, range)
|
|
71
|
-
const value = scale(base.value, range)
|
|
72
|
-
return {
|
|
73
|
-
channelId: base.channelId,
|
|
74
|
-
reach, leads, conversions, spend, value,
|
|
75
|
-
cvr: reach > 0 ? (conversions / reach) * 100 : 0,
|
|
76
|
-
cpa: conversions > 0 && spend > 0 ? spend / conversions : 0,
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function seedCampaigns(cfg: MockMarketingConfig): Campaign[] {
|
|
81
|
-
const names = CAMPAIGN_NAME_SEEDS[cfg.domain]
|
|
82
|
-
const base = channelBase30(cfg.channels, cfg.conversion)
|
|
83
|
-
const statuses: Campaign['status'][] = ['active', 'active', 'paused', 'ended', 'draft']
|
|
84
|
-
const startDays = [12, 26, 40, 8, 3]
|
|
85
|
-
const baseDate = new Date('2026-06-16T00:00:00Z').getTime()
|
|
86
|
-
|
|
87
|
-
return names.map((name, i) => {
|
|
88
|
-
const ch = cfg.channels[i % cfg.channels.length]
|
|
89
|
-
const b = base[i % base.length]
|
|
90
|
-
const status = statuses[i % statuses.length]
|
|
91
|
-
const draft = status === 'draft'
|
|
92
|
-
const start = new Date(baseDate - startDays[i % startDays.length] * 86_400_000).toISOString()
|
|
93
|
-
// Campaigns carry a slice of their channel's volume so totals stay plausible.
|
|
94
|
-
const share = 0.55 + (i % 3) * 0.12
|
|
95
|
-
return {
|
|
96
|
-
id: `cmp-${i + 1}`,
|
|
97
|
-
name,
|
|
98
|
-
channelId: ch.id,
|
|
99
|
-
status,
|
|
100
|
-
start,
|
|
101
|
-
end: status === 'ended' ? new Date(baseDate - 2 * 86_400_000).toISOString() : undefined,
|
|
102
|
-
spend: draft ? 0 : Math.round(b.spend * share),
|
|
103
|
-
reach: draft ? 0 : Math.round(b.reach * share),
|
|
104
|
-
leads: draft ? 0 : Math.round(b.leads * share),
|
|
105
|
-
conversions: draft ? 0 : Math.round(b.conversions * share),
|
|
106
|
-
value: draft ? 0 : Math.round(b.value * share),
|
|
107
|
-
}
|
|
108
|
-
})
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function scaleCampaign(c: Campaign, range: DateRangeKey): Campaign {
|
|
112
|
-
if (c.status === 'draft') return c
|
|
113
|
-
return {
|
|
114
|
-
...c,
|
|
115
|
-
spend: scale(c.spend, range),
|
|
116
|
-
reach: scale(c.reach, range),
|
|
117
|
-
leads: scale(c.leads, range),
|
|
118
|
-
conversions: scale(c.conversions, range),
|
|
119
|
-
value: scale(c.value, range),
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const MOCK_SITE: MarketingSite = {
|
|
124
|
-
id: 'site-1',
|
|
125
|
-
name: 'Site principal',
|
|
126
|
-
url: 'https://exemplo.com.br',
|
|
127
|
-
appId: 'app-demo',
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const MOCK_LEAD_TAGS = [
|
|
131
|
-
{ slug: 'lp-consulta', name: 'LP Consulta' },
|
|
132
|
-
{ slug: 'lp-promo', name: 'LP Promo' },
|
|
133
|
-
]
|
|
134
|
-
|
|
135
|
-
interface MockLandingPage extends LandingPage {
|
|
136
|
-
/** 30d baseline; scaled per range at read time. */
|
|
137
|
-
visits: number
|
|
138
|
-
conversions: number
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function seedLandingPages(): MockLandingPage[] {
|
|
142
|
-
return [
|
|
143
|
-
{ id: 'lp-1', siteId: MOCK_SITE.id, name: 'Consulta gratuita', route: '/consulta', leadTags: ['lp-consulta'], status: 'live', isActive: true, visits: 3820, conversions: 474 },
|
|
144
|
-
{ id: 'lp-2', siteId: MOCK_SITE.id, name: 'Promo da estação', route: '/promo', leadTags: ['lp-promo'], status: 'live', isActive: true, visits: 1560, conversions: 126 },
|
|
145
|
-
]
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function normalizeRoute(route: string): string {
|
|
149
|
-
const trimmed = route.trim().replace(/\/+$/, '')
|
|
150
|
-
if (!trimmed || trimmed === '/') return '/'
|
|
151
|
-
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function createMockMarketingProvider(cfg: MockMarketingConfig): MarketingDataProvider {
|
|
155
|
-
const base = channelBase30(cfg.channels, cfg.conversion)
|
|
156
|
-
const campaigns = seedCampaigns(cfg)
|
|
157
|
-
const landing = seedLandingPages()
|
|
158
|
-
let counter = 100
|
|
159
|
-
|
|
160
|
-
async function channelPerformance(query: MarketingQuery): Promise<ChannelPerformance[]> {
|
|
161
|
-
const list = base.map((b) => toPerformance(b, query.range))
|
|
162
|
-
return query.channelId ? list.filter((c) => c.channelId === query.channelId) : list
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
channelPerformance,
|
|
167
|
-
|
|
168
|
-
async overview(query: MarketingQuery): Promise<MarketingOverview> {
|
|
169
|
-
const perf = await channelPerformance(query)
|
|
170
|
-
const reach = perf.reduce((s, c) => s + c.reach, 0)
|
|
171
|
-
const conversions = perf.reduce((s, c) => s + c.conversions, 0)
|
|
172
|
-
const spend = perf.reduce((s, c) => s + c.spend, 0)
|
|
173
|
-
const value = perf.reduce((s, c) => s + c.value, 0)
|
|
174
|
-
const cvr = reach > 0 ? (conversions / reach) * 100 : 0
|
|
175
|
-
const top = [...perf].sort((a, b) => b.conversions - a.conversions)[0]
|
|
176
|
-
return {
|
|
177
|
-
conversions,
|
|
178
|
-
conversionsPrev: Math.round(conversions * 0.86),
|
|
179
|
-
cvr,
|
|
180
|
-
cvrPrev: cvr * 0.94,
|
|
181
|
-
spend,
|
|
182
|
-
cpa: conversions > 0 && spend > 0 ? spend / conversions : 0,
|
|
183
|
-
value,
|
|
184
|
-
topChannelId: top?.channelId ?? null,
|
|
185
|
-
channelMix: perf.map((c) => ({ channelId: c.channelId, conversions: c.conversions })),
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
|
|
189
|
-
async funnel(query: MarketingQuery): Promise<FunnelStage[]> {
|
|
190
|
-
const perf = await channelPerformance(query)
|
|
191
|
-
const reach = perf.reduce((s, c) => s + c.reach, 0)
|
|
192
|
-
const leads = perf.reduce((s, c) => s + c.leads, 0)
|
|
193
|
-
const conversions = perf.reduce((s, c) => s + c.conversions, 0)
|
|
194
|
-
const qualified = Math.round(leads * 0.62)
|
|
195
|
-
return [
|
|
196
|
-
{ id: 'reach', label: 'Reach / visits', count: reach, color: '#6366f1' },
|
|
197
|
-
{ id: 'leads', label: 'Leads', count: leads, color: '#0ea5e9' },
|
|
198
|
-
{ id: 'qualified', label: 'Qualified', count: qualified, color: '#14b8a6' },
|
|
199
|
-
{ id: 'converted', label: cfg.conversion.labelPlural, count: conversions, color: '#22c55e' },
|
|
200
|
-
]
|
|
201
|
-
},
|
|
202
|
-
|
|
203
|
-
async listCampaigns(query: MarketingQuery): Promise<Campaign[]> {
|
|
204
|
-
const list = query.channelId ? campaigns.filter((c) => c.channelId === query.channelId) : campaigns
|
|
205
|
-
return list.map((c) => scaleCampaign(c, query.range))
|
|
206
|
-
},
|
|
207
|
-
|
|
208
|
-
async getCampaign(id: string): Promise<Campaign | null> {
|
|
209
|
-
return campaigns.find((c) => c.id === id) ?? null
|
|
210
|
-
},
|
|
211
|
-
|
|
212
|
-
async saveCampaign(input: SaveCampaignInput): Promise<Campaign> {
|
|
213
|
-
if (input.id) {
|
|
214
|
-
const existing = campaigns.find((c) => c.id === input.id)
|
|
215
|
-
if (existing) {
|
|
216
|
-
Object.assign(existing, {
|
|
217
|
-
name: input.name, channelId: input.channelId, status: input.status,
|
|
218
|
-
start: input.start, end: input.end, spend: input.spend,
|
|
219
|
-
})
|
|
220
|
-
return existing
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
const b = base.find((x) => x.channelId === input.channelId) ?? base[0]
|
|
224
|
-
const active = input.status === 'active'
|
|
225
|
-
const created: Campaign = {
|
|
226
|
-
id: `cmp-${++counter}`,
|
|
227
|
-
name: input.name,
|
|
228
|
-
channelId: input.channelId,
|
|
229
|
-
status: input.status,
|
|
230
|
-
start: input.start,
|
|
231
|
-
end: input.end,
|
|
232
|
-
spend: input.spend,
|
|
233
|
-
// New campaigns ramp from a small slice of their channel's volume.
|
|
234
|
-
reach: active ? Math.round(b.reach * 0.15) : 0,
|
|
235
|
-
leads: active ? Math.round(b.leads * 0.15) : 0,
|
|
236
|
-
conversions: active ? Math.round(b.conversions * 0.15) : 0,
|
|
237
|
-
value: active ? Math.round(b.value * 0.15) : 0,
|
|
238
|
-
}
|
|
239
|
-
campaigns.unshift(created)
|
|
240
|
-
return created
|
|
241
|
-
},
|
|
242
|
-
|
|
243
|
-
async deleteCampaign(id: string): Promise<void> {
|
|
244
|
-
const idx = campaigns.findIndex((c) => c.id === id)
|
|
245
|
-
if (idx >= 0) campaigns.splice(idx, 1)
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
async landingPages(query: MarketingQuery): Promise<LandingPagePerf[]> {
|
|
249
|
-
return landing.map((p) => {
|
|
250
|
-
const visits = scale(p.visits, query.range)
|
|
251
|
-
const conversions = scale(p.conversions, query.range)
|
|
252
|
-
return {
|
|
253
|
-
...p,
|
|
254
|
-
siteName: MOCK_SITE.name,
|
|
255
|
-
siteUrl: MOCK_SITE.url,
|
|
256
|
-
appId: MOCK_SITE.appId,
|
|
257
|
-
editorUrl: MOCK_SITE.editorUrl,
|
|
258
|
-
url: `${MOCK_SITE.url}${p.route}`,
|
|
259
|
-
visits,
|
|
260
|
-
conversions,
|
|
261
|
-
cvr: visits > 0 ? (conversions / visits) * 100 : null,
|
|
262
|
-
}
|
|
263
|
-
})
|
|
264
|
-
},
|
|
265
|
-
|
|
266
|
-
async saveLandingPage(input: SaveLandingPageInput): Promise<LandingPage> {
|
|
267
|
-
const route = normalizeRoute(input.route)
|
|
268
|
-
const existing = input.id ? landing.find((p) => p.id === input.id) : undefined
|
|
269
|
-
if (existing) {
|
|
270
|
-
Object.assign(existing, {
|
|
271
|
-
siteId: input.siteId, name: input.name, route,
|
|
272
|
-
leadTags: input.leadTags ?? existing.leadTags,
|
|
273
|
-
status: input.status ?? existing.status,
|
|
274
|
-
prompt: input.prompt,
|
|
275
|
-
isActive: input.isActive ?? existing.isActive,
|
|
276
|
-
})
|
|
277
|
-
return existing
|
|
278
|
-
}
|
|
279
|
-
const created: MockLandingPage = {
|
|
280
|
-
id: `lp-${++counter}`,
|
|
281
|
-
siteId: input.siteId,
|
|
282
|
-
name: input.name,
|
|
283
|
-
route,
|
|
284
|
-
leadTags: input.leadTags ?? [],
|
|
285
|
-
status: input.status ?? 'live',
|
|
286
|
-
prompt: input.prompt,
|
|
287
|
-
isActive: input.isActive ?? true,
|
|
288
|
-
visits: 0,
|
|
289
|
-
conversions: 0,
|
|
290
|
-
}
|
|
291
|
-
landing.push(created)
|
|
292
|
-
return created
|
|
293
|
-
},
|
|
294
|
-
|
|
295
|
-
async deleteLandingPage(id: string): Promise<void> {
|
|
296
|
-
const idx = landing.findIndex((p) => p.id === id)
|
|
297
|
-
if (idx >= 0) landing.splice(idx, 1)
|
|
298
|
-
},
|
|
299
|
-
|
|
300
|
-
async listSites(): Promise<MarketingSite[]> {
|
|
301
|
-
return [MOCK_SITE]
|
|
302
|
-
},
|
|
303
|
-
|
|
304
|
-
async listLeadTags(): Promise<Array<{ slug: string; name: string }>> {
|
|
305
|
-
return MOCK_LEAD_TAGS
|
|
306
|
-
},
|
|
307
|
-
}
|
|
308
|
-
}
|