@fayz-ai/plugin-marketing 0.9.0 → 0.10.0
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 +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +241 -188
- package/dist/index.js.map +1 -1
- package/dist/integrations/ranklayer/connectorDef.d.ts +1 -1
- package/dist/integrations/ranklayer/connectorDef.d.ts.map +1 -1
- package/dist/integrations/ranklayer/data/supabase.d.ts +24 -6
- package/dist/integrations/ranklayer/data/supabase.d.ts.map +1 -1
- package/dist/integrations/ranklayer/outbound.d.ts +16 -0
- package/dist/integrations/ranklayer/outbound.d.ts.map +1 -0
- package/dist/integrations/ranklayer/settings.d.ts +12 -0
- package/dist/integrations/ranklayer/settings.d.ts.map +1 -0
- package/dist/integrations/ranklayer/types.d.ts +0 -1
- package/dist/integrations/ranklayer/types.d.ts.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/migrations/index.d.ts +1 -1
- package/dist/migrations/index.d.ts.map +1 -1
- 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
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'
|
|
2
|
-
import type {
|
|
3
|
-
ContentPlan,
|
|
4
|
-
ContentPlannerProvider,
|
|
5
|
-
ContentPost,
|
|
6
|
-
SocialAccount,
|
|
7
|
-
} from './contentTypes'
|
|
8
|
-
import { T } from './tables'
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Supabase content-planner provider. Tables: plg_marketing_social_accounts,
|
|
12
|
-
// plg_marketing_content_plans, plg_marketing_content_posts (see src/migrations/001_content_planner.sql).
|
|
13
|
-
// RLS scopes rows to the user's tenants; we still filter tenant_id explicitly
|
|
14
|
-
// on reads so multi-tenant sessions see only the active tenant.
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
function getClient() {
|
|
18
|
-
const supabase = getSupabaseClientOptional() as any
|
|
19
|
-
if (!supabase) throw new Error('Supabase not initialized')
|
|
20
|
-
return supabase
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function requireTenantId(): string {
|
|
24
|
-
const tenantId = getActiveTenantId()
|
|
25
|
-
if (!tenantId) throw new Error('No active tenant')
|
|
26
|
-
return tenantId
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function snakeToCamel(obj: Record<string, unknown>): Record<string, unknown> {
|
|
30
|
-
const result: Record<string, unknown> = {}
|
|
31
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
32
|
-
result[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = value ?? undefined
|
|
33
|
-
}
|
|
34
|
-
return result
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function toAccount(row: Record<string, unknown>): SocialAccount {
|
|
38
|
-
const account = snakeToCamel(row) as unknown as SocialAccount
|
|
39
|
-
if (!Array.isArray(account.platforms)) account.platforms = []
|
|
40
|
-
return account
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function toPlan(row: Record<string, unknown>): ContentPlan {
|
|
44
|
-
const plan = snakeToCamel(row) as unknown as ContentPlan
|
|
45
|
-
if (!Array.isArray(plan.pillars)) plan.pillars = []
|
|
46
|
-
if (!Array.isArray(plan.formats)) plan.formats = []
|
|
47
|
-
plan.briefMd = plan.briefMd ?? ''
|
|
48
|
-
return plan
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function toPost(row: Record<string, unknown>): ContentPost {
|
|
52
|
-
const post = snakeToCamel(row) as unknown as ContentPost
|
|
53
|
-
post.contentMd = post.contentMd ?? ''
|
|
54
|
-
if (!Array.isArray(post.platforms)) post.platforms = []
|
|
55
|
-
if (!Array.isArray(post.checklist)) post.checklist = []
|
|
56
|
-
return post
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function createSupabaseContentPlannerProvider(): ContentPlannerProvider {
|
|
60
|
-
return {
|
|
61
|
-
async listAccounts() {
|
|
62
|
-
const tenantId = getActiveTenantId()
|
|
63
|
-
if (!tenantId) return []
|
|
64
|
-
const { data } = await getClient()
|
|
65
|
-
.from(T.socialAccounts)
|
|
66
|
-
.select('*')
|
|
67
|
-
.eq('tenant_id', tenantId)
|
|
68
|
-
.eq('is_active', true)
|
|
69
|
-
.order('created_at', { ascending: true })
|
|
70
|
-
return (data ?? []).map(toAccount)
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
async saveAccount(input) {
|
|
74
|
-
if (input.id) {
|
|
75
|
-
const payload: Record<string, unknown> = {}
|
|
76
|
-
if (input.name !== undefined) payload.name = input.name
|
|
77
|
-
if (input.handle !== undefined) payload.handle = input.handle
|
|
78
|
-
if (input.platforms !== undefined) payload.platforms = input.platforms
|
|
79
|
-
if (input.isActive !== undefined) payload.is_active = input.isActive
|
|
80
|
-
const { data, error } = await getClient()
|
|
81
|
-
.from(T.socialAccounts)
|
|
82
|
-
.update(payload)
|
|
83
|
-
.eq('id', input.id)
|
|
84
|
-
.select('*')
|
|
85
|
-
.single()
|
|
86
|
-
if (error) throw error
|
|
87
|
-
return toAccount(data)
|
|
88
|
-
}
|
|
89
|
-
const { data, error } = await getClient()
|
|
90
|
-
.from(T.socialAccounts)
|
|
91
|
-
.insert({
|
|
92
|
-
tenant_id: requireTenantId(),
|
|
93
|
-
name: input.name,
|
|
94
|
-
handle: input.handle ?? null,
|
|
95
|
-
platforms: input.platforms ?? ['instagram'],
|
|
96
|
-
is_active: input.isActive ?? true,
|
|
97
|
-
})
|
|
98
|
-
.select('*')
|
|
99
|
-
.single()
|
|
100
|
-
if (error) throw error
|
|
101
|
-
return toAccount(data)
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
async deleteAccount(id) {
|
|
105
|
-
const { error } = await getClient().from(T.socialAccounts).delete().eq('id', id)
|
|
106
|
-
if (error) throw error
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
async listPlans(accountId) {
|
|
110
|
-
const tenantId = getActiveTenantId()
|
|
111
|
-
if (!tenantId) return []
|
|
112
|
-
const { data } = await getClient()
|
|
113
|
-
.from(T.contentPlans)
|
|
114
|
-
.select('*')
|
|
115
|
-
.eq('tenant_id', tenantId)
|
|
116
|
-
.eq('account_id', accountId)
|
|
117
|
-
.order('created_at', { ascending: false })
|
|
118
|
-
return (data ?? []).map(toPlan)
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
async getPlan(id) {
|
|
122
|
-
const { data } = await getClient()
|
|
123
|
-
.from(T.contentPlans)
|
|
124
|
-
.select('*')
|
|
125
|
-
.eq('id', id)
|
|
126
|
-
.maybeSingle()
|
|
127
|
-
return data ? toPlan(data) : null
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
async savePlan(input) {
|
|
131
|
-
if (input.id) {
|
|
132
|
-
const payload: Record<string, unknown> = { updated_at: new Date().toISOString() }
|
|
133
|
-
if (input.name !== undefined) payload.name = input.name
|
|
134
|
-
if (input.status !== undefined) payload.status = input.status
|
|
135
|
-
if (input.weeksCount !== undefined) payload.weeks_count = input.weeksCount
|
|
136
|
-
if (input.startDate !== undefined) payload.start_date = input.startDate
|
|
137
|
-
if (input.objective !== undefined) payload.objective = input.objective
|
|
138
|
-
if (input.tone !== undefined) payload.tone = input.tone
|
|
139
|
-
if (input.pillars !== undefined) payload.pillars = input.pillars
|
|
140
|
-
if (input.formats !== undefined) payload.formats = input.formats
|
|
141
|
-
if (input.weeklyFrequency !== undefined) payload.weekly_frequency = input.weeklyFrequency
|
|
142
|
-
if (input.briefMd !== undefined) payload.brief_md = input.briefMd
|
|
143
|
-
const { data, error } = await getClient()
|
|
144
|
-
.from(T.contentPlans)
|
|
145
|
-
.update(payload)
|
|
146
|
-
.eq('id', input.id)
|
|
147
|
-
.select('*')
|
|
148
|
-
.single()
|
|
149
|
-
if (error) throw error
|
|
150
|
-
return toPlan(data)
|
|
151
|
-
}
|
|
152
|
-
if (!input.accountId) throw new Error('accountId is required')
|
|
153
|
-
const { data, error } = await getClient()
|
|
154
|
-
.from(T.contentPlans)
|
|
155
|
-
.insert({
|
|
156
|
-
tenant_id: requireTenantId(),
|
|
157
|
-
account_id: input.accountId,
|
|
158
|
-
name: input.name ?? 'Novo plano',
|
|
159
|
-
status: input.status ?? 'draft',
|
|
160
|
-
weeks_count: input.weeksCount ?? 4,
|
|
161
|
-
start_date: input.startDate ?? null,
|
|
162
|
-
objective: input.objective ?? null,
|
|
163
|
-
tone: input.tone ?? null,
|
|
164
|
-
pillars: input.pillars ?? [],
|
|
165
|
-
formats: input.formats ?? ['reel', 'static'],
|
|
166
|
-
weekly_frequency: input.weeklyFrequency ?? 3,
|
|
167
|
-
brief_md: input.briefMd ?? '',
|
|
168
|
-
})
|
|
169
|
-
.select('*')
|
|
170
|
-
.single()
|
|
171
|
-
if (error) throw error
|
|
172
|
-
return toPlan(data)
|
|
173
|
-
},
|
|
174
|
-
|
|
175
|
-
async deletePlan(id) {
|
|
176
|
-
await getClient().from(T.contentPlans).delete().eq('id', id)
|
|
177
|
-
},
|
|
178
|
-
|
|
179
|
-
async listPosts(planId) {
|
|
180
|
-
const { data } = await getClient()
|
|
181
|
-
.from(T.contentPosts)
|
|
182
|
-
.select('*')
|
|
183
|
-
.eq('plan_id', planId)
|
|
184
|
-
.order('week_number', { ascending: true })
|
|
185
|
-
.order('position', { ascending: true })
|
|
186
|
-
return (data ?? []).map(toPost)
|
|
187
|
-
},
|
|
188
|
-
|
|
189
|
-
async getPost(id) {
|
|
190
|
-
const { data } = await getClient()
|
|
191
|
-
.from(T.contentPosts)
|
|
192
|
-
.select('*')
|
|
193
|
-
.eq('id', id)
|
|
194
|
-
.maybeSingle()
|
|
195
|
-
return data ? toPost(data) : null
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
async savePost(input) {
|
|
199
|
-
if (input.id) {
|
|
200
|
-
const payload: Record<string, unknown> = { updated_at: new Date().toISOString() }
|
|
201
|
-
if (input.weekNumber !== undefined) payload.week_number = input.weekNumber
|
|
202
|
-
if (input.position !== undefined) payload.position = input.position
|
|
203
|
-
if (input.title !== undefined) payload.title = input.title
|
|
204
|
-
if (input.format !== undefined) payload.format = input.format
|
|
205
|
-
if (input.status !== undefined) payload.status = input.status
|
|
206
|
-
if (input.scheduledDate !== undefined) payload.scheduled_date = input.scheduledDate
|
|
207
|
-
if (input.platforms !== undefined) payload.platforms = input.platforms
|
|
208
|
-
if (input.checklist !== undefined) payload.checklist = input.checklist
|
|
209
|
-
if (input.mediaUrl !== undefined) payload.media_url = input.mediaUrl
|
|
210
|
-
if (input.hook !== undefined) payload.hook = input.hook
|
|
211
|
-
if (input.cta !== undefined) payload.cta = input.cta
|
|
212
|
-
if (input.contentMd !== undefined) payload.content_md = input.contentMd
|
|
213
|
-
const { data, error } = await getClient()
|
|
214
|
-
.from(T.contentPosts)
|
|
215
|
-
.update(payload)
|
|
216
|
-
.eq('id', input.id)
|
|
217
|
-
.select('*')
|
|
218
|
-
.single()
|
|
219
|
-
if (error) throw error
|
|
220
|
-
return toPost(data)
|
|
221
|
-
}
|
|
222
|
-
if (!input.planId) throw new Error('planId is required')
|
|
223
|
-
const week = input.weekNumber ?? 1
|
|
224
|
-
|
|
225
|
-
// Next position within the (plan, week) group.
|
|
226
|
-
const { data: maxData } = await getClient()
|
|
227
|
-
.from(T.contentPosts)
|
|
228
|
-
.select('position')
|
|
229
|
-
.eq('plan_id', input.planId)
|
|
230
|
-
.eq('week_number', week)
|
|
231
|
-
.order('position', { ascending: false })
|
|
232
|
-
.limit(1)
|
|
233
|
-
const maxPos = maxData?.[0]?.position ?? -1
|
|
234
|
-
|
|
235
|
-
const { data, error } = await getClient()
|
|
236
|
-
.from(T.contentPosts)
|
|
237
|
-
.insert({
|
|
238
|
-
tenant_id: requireTenantId(),
|
|
239
|
-
plan_id: input.planId,
|
|
240
|
-
week_number: week,
|
|
241
|
-
position: input.position ?? maxPos + 1,
|
|
242
|
-
title: input.title ?? '',
|
|
243
|
-
format: input.format ?? 'reel',
|
|
244
|
-
status: input.status ?? 'idea',
|
|
245
|
-
scheduled_date: input.scheduledDate ?? null,
|
|
246
|
-
platforms: input.platforms ?? [],
|
|
247
|
-
checklist: input.checklist ?? [],
|
|
248
|
-
media_url: input.mediaUrl ?? null,
|
|
249
|
-
hook: input.hook ?? null,
|
|
250
|
-
cta: input.cta ?? null,
|
|
251
|
-
content_md: input.contentMd ?? '',
|
|
252
|
-
})
|
|
253
|
-
.select('*')
|
|
254
|
-
.single()
|
|
255
|
-
if (error) throw error
|
|
256
|
-
return toPost(data)
|
|
257
|
-
},
|
|
258
|
-
|
|
259
|
-
async deletePost(id) {
|
|
260
|
-
await getClient().from(T.contentPosts).delete().eq('id', id)
|
|
261
|
-
},
|
|
262
|
-
}
|
|
263
|
-
}
|
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
|
-
}
|