@fayz-ai/plugin-marketing 0.8.2 → 0.9.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts +6 -0
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/tables.d.ts +15 -0
- package/dist/data/tables.d.ts.map +1 -1
- package/dist/data/types.d.ts +43 -2
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +782 -104
- package/dist/index.js.map +1 -1
- package/dist/integrations/ranklayer/connectorDef.d.ts.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/registries.d.ts +3 -1
- package/dist/registries.d.ts.map +1 -1
- package/dist/store.d.ts +9 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +41 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/views/LandingPageComposer.d.ts +8 -0
- package/dist/views/LandingPageComposer.d.ts.map +1 -0
- package/dist/views/LandingPagesView.d.ts.map +1 -1
- package/package.json +14 -12
- package/src/data/mock.ts +83 -8
- package/src/data/supabase.ts +215 -3
- package/src/data/tables.ts +17 -0
- package/src/data/types.ts +41 -1
- package/src/index.ts +12 -2
- package/src/integrations/ranklayer/connectorDef.tsx +4 -0
- package/src/locales/en.ts +24 -1
- package/src/locales/pt-BR.ts +24 -1
- package/src/migrations/008_landing_pages.sql +112 -0
- package/src/migrations/index.ts +116 -1
- package/src/registries.ts +3 -1
- package/src/store.ts +26 -3
- package/src/types.ts +44 -5
- package/src/views/LandingPageComposer.tsx +201 -0
- package/src/views/LandingPagesView.tsx +145 -22
- package/dist/index.cjs +0 -5237
- package/dist/index.cjs.map +0 -1
|
@@ -1,49 +1,172 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { MousePointerClick, Layers,
|
|
2
|
+
import { MousePointerClick, Layers, Users, Plus, Pencil, Trash2, ExternalLink, Sparkles } from 'lucide-react'
|
|
3
3
|
import type { ColumnDef } from '@tanstack/react-table'
|
|
4
|
-
import { DataTable } from '@fayz-ai/ui'
|
|
4
|
+
import { Button, DataTable } from '@fayz-ai/ui'
|
|
5
|
+
import { PermissionGate } from '@fayz-ai/saas'
|
|
5
6
|
import { useTranslation } from '@fayz-ai/core'
|
|
6
7
|
import { useMarketingStore } from '../MarketingContext'
|
|
7
8
|
import { formatNumber, formatPercent } from '../format'
|
|
8
9
|
import { KpiCard, RangeTabs } from '../components/MarketingBits'
|
|
10
|
+
import { LandingPageComposer } from './LandingPageComposer'
|
|
9
11
|
import type { LandingPagePerf } from '../types'
|
|
10
12
|
|
|
11
|
-
const TYPE_STYLE: Record<string, string> = {
|
|
12
|
-
Funnel: 'bg-indigo-100 text-indigo-700',
|
|
13
|
-
'Landing page': 'bg-pink-100 text-pink-700',
|
|
14
|
-
Website: 'bg-sky-100 text-sky-700',
|
|
15
|
-
}
|
|
16
|
-
|
|
17
13
|
export function LandingPagesView() {
|
|
18
14
|
const t = useTranslation()
|
|
19
15
|
const pages = useMarketingStore((s) => s.landingPages)
|
|
16
|
+
const deleteLandingPage = useMarketingStore((s) => s.deleteLandingPage)
|
|
17
|
+
const [editing, setEditing] = React.useState<LandingPagePerf | null>(null)
|
|
18
|
+
const [composerOpen, setComposerOpen] = React.useState(false)
|
|
19
|
+
|
|
20
|
+
// Visits are only known for pages whose host reports them; a page with an
|
|
21
|
+
// unknown count must not drag the total down to a wrong number.
|
|
22
|
+
const measured = pages.filter((p) => p.visits !== null)
|
|
23
|
+
const totalVisits = measured.reduce((s, p) => s + (p.visits ?? 0), 0)
|
|
24
|
+
const totalLeads = pages.reduce((s, p) => s + p.conversions, 0)
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
function openNew() {
|
|
27
|
+
setEditing(null)
|
|
28
|
+
setComposerOpen(true)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function openEdit(page: LandingPagePerf) {
|
|
32
|
+
setEditing(page)
|
|
33
|
+
setComposerOpen(true)
|
|
34
|
+
}
|
|
24
35
|
|
|
25
36
|
const columns = React.useMemo<ColumnDef<LandingPagePerf, any>[]>(() => [
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
{
|
|
38
|
+
accessorKey: 'name',
|
|
39
|
+
header: t('marketing.col.page'),
|
|
40
|
+
cell: ({ row }) => (
|
|
41
|
+
<div className="min-w-0">
|
|
42
|
+
<span className="flex items-center gap-1.5 truncate font-medium text-foreground">
|
|
43
|
+
{row.original.name}
|
|
44
|
+
{row.original.status === 'planned' ? (
|
|
45
|
+
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-semibold text-amber-800">
|
|
46
|
+
{t('marketing.landing.statusPlanned')}
|
|
47
|
+
</span>
|
|
48
|
+
) : null}
|
|
49
|
+
</span>
|
|
50
|
+
<span className="block truncate text-xs text-muted-foreground">{row.original.siteName}</span>
|
|
51
|
+
</div>
|
|
52
|
+
),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
accessorKey: 'route',
|
|
56
|
+
header: t('marketing.col.route'),
|
|
57
|
+
cell: ({ row }) => (
|
|
58
|
+
<a
|
|
59
|
+
href={row.original.url}
|
|
60
|
+
target="_blank"
|
|
61
|
+
rel="noreferrer"
|
|
62
|
+
onClick={(e) => e.stopPropagation()}
|
|
63
|
+
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground hover:underline"
|
|
64
|
+
>
|
|
65
|
+
{row.original.route}
|
|
66
|
+
<ExternalLink className="h-3 w-3" />
|
|
67
|
+
</a>
|
|
68
|
+
),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
accessorKey: 'leadTags',
|
|
72
|
+
header: t('marketing.col.leadTag'),
|
|
73
|
+
cell: ({ getValue }) => {
|
|
74
|
+
const tags = (getValue() as string[]) ?? []
|
|
75
|
+
if (!tags.length) return <span className="text-muted-foreground">—</span>
|
|
76
|
+
return (
|
|
77
|
+
<div className="flex flex-wrap gap-1">
|
|
78
|
+
{tags.map((tag) => (
|
|
79
|
+
<span key={tag} className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">{tag}</span>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
accessorKey: 'visits',
|
|
87
|
+
header: t('marketing.col.visits'),
|
|
88
|
+
cell: ({ getValue }) => {
|
|
89
|
+
const visits = getValue() as number | null
|
|
90
|
+
return visits === null
|
|
91
|
+
? <span className="text-muted-foreground" title={t('marketing.landing.visitsPending')}>—</span>
|
|
92
|
+
: <span className="text-muted-foreground">{formatNumber(visits)}</span>
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
accessorKey: 'conversions',
|
|
97
|
+
header: t('marketing.col.leads'),
|
|
98
|
+
cell: ({ getValue }) => <span className="font-medium text-foreground">{formatNumber(getValue() as number)}</span>,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
accessorKey: 'cvr',
|
|
102
|
+
header: t('marketing.col.cvr'),
|
|
103
|
+
cell: ({ getValue }) => {
|
|
104
|
+
const cvr = getValue() as number | null
|
|
105
|
+
return cvr === null
|
|
106
|
+
? <span className="text-muted-foreground">—</span>
|
|
107
|
+
: <span className="text-muted-foreground">{formatPercent(cvr)}</span>
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'actions', header: '', enableSorting: false,
|
|
112
|
+
cell: ({ row }) => (
|
|
113
|
+
<div className="flex items-center justify-end gap-1">
|
|
114
|
+
{row.original.status === 'planned' && row.original.editorUrl ? (
|
|
115
|
+
<button
|
|
116
|
+
onClick={(e) => { e.stopPropagation(); window.open(row.original.editorUrl!, '_blank', 'noopener,noreferrer') }}
|
|
117
|
+
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
118
|
+
title={t('marketing.landing.buildWithFayz')}
|
|
119
|
+
>
|
|
120
|
+
<Sparkles className="h-4 w-4" />
|
|
121
|
+
</button>
|
|
122
|
+
) : null}
|
|
123
|
+
<button
|
|
124
|
+
onClick={(e) => { e.stopPropagation(); openEdit(row.original) }}
|
|
125
|
+
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
126
|
+
title={t('marketing.landing.edit')}
|
|
127
|
+
>
|
|
128
|
+
<Pencil className="h-4 w-4" />
|
|
129
|
+
</button>
|
|
130
|
+
<button
|
|
131
|
+
onClick={(e) => { e.stopPropagation(); void deleteLandingPage(row.original.id) }}
|
|
132
|
+
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
|
133
|
+
title={t('marketing.landing.delete')}
|
|
134
|
+
>
|
|
135
|
+
<Trash2 className="h-4 w-4" />
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
),
|
|
139
|
+
},
|
|
140
|
+
], [t, deleteLandingPage])
|
|
32
141
|
|
|
33
142
|
return (
|
|
34
143
|
<div className="mx-auto max-w-6xl space-y-4">
|
|
35
|
-
<div className="flex items-center justify-
|
|
36
|
-
<p className="text-sm text-muted-foreground">{t('marketing.landing.subtitle')}</p>
|
|
144
|
+
<div className="flex items-center justify-end gap-2">
|
|
37
145
|
<RangeTabs />
|
|
146
|
+
<PermissionGate feature="marketing" action="create">
|
|
147
|
+
<Button onClick={openNew}>
|
|
148
|
+
<Plus className="mr-1.5 h-4 w-4" /> {t('marketing.landing.new')}
|
|
149
|
+
</Button>
|
|
150
|
+
</PermissionGate>
|
|
38
151
|
</div>
|
|
39
152
|
|
|
40
153
|
<div className="grid grid-cols-3 gap-4">
|
|
41
|
-
<KpiCard icon={MousePointerClick} label={t('marketing.metric.totalVisits')} value={formatNumber(totalVisits)} />
|
|
42
154
|
<KpiCard icon={Layers} label={t('marketing.metric.pages')} value={String(pages.length)} />
|
|
43
|
-
<KpiCard
|
|
155
|
+
<KpiCard
|
|
156
|
+
icon={MousePointerClick}
|
|
157
|
+
label={t('marketing.metric.totalVisits')}
|
|
158
|
+
value={measured.length > 0 ? formatNumber(totalVisits) : '—'}
|
|
159
|
+
/>
|
|
160
|
+
<KpiCard icon={Users} label={t('marketing.metric.leads')} value={formatNumber(totalLeads)} />
|
|
44
161
|
</div>
|
|
45
162
|
|
|
46
|
-
<DataTable columns={columns} data={pages} emptyMessage={t('marketing.landing.
|
|
163
|
+
<DataTable columns={columns} data={pages} emptyMessage={t('marketing.landing.empty')} />
|
|
164
|
+
|
|
165
|
+
{measured.length < pages.length ? (
|
|
166
|
+
<p className="text-xs text-muted-foreground">{t('marketing.landing.visitsPending')}</p>
|
|
167
|
+
) : null}
|
|
168
|
+
|
|
169
|
+
<LandingPageComposer open={composerOpen} page={editing} onClose={() => { setComposerOpen(false); setEditing(null) }} />
|
|
47
170
|
</div>
|
|
48
171
|
)
|
|
49
172
|
}
|