@fayz-ai/plugin-marketing 0.9.0-next.1 → 0.9.1

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