@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,72 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
3
|
-
import { SettingsGroup, ToggleRow, SelectRow } from '@fayz-ai/saas'
|
|
4
|
-
import { useMarketingConfig } from '../MarketingContext'
|
|
5
|
-
|
|
6
|
-
const SOURCE_OPTIONS = [
|
|
7
|
-
{ value: 'crm', label: 'CRM' },
|
|
8
|
-
{ value: 'agenda', label: 'Agenda' },
|
|
9
|
-
{ value: 'orders', label: 'Orders' },
|
|
10
|
-
{ value: 'custom', label: 'Custom' },
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
export function SettingsView() {
|
|
14
|
-
const t = useTranslation()
|
|
15
|
-
const { conversion, channels } = useMarketingConfig()
|
|
16
|
-
|
|
17
|
-
// Local UI state — mock toggles (no backend yet), mirrors other plugins' settings.
|
|
18
|
-
const [source, setSource] = React.useState(conversion.source)
|
|
19
|
-
const [trackValue, setTrackValue] = React.useState(true)
|
|
20
|
-
const [autoSync, setAutoSync] = React.useState(true)
|
|
21
|
-
const [assisted, setAssisted] = React.useState(false)
|
|
22
|
-
const [tracked, setTracked] = React.useState<Record<string, boolean>>(
|
|
23
|
-
() => Object.fromEntries(channels.map((c) => [c.id, true])),
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<div className="space-y-4">
|
|
28
|
-
<SettingsGroup title={t('marketing.settings.conversionModel')} description={t('marketing.settings.conversionModelDesc')}>
|
|
29
|
-
<SelectRow
|
|
30
|
-
label={t('marketing.settings.attributedFrom')}
|
|
31
|
-
description={conversion.label}
|
|
32
|
-
value={source}
|
|
33
|
-
options={SOURCE_OPTIONS}
|
|
34
|
-
onChange={(v) => setSource(v as typeof source)}
|
|
35
|
-
/>
|
|
36
|
-
<ToggleRow
|
|
37
|
-
label={t('marketing.settings.trackValue')}
|
|
38
|
-
description={t('marketing.settings.trackValueDesc')}
|
|
39
|
-
checked={trackValue}
|
|
40
|
-
onChange={setTrackValue}
|
|
41
|
-
/>
|
|
42
|
-
</SettingsGroup>
|
|
43
|
-
|
|
44
|
-
<SettingsGroup title={t('marketing.settings.channels')} description={t('marketing.settings.channelsDesc')}>
|
|
45
|
-
{channels.map((c) => (
|
|
46
|
-
<ToggleRow
|
|
47
|
-
key={c.id}
|
|
48
|
-
label={c.label}
|
|
49
|
-
description={t('marketing.settings.channelTrackDesc')}
|
|
50
|
-
checked={tracked[c.id] ?? true}
|
|
51
|
-
onChange={(v) => setTracked((prev) => ({ ...prev, [c.id]: v }))}
|
|
52
|
-
/>
|
|
53
|
-
))}
|
|
54
|
-
</SettingsGroup>
|
|
55
|
-
|
|
56
|
-
<SettingsGroup title={t('marketing.settings.attribution')} description={t('marketing.settings.attributionDesc')}>
|
|
57
|
-
<ToggleRow
|
|
58
|
-
label={t('marketing.settings.autoSync')}
|
|
59
|
-
description={t('marketing.settings.autoSyncDesc')}
|
|
60
|
-
checked={autoSync}
|
|
61
|
-
onChange={setAutoSync}
|
|
62
|
-
/>
|
|
63
|
-
<ToggleRow
|
|
64
|
-
label={t('marketing.settings.assisted')}
|
|
65
|
-
description={t('marketing.settings.assistedDesc')}
|
|
66
|
-
checked={assisted}
|
|
67
|
-
onChange={setAssisted}
|
|
68
|
-
/>
|
|
69
|
-
</SettingsGroup>
|
|
70
|
-
</div>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useState } from 'react'
|
|
2
|
-
import { CrudListView } from '@fayz-ai/saas'
|
|
3
|
-
import { SegmentedControl } from '@fayz-ai/ui'
|
|
4
|
-
import { useMarketingConfig } from '../../MarketingContext'
|
|
5
|
-
import { buildBlogPostEntity, buildBlogCategoryEntity } from '../../data/blogEntities'
|
|
6
|
-
import { useEntityCrud } from './useEntityCrud'
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Blog backoffice — an in-page marketing tab with two lenses (Posts /
|
|
10
|
-
// Categories), each a generic CrudListView backed by the blog tables. Follows
|
|
11
|
-
// the content-planner pattern: this plugin owns the data + routing; create/edit
|
|
12
|
-
// deep-link through the marketing module nav (navigate('blog-post:id') etc.).
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
function PostsList({ navigate }: { navigate: (view: string) => void }) {
|
|
16
|
-
const entity = useMemo(() => buildBlogPostEntity(), [])
|
|
17
|
-
const { items, total, fetch } = useEntityCrud(entity)
|
|
18
|
-
const [search, setSearch] = useState('')
|
|
19
|
-
const [statusFilter, setStatusFilter] = useState<string | undefined>()
|
|
20
|
-
const [loadedOnce, setLoadedOnce] = useState(false)
|
|
21
|
-
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
let active = true
|
|
24
|
-
Promise.resolve(
|
|
25
|
-
fetch({ search: search || undefined, filters: statusFilter ? { status: statusFilter } : undefined }),
|
|
26
|
-
).finally(() => { if (active) setLoadedOnce(true) })
|
|
27
|
-
return () => { active = false }
|
|
28
|
-
}, [search, statusFilter, fetch])
|
|
29
|
-
|
|
30
|
-
const facets = useMemo(() => [{
|
|
31
|
-
field: 'status',
|
|
32
|
-
allLabel: 'All',
|
|
33
|
-
options: [{ value: 'draft', label: 'Draft' }, { value: 'published', label: 'Published' }],
|
|
34
|
-
}], [])
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<CrudListView
|
|
38
|
-
entityDef={entity}
|
|
39
|
-
items={loadedOnce ? items : null}
|
|
40
|
-
total={total}
|
|
41
|
-
search={search}
|
|
42
|
-
onSearchChange={setSearch}
|
|
43
|
-
searchPlaceholder="Search posts…"
|
|
44
|
-
facets={facets}
|
|
45
|
-
activeFilters={{ status: statusFilter }}
|
|
46
|
-
onFacetChange={(_field, value) => setStatusFilter(value)}
|
|
47
|
-
onNew={() => navigate('blog-new')}
|
|
48
|
-
addLabel="New post"
|
|
49
|
-
onRowClick={(row) => navigate(`blog-post:${(row as { id: string }).id}`)}
|
|
50
|
-
/>
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function CategoriesList({ navigate }: { navigate: (view: string) => void }) {
|
|
55
|
-
const entity = useMemo(() => buildBlogCategoryEntity(), [])
|
|
56
|
-
const { items, total, fetch } = useEntityCrud(entity)
|
|
57
|
-
const [search, setSearch] = useState('')
|
|
58
|
-
const [loadedOnce, setLoadedOnce] = useState(false)
|
|
59
|
-
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
let active = true
|
|
62
|
-
Promise.resolve(fetch({ search: search || undefined })).finally(() => { if (active) setLoadedOnce(true) })
|
|
63
|
-
return () => { active = false }
|
|
64
|
-
}, [search, fetch])
|
|
65
|
-
|
|
66
|
-
return (
|
|
67
|
-
<CrudListView
|
|
68
|
-
entityDef={entity}
|
|
69
|
-
items={loadedOnce ? items : null}
|
|
70
|
-
total={total}
|
|
71
|
-
search={search}
|
|
72
|
-
onSearchChange={setSearch}
|
|
73
|
-
searchPlaceholder="Search categories…"
|
|
74
|
-
onNew={() => navigate('blog-cat-new')}
|
|
75
|
-
addLabel="New category"
|
|
76
|
-
onRowClick={(row) => navigate(`blog-cat:${(row as { id: string }).id}`)}
|
|
77
|
-
/>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function BlogAdminView({ navigate }: { navigate: (view: string) => void }) {
|
|
82
|
-
const { labels } = useMarketingConfig()
|
|
83
|
-
const [lens, setLens] = useState<'posts' | 'categories'>('posts')
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<div className="space-y-4">
|
|
87
|
-
<SegmentedControl
|
|
88
|
-
aria-label="Blog section"
|
|
89
|
-
options={[
|
|
90
|
-
{ value: 'posts', label: labels.blog },
|
|
91
|
-
{ value: 'categories', label: labels.blogCategories },
|
|
92
|
-
]}
|
|
93
|
-
value={lens}
|
|
94
|
-
onChange={(v) => setLens(v as 'posts' | 'categories')}
|
|
95
|
-
/>
|
|
96
|
-
{lens === 'posts' ? <PostsList navigate={navigate} /> : <CategoriesList navigate={navigate} />}
|
|
97
|
-
</div>
|
|
98
|
-
)
|
|
99
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { SubpageHeader, Button, toast } from '@fayz-ai/ui'
|
|
3
|
-
import { CrudFormPage, useLimitGuard, invalidateLimit } from '@fayz-ai/saas'
|
|
4
|
-
import type { EntityDef } from '@fayz-ai/core'
|
|
5
|
-
import { useEntityCrud } from './useEntityCrud'
|
|
6
|
-
import { BLOG_POSTS_TABLE } from '../../data/blogEntities'
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Generic create/edit for a blog entity (post or category), rendered through
|
|
10
|
-
// the shared CrudFormPage (declarative EntityDef → fields, incl. the markdown
|
|
11
|
-
// body + category relation). Persistence flows through the generic Supabase
|
|
12
|
-
// provider; this only wires load / save / delete + the back link.
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
export function BlogEntityForm<T extends { id: string }>({
|
|
16
|
-
entity,
|
|
17
|
-
editId,
|
|
18
|
-
parentLabel,
|
|
19
|
-
onDone,
|
|
20
|
-
}: {
|
|
21
|
-
entity: EntityDef<T>
|
|
22
|
-
editId?: string
|
|
23
|
-
parentLabel: string
|
|
24
|
-
onDone: () => void
|
|
25
|
-
}) {
|
|
26
|
-
const { getById, create, update, remove } = useEntityCrud(entity)
|
|
27
|
-
const isEdit = !!editId
|
|
28
|
-
// Plan quantity guard applies to blog POSTS only (categories are unbounded).
|
|
29
|
-
const isPost = (entity as { data?: { table?: string } }).data?.table === BLOG_POSTS_TABLE
|
|
30
|
-
const guardBlogPosts = useLimitGuard('blog_posts')
|
|
31
|
-
const namePlural = entity.namePlural ?? entity.name + 's'
|
|
32
|
-
const [initialData, setInitialData] = React.useState<Record<string, any> | null>(isEdit ? null : {})
|
|
33
|
-
|
|
34
|
-
React.useEffect(() => {
|
|
35
|
-
if (!editId) return
|
|
36
|
-
let cancelled = false
|
|
37
|
-
;(async () => {
|
|
38
|
-
const item = await getById(editId)
|
|
39
|
-
if (!cancelled) setInitialData((item as Record<string, any>) ?? {})
|
|
40
|
-
})()
|
|
41
|
-
return () => { cancelled = true }
|
|
42
|
-
}, [editId, getById])
|
|
43
|
-
|
|
44
|
-
async function save(values: Record<string, any>) {
|
|
45
|
-
// Client-side plan guard before the provider create — posts only, on create.
|
|
46
|
-
if (!isEdit && isPost && (await guardBlogPosts()) === 'blocked') return
|
|
47
|
-
try {
|
|
48
|
-
if (isEdit) await update(editId!, values)
|
|
49
|
-
else {
|
|
50
|
-
await create(values)
|
|
51
|
-
if (isPost) invalidateLimit('blog_posts')
|
|
52
|
-
}
|
|
53
|
-
toast.success(isEdit ? `${entity.name} updated` : `${entity.name} created`)
|
|
54
|
-
onDone()
|
|
55
|
-
} catch (err) {
|
|
56
|
-
toast.error(`Failed to save ${entity.name.toLowerCase()}`)
|
|
57
|
-
throw err
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function del() {
|
|
62
|
-
if (!editId) return
|
|
63
|
-
try {
|
|
64
|
-
await remove(editId)
|
|
65
|
-
toast.success(`${entity.name} deleted`)
|
|
66
|
-
onDone()
|
|
67
|
-
} catch {
|
|
68
|
-
toast.error(`Failed to delete ${entity.name.toLowerCase()}`)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const title = isEdit
|
|
73
|
-
? (initialData?.[entity.displayField ?? 'name'] ?? entity.name)
|
|
74
|
-
: `New ${entity.name.toLowerCase()}`
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<div className="space-y-4">
|
|
78
|
-
<SubpageHeader
|
|
79
|
-
title={String(title)}
|
|
80
|
-
onBack={onDone}
|
|
81
|
-
parentLabel={parentLabel}
|
|
82
|
-
actions={isEdit ? (
|
|
83
|
-
<Button variant="ghost" className="text-destructive hover:text-destructive" onClick={del}>
|
|
84
|
-
Delete
|
|
85
|
-
</Button>
|
|
86
|
-
) : undefined}
|
|
87
|
-
/>
|
|
88
|
-
{initialData !== null && (
|
|
89
|
-
<CrudFormPage
|
|
90
|
-
entityDef={entity}
|
|
91
|
-
mode={isEdit ? 'edit' : 'create'}
|
|
92
|
-
initialData={initialData}
|
|
93
|
-
namePlural={namePlural}
|
|
94
|
-
onCancel={onDone}
|
|
95
|
-
onSubmit={save}
|
|
96
|
-
hideBreadcrumb
|
|
97
|
-
/>
|
|
98
|
-
)}
|
|
99
|
-
</div>
|
|
100
|
-
)
|
|
101
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { useMemo, useState, useCallback } from 'react'
|
|
2
|
-
import { resolveDataProvider } from '@fayz-ai/core'
|
|
3
|
-
import type { EntityDef } from '@fayz-ai/core'
|
|
4
|
-
|
|
5
|
-
interface ListQuery {
|
|
6
|
-
search?: string
|
|
7
|
-
filters?: Record<string, unknown>
|
|
8
|
-
page?: number
|
|
9
|
-
pageSize?: number
|
|
10
|
-
sortBy?: string
|
|
11
|
-
sortDir?: 'asc' | 'desc'
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Minimal store-less CRUD wiring for a plugin that owns its data + routing.
|
|
16
|
-
* Resolves the generic Supabase provider from the EntityDef's `data.table`
|
|
17
|
-
* (mock fallback when Supabase is not configured) and exposes list/get/save/
|
|
18
|
-
* remove. Used by the blog backoffice list + form views — the heavy list/form
|
|
19
|
-
* rendering stays in the shared CrudListView / CrudFormPage.
|
|
20
|
-
*/
|
|
21
|
-
export function useEntityCrud<T extends { id: string }>(entity: EntityDef<T>) {
|
|
22
|
-
const provider = useMemo(() => resolveDataProvider<T>(entity), [entity])
|
|
23
|
-
const [items, setItems] = useState<T[] | null>(null)
|
|
24
|
-
const [total, setTotal] = useState(0)
|
|
25
|
-
|
|
26
|
-
const fetch = useCallback(async (query: ListQuery = {}) => {
|
|
27
|
-
try {
|
|
28
|
-
const res = await provider.list(query as any)
|
|
29
|
-
setItems(res.data)
|
|
30
|
-
setTotal(res.total)
|
|
31
|
-
} catch (err) {
|
|
32
|
-
// Table not provisioned yet (migration not applied to this pool) or a
|
|
33
|
-
// transient error — surface an empty list instead of a perpetual skeleton.
|
|
34
|
-
console.warn('[blog] list failed; showing empty. Apply the plugin-blog migration to this pool.', err)
|
|
35
|
-
setItems([])
|
|
36
|
-
setTotal(0)
|
|
37
|
-
}
|
|
38
|
-
}, [provider])
|
|
39
|
-
|
|
40
|
-
const getById = useCallback(async (id: string): Promise<T | null> => {
|
|
41
|
-
const res = await provider.list({ filters: { id }, page: 1, pageSize: 1 } as any)
|
|
42
|
-
return res.data[0] ?? null
|
|
43
|
-
}, [provider])
|
|
44
|
-
|
|
45
|
-
const create = useCallback((data: Record<string, unknown>) => provider.create(data as any), [provider])
|
|
46
|
-
const update = useCallback((id: string, data: Record<string, unknown>) => provider.update(id, data as any), [provider])
|
|
47
|
-
const remove = useCallback((id: string) => provider.remove(id), [provider])
|
|
48
|
-
|
|
49
|
-
return { items, total, fetch, getById, create, update, remove }
|
|
50
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { createPluginContext } from '@fayz-ai/saas'
|
|
2
|
-
import type { ResolvedMarketingConfig } from '../../MarketingContext'
|
|
3
|
-
import type { ContentPlannerProvider } from '../../data/contentTypes'
|
|
4
|
-
import type { ContentPlannerUIState } from './contentStore'
|
|
5
|
-
|
|
6
|
-
// Separate context trio for the content planner — MarketingContext's generics
|
|
7
|
-
// are fixed to the analytics provider/store, so the planner mounts its own.
|
|
8
|
-
const ctx = createPluginContext<ResolvedMarketingConfig, ContentPlannerProvider, ContentPlannerUIState>('ContentPlanner')
|
|
9
|
-
|
|
10
|
-
export const ContentPlannerContextProvider = ctx.ContextProvider
|
|
11
|
-
export const useContentPlannerConfig = ctx.useConfig
|
|
12
|
-
export const useContentPlannerProvider = ctx.useProvider
|
|
13
|
-
export const useContentPlannerStore = ctx.useStore
|