@fayz-ai/plugin-marketing 0.8.1 → 0.9.0-next.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/dist/MarketingPage.d.ts.map +1 -1
- package/dist/data/capability.test.d.ts +2 -0
- package/dist/data/capability.test.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +10 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/tables.d.ts +7 -0
- package/dist/data/tables.d.ts.map +1 -1
- package/dist/data/types.d.ts +2 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +746 -61
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/registries.d.ts +3 -3
- package/dist/registries.d.ts.map +1 -1
- package/dist/schema/index.d.ts +354 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema.cjs +30 -0
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.js +30 -2
- package/dist/schema.js.map +1 -1
- package/dist/store.d.ts +2 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/views/CampaignsView.d.ts.map +1 -1
- package/dist/views/blog/BlogEntityForm.d.ts.map +1 -1
- package/dist/views/content/ContentView.d.ts.map +1 -1
- package/package.json +16 -12
- package/src/MarketingPage.tsx +5 -1
- package/src/data/capability.test.ts +92 -0
- package/src/data/supabase.ts +401 -0
- package/src/data/tables.ts +9 -0
- package/src/data/types.ts +2 -0
- package/src/index.ts +138 -10
- package/src/migrations/005_analytics_base.sql +60 -0
- package/src/migrations/006_analytics_views.sql +68 -0
- package/src/migrations/007_agent_rpcs.sql +119 -0
- package/src/migrations/index.ts +257 -1
- package/src/registries.ts +18 -6
- package/src/schema/index.ts +38 -1
- package/src/store.ts +21 -8
- package/src/views/CampaignsView.tsx +4 -1
- package/src/views/blog/BlogEntityForm.tsx +11 -2
- package/src/views/content/ContentView.tsx +8 -1
- package/dist/index.cjs +0 -4551
- package/dist/index.cjs.map +0 -1
package/dist/index.cjs
DELETED
|
@@ -1,4551 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var React8 = require('react');
|
|
4
|
-
var core = require('@fayz-ai/core');
|
|
5
|
-
var saas = require('@fayz-ai/saas');
|
|
6
|
-
var ui = require('@fayz-ai/ui');
|
|
7
|
-
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
-
var lucideReact = require('lucide-react');
|
|
9
|
-
var vanilla = require('zustand/vanilla');
|
|
10
|
-
|
|
11
|
-
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
-
|
|
13
|
-
var React8__default = /*#__PURE__*/_interopDefault(React8);
|
|
14
|
-
|
|
15
|
-
// src/index.ts
|
|
16
|
-
var ctx = saas.createPluginContext("MarketingPage");
|
|
17
|
-
var MarketingContextProvider = ctx.ContextProvider;
|
|
18
|
-
var useMarketingConfig = ctx.useConfig;
|
|
19
|
-
var useMarketingProvider = ctx.useProvider;
|
|
20
|
-
var useMarketingStore = ctx.useStore;
|
|
21
|
-
function useChannelLookup() {
|
|
22
|
-
const { channels } = useMarketingConfig();
|
|
23
|
-
return React8__default.default.useMemo(() => new Map(channels.map((c) => [c.id, c])), [channels]);
|
|
24
|
-
}
|
|
25
|
-
function OverviewView() {
|
|
26
|
-
const setRange = useMarketingStore((s) => s.setRange);
|
|
27
|
-
const range = useMarketingStore((s) => s.range);
|
|
28
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
29
|
-
ui.DashboardCanvas,
|
|
30
|
-
{
|
|
31
|
-
surface: "plugin-home",
|
|
32
|
-
domain: "marketing",
|
|
33
|
-
showHeader: false,
|
|
34
|
-
className: "mx-auto max-w-6xl space-y-6",
|
|
35
|
-
range: { default: range, onChange: (r) => void setRange(r) }
|
|
36
|
-
}
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// src/format.ts
|
|
41
|
-
var DEFAULT_CURRENCY = { code: "USD", locale: "en-US", symbol: "$" };
|
|
42
|
-
function formatCurrency(value, currency) {
|
|
43
|
-
try {
|
|
44
|
-
return new Intl.NumberFormat(currency.locale, {
|
|
45
|
-
style: "currency",
|
|
46
|
-
currency: currency.code,
|
|
47
|
-
maximumFractionDigits: value >= 1e3 ? 0 : 2
|
|
48
|
-
}).format(value);
|
|
49
|
-
} catch {
|
|
50
|
-
return `${currency.symbol}${Math.round(value).toLocaleString()}`;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
function formatNumber(value) {
|
|
54
|
-
return Math.round(value).toLocaleString();
|
|
55
|
-
}
|
|
56
|
-
function formatPercent(value, digits = 1) {
|
|
57
|
-
return `${value.toFixed(digits)}%`;
|
|
58
|
-
}
|
|
59
|
-
function formatCompact(value) {
|
|
60
|
-
if (value >= 1e3) return `${(value / 1e3).toFixed(value >= 1e4 ? 0 : 1)}k`;
|
|
61
|
-
return String(Math.round(value));
|
|
62
|
-
}
|
|
63
|
-
function trendDelta(current, previous) {
|
|
64
|
-
if (previous === 0) return { pct: current > 0 ? 100 : 0, dir: current > 0 ? "up" : "neutral" };
|
|
65
|
-
const pct = (current - previous) / previous * 100;
|
|
66
|
-
return { pct: Math.abs(pct), dir: pct > 0.5 ? "up" : pct < -0.5 ? "down" : "neutral" };
|
|
67
|
-
}
|
|
68
|
-
var ICONS = {
|
|
69
|
-
Search: lucideReact.Search,
|
|
70
|
-
Megaphone: lucideReact.Megaphone,
|
|
71
|
-
Globe: lucideReact.Globe,
|
|
72
|
-
Mail: lucideReact.Mail,
|
|
73
|
-
Users: lucideReact.Users,
|
|
74
|
-
MousePointerClick: lucideReact.MousePointerClick,
|
|
75
|
-
Instagram: lucideReact.Instagram,
|
|
76
|
-
MessageCircle: lucideReact.MessageCircle,
|
|
77
|
-
DoorOpen: lucideReact.DoorOpen,
|
|
78
|
-
Utensils: lucideReact.Utensils,
|
|
79
|
-
Bike: lucideReact.Bike
|
|
80
|
-
};
|
|
81
|
-
function ChannelIcon({ name, className }) {
|
|
82
|
-
const Icon = ICONS[name] ?? lucideReact.Link2;
|
|
83
|
-
return /* @__PURE__ */ jsxRuntime.jsx(Icon, { className });
|
|
84
|
-
}
|
|
85
|
-
var PLATFORM_ICONS = {
|
|
86
|
-
instagram: lucideReact.Instagram,
|
|
87
|
-
tiktok: lucideReact.Music2,
|
|
88
|
-
youtube: lucideReact.Youtube,
|
|
89
|
-
facebook: lucideReact.Facebook,
|
|
90
|
-
linkedin: lucideReact.Linkedin,
|
|
91
|
-
x: lucideReact.Twitter,
|
|
92
|
-
pinterest: lucideReact.Pin,
|
|
93
|
-
threads: lucideReact.AtSign
|
|
94
|
-
};
|
|
95
|
-
function PlatformIcon({ platform, className }) {
|
|
96
|
-
const Icon = PLATFORM_ICONS[platform] ?? lucideReact.Link2;
|
|
97
|
-
return /* @__PURE__ */ jsxRuntime.jsx(Icon, { className });
|
|
98
|
-
}
|
|
99
|
-
function KpiCard({
|
|
100
|
-
icon: Icon,
|
|
101
|
-
label,
|
|
102
|
-
value,
|
|
103
|
-
sub,
|
|
104
|
-
current,
|
|
105
|
-
previous,
|
|
106
|
-
invertTrend
|
|
107
|
-
}) {
|
|
108
|
-
const trend = current != null && previous != null ? trendDelta(current, previous) : null;
|
|
109
|
-
const good = trend ? invertTrend ? trend.dir === "down" : trend.dir === "up" : false;
|
|
110
|
-
const bad = trend ? invertTrend ? trend.dir === "up" : trend.dir === "down" : false;
|
|
111
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-card border border-border bg-card p-4", children: [
|
|
112
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [
|
|
113
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-4 w-4" }),
|
|
114
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium", children: label })
|
|
115
|
-
] }),
|
|
116
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex items-end justify-between gap-2", children: [
|
|
117
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-2xl font-semibold text-foreground", children: value }),
|
|
118
|
-
trend && trend.dir !== "neutral" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: ui.cn(
|
|
119
|
-
"mb-0.5 inline-flex items-center gap-0.5 text-xs font-medium",
|
|
120
|
-
good && "text-emerald-600",
|
|
121
|
-
bad && "text-rose-600",
|
|
122
|
-
!good && !bad && "text-muted-foreground"
|
|
123
|
-
), children: [
|
|
124
|
-
trend.dir === "up" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUpRight, { className: "h-3 w-3" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowDownRight, { className: "h-3 w-3" }),
|
|
125
|
-
trend.pct.toFixed(0),
|
|
126
|
-
"%"
|
|
127
|
-
] })
|
|
128
|
-
] }),
|
|
129
|
-
sub && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: sub })
|
|
130
|
-
] });
|
|
131
|
-
}
|
|
132
|
-
var RANGES = ["7d", "30d", "90d"];
|
|
133
|
-
function RangeTabs() {
|
|
134
|
-
const range = useMarketingStore((s) => s.range);
|
|
135
|
-
const setRange = useMarketingStore((s) => s.setRange);
|
|
136
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
137
|
-
ui.SegmentedControl,
|
|
138
|
-
{
|
|
139
|
-
options: RANGES,
|
|
140
|
-
value: range,
|
|
141
|
-
onChange: (r) => void setRange(r),
|
|
142
|
-
"aria-label": "Time range"
|
|
143
|
-
}
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
var STATUS_STYLE = {
|
|
147
|
-
active: "bg-emerald-100 text-emerald-700",
|
|
148
|
-
paused: "bg-amber-100 text-amber-700",
|
|
149
|
-
ended: "bg-muted text-muted-foreground",
|
|
150
|
-
draft: "bg-slate-100 text-slate-600"
|
|
151
|
-
};
|
|
152
|
-
function StatusBadge({ status }) {
|
|
153
|
-
const t = core.useTranslation();
|
|
154
|
-
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.cn("rounded-full px-2 py-0.5 text-xs font-medium capitalize", STATUS_STYLE[status]), children: t(`marketing.status.${status}`) });
|
|
155
|
-
}
|
|
156
|
-
function ChannelCell({ channelId }) {
|
|
157
|
-
const lookup = useChannelLookup();
|
|
158
|
-
const ch = lookup.get(channelId);
|
|
159
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1.5 text-foreground", children: [
|
|
160
|
-
/* @__PURE__ */ jsxRuntime.jsx(ChannelIcon, { name: ch?.icon ?? "Link2", className: "h-3.5 w-3.5 text-muted-foreground" }),
|
|
161
|
-
ch?.label ?? channelId
|
|
162
|
-
] });
|
|
163
|
-
}
|
|
164
|
-
function ProportionBar({ value, max, color, label, right }) {
|
|
165
|
-
const pct = max > 0 ? Math.max(value / max * 100, 2) : 0;
|
|
166
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
167
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-32 shrink-0 text-sm text-muted-foreground", children: label }),
|
|
168
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-7 flex-1 overflow-hidden rounded-md bg-muted/40", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
169
|
-
"div",
|
|
170
|
-
{
|
|
171
|
-
className: "flex h-full items-center rounded-md px-2 text-xs font-semibold",
|
|
172
|
-
style: { width: `${pct}%`, backgroundColor: color + "26", color },
|
|
173
|
-
children: right
|
|
174
|
-
}
|
|
175
|
-
) })
|
|
176
|
-
] });
|
|
177
|
-
}
|
|
178
|
-
function ChannelsView({ onOpen }) {
|
|
179
|
-
const t = core.useTranslation();
|
|
180
|
-
const { conversion, currency } = useMarketingConfig();
|
|
181
|
-
const channels = useMarketingStore((s) => s.channels);
|
|
182
|
-
const sorted = [...channels].sort((a, b) => b.conversions - a.conversions);
|
|
183
|
-
const columns = React8__default.default.useMemo(() => [
|
|
184
|
-
{ accessorKey: "channelId", header: t("marketing.col.channel"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx(ChannelCell, { channelId: getValue() }) },
|
|
185
|
-
{ accessorKey: "reach", header: t("marketing.col.reach"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
186
|
-
{ accessorKey: "leads", header: t("marketing.col.leads"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
187
|
-
{ accessorKey: "conversions", header: conversion.labelPlural, cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: formatNumber(getValue()) }) },
|
|
188
|
-
{ accessorKey: "cvr", header: t("marketing.col.cvr"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatPercent(getValue()) }) },
|
|
189
|
-
{ accessorKey: "spend", header: t("marketing.col.spend"), cell: ({ getValue }) => {
|
|
190
|
-
const v = getValue();
|
|
191
|
-
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: v > 0 ? formatCurrency(v, currency) : "\u2014" });
|
|
192
|
-
} },
|
|
193
|
-
{ accessorKey: "cpa", header: t("marketing.col.cpa"), cell: ({ getValue }) => {
|
|
194
|
-
const v = getValue();
|
|
195
|
-
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: v > 0 ? formatCurrency(v, currency) : "\u2014" });
|
|
196
|
-
} },
|
|
197
|
-
{ accessorKey: "value", header: conversion.valueLabel, cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatCurrency(getValue(), currency) }) },
|
|
198
|
-
{ id: "open", header: "", enableSorting: false, cell: () => /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4 text-muted-foreground" }) }
|
|
199
|
-
], [t, conversion.labelPlural, conversion.valueLabel, currency]);
|
|
200
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
|
|
201
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
202
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.channels.subtitle") }),
|
|
203
|
-
/* @__PURE__ */ jsxRuntime.jsx(RangeTabs, {})
|
|
204
|
-
] }),
|
|
205
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
206
|
-
ui.DataTable,
|
|
207
|
-
{
|
|
208
|
-
columns,
|
|
209
|
-
data: sorted,
|
|
210
|
-
onRowClick: (row) => onOpen(row.channelId),
|
|
211
|
-
emptyMessage: t("marketing.channels.subtitle")
|
|
212
|
-
}
|
|
213
|
-
)
|
|
214
|
-
] });
|
|
215
|
-
}
|
|
216
|
-
function ChannelDetailView({ channelId, onBack }) {
|
|
217
|
-
const t = core.useTranslation();
|
|
218
|
-
const { conversion, currency, labels } = useMarketingConfig();
|
|
219
|
-
const range = useMarketingStore((s) => s.range);
|
|
220
|
-
const channels = useMarketingStore((s) => s.channels);
|
|
221
|
-
const lookup = useChannelLookup();
|
|
222
|
-
const provider2 = useMarketingProvider();
|
|
223
|
-
const [campaigns, setCampaigns] = React8__default.default.useState([]);
|
|
224
|
-
React8__default.default.useEffect(() => {
|
|
225
|
-
void provider2.listCampaigns({ range, channelId }).then(setCampaigns);
|
|
226
|
-
}, [provider2, range, channelId]);
|
|
227
|
-
const perf = channels.find((c) => c.channelId === channelId);
|
|
228
|
-
const channel = lookup.get(channelId);
|
|
229
|
-
const columns = React8__default.default.useMemo(() => [
|
|
230
|
-
{ accessorKey: "name", header: t("marketing.col.campaign"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
|
|
231
|
-
{ accessorKey: "conversions", header: conversion.labelPlural, cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
232
|
-
{ id: "cvr", accessorFn: (c) => c.reach > 0 ? c.conversions / c.reach * 100 : 0, header: t("marketing.col.cvr"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatPercent(getValue()) }) },
|
|
233
|
-
{ accessorKey: "spend", header: t("marketing.col.spend"), cell: ({ getValue }) => {
|
|
234
|
-
const v = getValue();
|
|
235
|
-
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: v > 0 ? formatCurrency(v, currency) : "\u2014" });
|
|
236
|
-
} },
|
|
237
|
-
{ accessorKey: "status", header: t("marketing.col.status"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: getValue() }) }
|
|
238
|
-
], [t, conversion.labelPlural, currency]);
|
|
239
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl space-y-6", children: [
|
|
240
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
241
|
-
ui.SubpageHeader,
|
|
242
|
-
{
|
|
243
|
-
title: channel?.label ?? channelId,
|
|
244
|
-
subtitle: channel?.kind,
|
|
245
|
-
onBack,
|
|
246
|
-
parentLabel: labels.channels
|
|
247
|
-
}
|
|
248
|
-
),
|
|
249
|
-
perf && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4 lg:grid-cols-4", children: [
|
|
250
|
-
/* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.Users, label: t("marketing.metric.reach"), value: formatNumber(perf.reach) }),
|
|
251
|
-
/* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.Target, label: conversion.labelPlural, value: formatNumber(perf.conversions) }),
|
|
252
|
-
/* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.Percent, label: t("marketing.metric.cvr"), value: formatPercent(perf.cvr) }),
|
|
253
|
-
/* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.DollarSign, label: t("marketing.col.cpa"), value: perf.cpa > 0 ? formatCurrency(perf.cpa, currency) : "\u2014", sub: perf.spend > 0 ? `${formatCurrency(perf.spend, currency)} ${t("marketing.metric.spend")}` : "\u2014" })
|
|
254
|
-
] }),
|
|
255
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
|
|
256
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-sm font-semibold text-foreground", children: t("marketing.channels.campaignsOn") }),
|
|
257
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns, data: campaigns, emptyMessage: t("marketing.channels.noCampaigns") })
|
|
258
|
-
] })
|
|
259
|
-
] });
|
|
260
|
-
}
|
|
261
|
-
var STATUSES = ["draft", "active", "paused"];
|
|
262
|
-
function CampaignComposer({ open, onClose }) {
|
|
263
|
-
const t = core.useTranslation();
|
|
264
|
-
const { channels } = useMarketingConfig();
|
|
265
|
-
const saveCampaign = useMarketingStore((s) => s.saveCampaign);
|
|
266
|
-
const [name, setName] = React8__default.default.useState("");
|
|
267
|
-
const [channelId, setChannelId] = React8__default.default.useState(channels[0]?.id ?? "");
|
|
268
|
-
const [status, setStatus] = React8__default.default.useState("active");
|
|
269
|
-
const [spend, setSpend] = React8__default.default.useState("");
|
|
270
|
-
const [saving, setSaving] = React8__default.default.useState(false);
|
|
271
|
-
React8__default.default.useEffect(() => {
|
|
272
|
-
if (open) {
|
|
273
|
-
setName("");
|
|
274
|
-
setChannelId(channels[0]?.id ?? "");
|
|
275
|
-
setStatus("active");
|
|
276
|
-
setSpend("");
|
|
277
|
-
}
|
|
278
|
-
}, [open, channels]);
|
|
279
|
-
async function handleSave() {
|
|
280
|
-
if (!name.trim() || !channelId) return;
|
|
281
|
-
setSaving(true);
|
|
282
|
-
await saveCampaign({
|
|
283
|
-
name: name.trim(),
|
|
284
|
-
channelId,
|
|
285
|
-
status,
|
|
286
|
-
start: (/* @__PURE__ */ new Date()).toISOString(),
|
|
287
|
-
spend: Number(spend) || 0
|
|
288
|
-
});
|
|
289
|
-
setSaving(false);
|
|
290
|
-
onClose();
|
|
291
|
-
}
|
|
292
|
-
return /* @__PURE__ */ jsxRuntime.jsx(ui.Modal, { open, onOpenChange: (o) => {
|
|
293
|
-
if (!o) onClose();
|
|
294
|
-
}, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.ModalContent, { size: "md", children: [
|
|
295
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.ModalHeader, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.ModalTitle, { children: t("marketing.composer.title") }) }),
|
|
296
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.ModalBody, { className: "space-y-4", children: [
|
|
297
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "block space-y-1.5", children: [
|
|
298
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.name") }),
|
|
299
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: name, onChange: (e) => setName(e.target.value), placeholder: t("marketing.composer.namePlaceholder") })
|
|
300
|
-
] }),
|
|
301
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "block space-y-1.5", children: [
|
|
302
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.channel") }),
|
|
303
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: channelId, onValueChange: setChannelId, children: [
|
|
304
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectTrigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.SelectValue, { placeholder: t("marketing.composer.channelPlaceholder") }) }),
|
|
305
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectContent, { children: channels.map((c) => /* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: c.id, children: c.label }, c.id)) })
|
|
306
|
-
] })
|
|
307
|
-
] }),
|
|
308
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
309
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "block space-y-1.5", children: [
|
|
310
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.status") }),
|
|
311
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: status, onValueChange: (v) => setStatus(v), children: [
|
|
312
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectTrigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.SelectValue, {}) }),
|
|
313
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectContent, { children: STATUSES.map((s) => /* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: s, className: "capitalize", children: t(`marketing.status.${s}`) }, s)) })
|
|
314
|
-
] })
|
|
315
|
-
] }),
|
|
316
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "block space-y-1.5", children: [
|
|
317
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.budget") }),
|
|
318
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { type: "number", min: 0, value: spend, onChange: (e) => setSpend(e.target.value), placeholder: "0" })
|
|
319
|
-
] })
|
|
320
|
-
] })
|
|
321
|
-
] }),
|
|
322
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.ModalFooter, { children: [
|
|
323
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", onClick: onClose, children: t("marketing.composer.cancel") }),
|
|
324
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: handleSave, disabled: saving || !name.trim(), children: t("marketing.composer.create") })
|
|
325
|
-
] })
|
|
326
|
-
] }) });
|
|
327
|
-
}
|
|
328
|
-
function CampaignsView() {
|
|
329
|
-
const t = core.useTranslation();
|
|
330
|
-
const { conversion, currency } = useMarketingConfig();
|
|
331
|
-
const campaigns = useMarketingStore((s) => s.campaigns);
|
|
332
|
-
const deleteCampaign = useMarketingStore((s) => s.deleteCampaign);
|
|
333
|
-
const [composerOpen, setComposerOpen] = React8__default.default.useState(false);
|
|
334
|
-
const columns = React8__default.default.useMemo(() => [
|
|
335
|
-
{ accessorKey: "name", header: t("marketing.col.campaign"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
|
|
336
|
-
{ accessorKey: "channelId", header: t("marketing.col.channel"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx(ChannelCell, { channelId: getValue() }) },
|
|
337
|
-
{ accessorKey: "reach", header: t("marketing.col.reach"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
338
|
-
{ accessorKey: "conversions", header: conversion.labelPlural, cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: formatNumber(getValue()) }) },
|
|
339
|
-
{ id: "cvr", accessorFn: (c) => c.reach > 0 ? c.conversions / c.reach * 100 : 0, header: t("marketing.col.cvr"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatPercent(getValue()) }) },
|
|
340
|
-
{ accessorKey: "spend", header: t("marketing.col.spend"), cell: ({ getValue }) => {
|
|
341
|
-
const v = getValue();
|
|
342
|
-
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: v > 0 ? formatCurrency(v, currency) : "\u2014" });
|
|
343
|
-
} },
|
|
344
|
-
{ accessorKey: "status", header: t("marketing.col.status"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: getValue() }) },
|
|
345
|
-
{
|
|
346
|
-
id: "actions",
|
|
347
|
-
header: "",
|
|
348
|
-
enableSorting: false,
|
|
349
|
-
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
350
|
-
"button",
|
|
351
|
-
{
|
|
352
|
-
onClick: (e) => {
|
|
353
|
-
e.stopPropagation();
|
|
354
|
-
void deleteCampaign(row.original.id);
|
|
355
|
-
},
|
|
356
|
-
className: "rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive",
|
|
357
|
-
title: t("marketing.campaigns.delete"),
|
|
358
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-4 w-4" })
|
|
359
|
-
}
|
|
360
|
-
)
|
|
361
|
-
}
|
|
362
|
-
], [t, conversion.labelPlural, currency, deleteCampaign]);
|
|
363
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
|
|
364
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
365
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.campaigns.subtitle") }),
|
|
366
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
367
|
-
/* @__PURE__ */ jsxRuntime.jsx(RangeTabs, {}),
|
|
368
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { onClick: () => setComposerOpen(true), children: [
|
|
369
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "mr-1.5 h-4 w-4" }),
|
|
370
|
-
" ",
|
|
371
|
-
t("marketing.campaigns.new")
|
|
372
|
-
] })
|
|
373
|
-
] })
|
|
374
|
-
] }),
|
|
375
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns, data: campaigns, emptyMessage: t("marketing.campaigns.empty") }),
|
|
376
|
-
/* @__PURE__ */ jsxRuntime.jsx(CampaignComposer, { open: composerOpen, onClose: () => setComposerOpen(false) })
|
|
377
|
-
] });
|
|
378
|
-
}
|
|
379
|
-
function FunnelView() {
|
|
380
|
-
const t = core.useTranslation();
|
|
381
|
-
const { conversion } = useMarketingConfig();
|
|
382
|
-
const funnel = useMarketingStore((s) => s.funnel);
|
|
383
|
-
const max = Math.max(...funnel.map((s) => s.count), 1);
|
|
384
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-4xl space-y-4", children: [
|
|
385
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
386
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
387
|
-
t("marketing.funnel.subtitlePrefix"),
|
|
388
|
-
" ",
|
|
389
|
-
conversion.label.toLowerCase()
|
|
390
|
-
] }),
|
|
391
|
-
/* @__PURE__ */ jsxRuntime.jsx(RangeTabs, {})
|
|
392
|
-
] }),
|
|
393
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-card border border-border bg-card p-5", children: [
|
|
394
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-3", children: funnel.map((stage, i) => {
|
|
395
|
-
const pct = max > 0 ? Math.max(stage.count / max * 100, 4) : 0;
|
|
396
|
-
const prev = funnel[i - 1];
|
|
397
|
-
const stepRate = prev && prev.count > 0 ? stage.count / prev.count * 100 : null;
|
|
398
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
399
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-1 flex items-center justify-between text-sm", children: [
|
|
400
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: stage.label }),
|
|
401
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
|
|
402
|
-
formatNumber(stage.count),
|
|
403
|
-
stepRate != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-2 text-xs", children: [
|
|
404
|
-
"(",
|
|
405
|
-
formatPercent(stepRate, 0),
|
|
406
|
-
" ",
|
|
407
|
-
t("marketing.funnel.ofPrev"),
|
|
408
|
-
")"
|
|
409
|
-
] })
|
|
410
|
-
] })
|
|
411
|
-
] }),
|
|
412
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-9 overflow-hidden rounded-md bg-muted/40", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full rounded-md", style: { width: `${pct}%`, backgroundColor: stage.color + "33", borderLeft: `3px solid ${stage.color}` } }) })
|
|
413
|
-
] }, stage.id);
|
|
414
|
-
}) }),
|
|
415
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-5 border-t border-border pt-4 text-sm", children: [
|
|
416
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
|
|
417
|
-
t("marketing.funnel.overall"),
|
|
418
|
-
": "
|
|
419
|
-
] }),
|
|
420
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold text-foreground", children: funnel.length > 1 && funnel[0].count > 0 ? formatPercent(funnel[funnel.length - 1].count / funnel[0].count * 100) : "\u2014" })
|
|
421
|
-
] })
|
|
422
|
-
] })
|
|
423
|
-
] });
|
|
424
|
-
}
|
|
425
|
-
var TYPE_STYLE = {
|
|
426
|
-
Funnel: "bg-indigo-100 text-indigo-700",
|
|
427
|
-
"Landing page": "bg-pink-100 text-pink-700",
|
|
428
|
-
Website: "bg-sky-100 text-sky-700"
|
|
429
|
-
};
|
|
430
|
-
function LandingPagesView() {
|
|
431
|
-
const t = core.useTranslation();
|
|
432
|
-
const pages = useMarketingStore((s) => s.landingPages);
|
|
433
|
-
const totalVisits = pages.reduce((s, p) => s + p.visits, 0);
|
|
434
|
-
const totalConv = pages.reduce((s, p) => s + p.conversions, 0);
|
|
435
|
-
const avgCvr = totalVisits > 0 ? totalConv / totalVisits * 100 : 0;
|
|
436
|
-
const columns = React8__default.default.useMemo(() => [
|
|
437
|
-
{ accessorKey: "name", header: t("marketing.col.page"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
|
|
438
|
-
{ accessorKey: "type", header: t("marketing.col.type"), cell: ({ getValue }) => {
|
|
439
|
-
const v = getValue();
|
|
440
|
-
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `rounded-full px-2 py-0.5 text-xs font-medium ${TYPE_STYLE[v] ?? "bg-muted text-muted-foreground"}`, children: v });
|
|
441
|
-
} },
|
|
442
|
-
{ accessorKey: "visits", header: t("marketing.col.visits"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
443
|
-
{ accessorKey: "conversions", header: t("marketing.col.conversions"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: formatNumber(getValue()) }) },
|
|
444
|
-
{ accessorKey: "cvr", header: t("marketing.col.cvr"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatPercent(getValue()) }) }
|
|
445
|
-
], [t]);
|
|
446
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
|
|
447
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
448
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.landing.subtitle") }),
|
|
449
|
-
/* @__PURE__ */ jsxRuntime.jsx(RangeTabs, {})
|
|
450
|
-
] }),
|
|
451
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-3 gap-4", children: [
|
|
452
|
-
/* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.MousePointerClick, label: t("marketing.metric.totalVisits"), value: formatNumber(totalVisits) }),
|
|
453
|
-
/* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.Layers, label: t("marketing.metric.pages"), value: String(pages.length) }),
|
|
454
|
-
/* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.Percent, label: t("marketing.metric.avgConversion"), value: formatPercent(avgCvr) })
|
|
455
|
-
] }),
|
|
456
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns, data: pages, emptyMessage: t("marketing.landing.subtitle") })
|
|
457
|
-
] });
|
|
458
|
-
}
|
|
459
|
-
var ctx2 = saas.createPluginContext("ContentPlanner");
|
|
460
|
-
var ContentPlannerContextProvider = ctx2.ContextProvider;
|
|
461
|
-
ctx2.useConfig;
|
|
462
|
-
var useContentPlannerProvider = ctx2.useProvider;
|
|
463
|
-
var useContentPlannerStore = ctx2.useStore;
|
|
464
|
-
|
|
465
|
-
// src/data/contentTypes.ts
|
|
466
|
-
var PLATFORMS = [
|
|
467
|
-
"instagram",
|
|
468
|
-
"tiktok",
|
|
469
|
-
"youtube",
|
|
470
|
-
"facebook",
|
|
471
|
-
"linkedin",
|
|
472
|
-
"x",
|
|
473
|
-
"pinterest",
|
|
474
|
-
"threads"
|
|
475
|
-
];
|
|
476
|
-
var POST_STATUS_ORDER = ["idea", "script", "recording", "editing", "scheduled", "published"];
|
|
477
|
-
var POST_FORMATS = ["reel", "static", "carousel", "story", "video", "live", "article"];
|
|
478
|
-
var FORMAT_STYLE = {
|
|
479
|
-
reel: "bg-violet-100 text-violet-700",
|
|
480
|
-
static: "bg-sky-100 text-sky-700",
|
|
481
|
-
carousel: "bg-amber-100 text-amber-700",
|
|
482
|
-
story: "bg-pink-100 text-pink-700",
|
|
483
|
-
video: "bg-red-100 text-red-700",
|
|
484
|
-
live: "bg-rose-100 text-rose-700",
|
|
485
|
-
article: "bg-teal-100 text-teal-700"
|
|
486
|
-
};
|
|
487
|
-
var POST_STATUS_COLOR = {
|
|
488
|
-
idea: "#6b7280",
|
|
489
|
-
script: "#6366f1",
|
|
490
|
-
recording: "#f59e0b",
|
|
491
|
-
editing: "#f97316",
|
|
492
|
-
scheduled: "#3b82f6",
|
|
493
|
-
published: "#10b981"
|
|
494
|
-
};
|
|
495
|
-
function StatusDot({ status, className }) {
|
|
496
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
497
|
-
"span",
|
|
498
|
-
{
|
|
499
|
-
className: ui.cn("h-2 w-2 shrink-0 rounded-full", className),
|
|
500
|
-
style: { backgroundColor: POST_STATUS_COLOR[status] }
|
|
501
|
-
}
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
var STATUS_STYLE2 = {
|
|
505
|
-
idea: "bg-slate-100 text-slate-600",
|
|
506
|
-
script: "bg-indigo-100 text-indigo-700",
|
|
507
|
-
recording: "bg-amber-100 text-amber-700",
|
|
508
|
-
editing: "bg-orange-100 text-orange-700",
|
|
509
|
-
scheduled: "bg-sky-100 text-sky-700",
|
|
510
|
-
published: "bg-emerald-100 text-emerald-700"
|
|
511
|
-
};
|
|
512
|
-
function FormatBadge({ format }) {
|
|
513
|
-
const t = core.useTranslation();
|
|
514
|
-
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.cn("rounded-full px-2 py-0.5 text-xs font-medium", FORMAT_STYLE[format]), children: t(`marketing.content.format.${format}`) });
|
|
515
|
-
}
|
|
516
|
-
function PostStatusBadge({ status }) {
|
|
517
|
-
const t = core.useTranslation();
|
|
518
|
-
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.cn("rounded-full px-2 py-0.5 text-xs font-medium", STATUS_STYLE2[status]), children: t(`marketing.content.status.${status}`) });
|
|
519
|
-
}
|
|
520
|
-
function PlatformBadges({ platforms, className }) {
|
|
521
|
-
if (platforms.length === 0) return null;
|
|
522
|
-
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.cn("inline-flex items-center gap-1 text-muted-foreground", className), children: platforms.map((p) => /* @__PURE__ */ jsxRuntime.jsx(PlatformIcon, { platform: p, className: "h-3.5 w-3.5" }, p)) });
|
|
523
|
-
}
|
|
524
|
-
function PlatformPicker({ value, onChange }) {
|
|
525
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap items-center gap-1.5", children: PLATFORMS.map((platform) => {
|
|
526
|
-
const selected = value.includes(platform);
|
|
527
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
528
|
-
"button",
|
|
529
|
-
{
|
|
530
|
-
type: "button",
|
|
531
|
-
"aria-pressed": selected,
|
|
532
|
-
onClick: () => onChange(selected ? value.filter((p) => p !== platform) : [...value, platform]),
|
|
533
|
-
className: ui.cn(
|
|
534
|
-
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium capitalize transition-colors",
|
|
535
|
-
selected ? "border-primary bg-primary/10 text-foreground" : "border-border text-muted-foreground hover:bg-muted/40"
|
|
536
|
-
),
|
|
537
|
-
children: [
|
|
538
|
-
/* @__PURE__ */ jsxRuntime.jsx(PlatformIcon, { platform, className: "h-3.5 w-3.5" }),
|
|
539
|
-
platform
|
|
540
|
-
]
|
|
541
|
-
},
|
|
542
|
-
platform
|
|
543
|
-
);
|
|
544
|
-
}) });
|
|
545
|
-
}
|
|
546
|
-
function toDraft(plan) {
|
|
547
|
-
return {
|
|
548
|
-
name: plan.name,
|
|
549
|
-
objective: plan.objective ?? "",
|
|
550
|
-
tone: plan.tone ?? "",
|
|
551
|
-
pillars: plan.pillars,
|
|
552
|
-
formats: plan.formats,
|
|
553
|
-
weeklyFrequency: plan.weeklyFrequency,
|
|
554
|
-
weeksCount: plan.weeksCount,
|
|
555
|
-
briefMd: plan.briefMd
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
function Field({ label, children }) {
|
|
559
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
560
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: label }),
|
|
561
|
-
children
|
|
562
|
-
] });
|
|
563
|
-
}
|
|
564
|
-
function useBriefDraft() {
|
|
565
|
-
const plan = useContentPlannerStore((s) => s.activePlan);
|
|
566
|
-
const savePlan = useContentPlannerStore((s) => s.savePlan);
|
|
567
|
-
const [draft, setDraft] = React8__default.default.useState(plan ? toDraft(plan) : null);
|
|
568
|
-
const [saving, setSaving] = React8__default.default.useState(false);
|
|
569
|
-
React8__default.default.useEffect(() => {
|
|
570
|
-
if (plan) setDraft(toDraft(plan));
|
|
571
|
-
}, [plan?.id]);
|
|
572
|
-
const patch = (partial) => setDraft((d) => d ? { ...d, ...partial } : d);
|
|
573
|
-
const dirty = !!plan && !!draft && JSON.stringify(draft) !== JSON.stringify(toDraft(plan));
|
|
574
|
-
const save = async () => {
|
|
575
|
-
if (!plan || !draft) return;
|
|
576
|
-
setSaving(true);
|
|
577
|
-
try {
|
|
578
|
-
await savePlan({
|
|
579
|
-
id: plan.id,
|
|
580
|
-
name: draft.name,
|
|
581
|
-
objective: draft.objective,
|
|
582
|
-
tone: draft.tone,
|
|
583
|
-
pillars: draft.pillars,
|
|
584
|
-
formats: draft.formats,
|
|
585
|
-
weeklyFrequency: draft.weeklyFrequency,
|
|
586
|
-
weeksCount: draft.weeksCount,
|
|
587
|
-
briefMd: draft.briefMd
|
|
588
|
-
});
|
|
589
|
-
} finally {
|
|
590
|
-
setSaving(false);
|
|
591
|
-
}
|
|
592
|
-
};
|
|
593
|
-
return { plan, draft, patch, dirty, saving, save };
|
|
594
|
-
}
|
|
595
|
-
function BriefFields({ draft, patch }) {
|
|
596
|
-
const t = core.useTranslation();
|
|
597
|
-
const [pillarInput, setPillarInput] = React8__default.default.useState("");
|
|
598
|
-
const addPillar = () => {
|
|
599
|
-
const value = pillarInput.trim();
|
|
600
|
-
if (!value || draft.pillars.includes(value)) return;
|
|
601
|
-
patch({ pillars: [...draft.pillars, value] });
|
|
602
|
-
setPillarInput("");
|
|
603
|
-
};
|
|
604
|
-
const toggleFormat = (format) => {
|
|
605
|
-
patch({
|
|
606
|
-
formats: draft.formats.includes(format) ? draft.formats.filter((f) => f !== format) : [...draft.formats, format]
|
|
607
|
-
});
|
|
608
|
-
};
|
|
609
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-5", children: [
|
|
610
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: [
|
|
611
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.planName"), children: /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: draft.name, onChange: (e) => patch({ name: e.target.value }) }) }),
|
|
612
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.objective"), children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
613
|
-
ui.Input,
|
|
614
|
-
{
|
|
615
|
-
value: draft.objective,
|
|
616
|
-
onChange: (e) => patch({ objective: e.target.value }),
|
|
617
|
-
placeholder: t("marketing.content.objectivePlaceholder")
|
|
618
|
-
}
|
|
619
|
-
) }),
|
|
620
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.tone"), children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
621
|
-
ui.Input,
|
|
622
|
-
{
|
|
623
|
-
value: draft.tone,
|
|
624
|
-
onChange: (e) => patch({ tone: e.target.value }),
|
|
625
|
-
placeholder: t("marketing.content.tonePlaceholder")
|
|
626
|
-
}
|
|
627
|
-
) }),
|
|
628
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
629
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.weeklyFrequency"), children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
630
|
-
ui.Input,
|
|
631
|
-
{
|
|
632
|
-
type: "number",
|
|
633
|
-
min: 1,
|
|
634
|
-
max: 14,
|
|
635
|
-
value: draft.weeklyFrequency,
|
|
636
|
-
onChange: (e) => patch({ weeklyFrequency: Math.max(1, Number(e.target.value) || 1) })
|
|
637
|
-
}
|
|
638
|
-
) }),
|
|
639
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.weeksCount"), children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
640
|
-
ui.Input,
|
|
641
|
-
{
|
|
642
|
-
type: "number",
|
|
643
|
-
min: 1,
|
|
644
|
-
max: 52,
|
|
645
|
-
value: draft.weeksCount,
|
|
646
|
-
onChange: (e) => patch({ weeksCount: Math.max(1, Number(e.target.value) || 1) })
|
|
647
|
-
}
|
|
648
|
-
) })
|
|
649
|
-
] })
|
|
650
|
-
] }),
|
|
651
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.pillars"), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
|
|
652
|
-
draft.pillars.map((pillar) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
653
|
-
"span",
|
|
654
|
-
{
|
|
655
|
-
className: "inline-flex items-center gap-1 rounded-full bg-muted px-2.5 py-0.5 text-xs font-medium text-foreground",
|
|
656
|
-
children: [
|
|
657
|
-
pillar,
|
|
658
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
659
|
-
"button",
|
|
660
|
-
{
|
|
661
|
-
type: "button",
|
|
662
|
-
onClick: () => patch({ pillars: draft.pillars.filter((p) => p !== pillar) }),
|
|
663
|
-
className: "text-muted-foreground hover:text-foreground",
|
|
664
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-3 w-3" })
|
|
665
|
-
}
|
|
666
|
-
)
|
|
667
|
-
]
|
|
668
|
-
},
|
|
669
|
-
pillar
|
|
670
|
-
)),
|
|
671
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
672
|
-
ui.Input,
|
|
673
|
-
{
|
|
674
|
-
value: pillarInput,
|
|
675
|
-
onChange: (e) => setPillarInput(e.target.value),
|
|
676
|
-
onKeyDown: (e) => {
|
|
677
|
-
if (e.key === "Enter") {
|
|
678
|
-
e.preventDefault();
|
|
679
|
-
addPillar();
|
|
680
|
-
}
|
|
681
|
-
},
|
|
682
|
-
onBlur: addPillar,
|
|
683
|
-
placeholder: t("marketing.content.pillarPlaceholder"),
|
|
684
|
-
className: "h-7 w-44 text-xs"
|
|
685
|
-
}
|
|
686
|
-
)
|
|
687
|
-
] }) }),
|
|
688
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.formatsLabel"), children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-4", children: POST_FORMATS.map((format) => /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 text-sm text-foreground", children: [
|
|
689
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
690
|
-
ui.Checkbox,
|
|
691
|
-
{
|
|
692
|
-
checked: draft.formats.includes(format),
|
|
693
|
-
onChange: () => toggleFormat(format)
|
|
694
|
-
}
|
|
695
|
-
),
|
|
696
|
-
t(`marketing.content.format.${format}`)
|
|
697
|
-
] }, format)) }) }),
|
|
698
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.briefBody"), children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
699
|
-
ui.MarkdownEditor,
|
|
700
|
-
{
|
|
701
|
-
value: draft.briefMd,
|
|
702
|
-
onChange: (briefMd) => patch({ briefMd }),
|
|
703
|
-
placeholder: t("marketing.content.briefPlaceholder"),
|
|
704
|
-
editLabel: t("marketing.content.edit"),
|
|
705
|
-
previewLabel: t("marketing.content.preview"),
|
|
706
|
-
minRows: 10
|
|
707
|
-
}
|
|
708
|
-
) })
|
|
709
|
-
] });
|
|
710
|
-
}
|
|
711
|
-
function PlanBriefView({ onBack }) {
|
|
712
|
-
const t = core.useTranslation();
|
|
713
|
-
const { plan, draft, patch, dirty, saving, save } = useBriefDraft();
|
|
714
|
-
if (!plan || !draft) {
|
|
715
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto max-w-3xl text-sm text-muted-foreground", children: "\u2026" });
|
|
716
|
-
}
|
|
717
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-3xl space-y-5", children: [
|
|
718
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
719
|
-
ui.SubpageHeader,
|
|
720
|
-
{
|
|
721
|
-
title: t("marketing.content.brief"),
|
|
722
|
-
subtitle: plan.name,
|
|
723
|
-
parentLabel: t("marketing.content.title"),
|
|
724
|
-
onBack,
|
|
725
|
-
actions: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "sm", onClick: () => void save(), disabled: !dirty || saving, children: t("marketing.content.save") })
|
|
726
|
-
}
|
|
727
|
-
),
|
|
728
|
-
/* @__PURE__ */ jsxRuntime.jsx(BriefFields, { draft, patch })
|
|
729
|
-
] });
|
|
730
|
-
}
|
|
731
|
-
function PlanBriefSheet({ open, onClose }) {
|
|
732
|
-
const t = core.useTranslation();
|
|
733
|
-
const { plan, draft, patch, dirty, saving, save } = useBriefDraft();
|
|
734
|
-
return /* @__PURE__ */ jsxRuntime.jsx(ui.Sheet, { open, onOpenChange: (v) => {
|
|
735
|
-
if (!v) onClose();
|
|
736
|
-
}, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.SheetContent, { width: "max-w-2xl", children: [
|
|
737
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.SheetHeader, { children: [
|
|
738
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SheetTitle, { children: t("marketing.content.brief") }),
|
|
739
|
-
plan && /* @__PURE__ */ jsxRuntime.jsx(ui.SheetDescription, { children: plan.name })
|
|
740
|
-
] }),
|
|
741
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SheetBody, { children: draft ? /* @__PURE__ */ jsxRuntime.jsx(BriefFields, { draft, patch }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" }) }),
|
|
742
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.SheetFooter, { children: [
|
|
743
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", onClick: onClose, disabled: saving, children: t("marketing.content.cancel") }),
|
|
744
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: () => void save(), disabled: !dirty || saving, children: t("marketing.content.save") })
|
|
745
|
-
] })
|
|
746
|
-
] }) });
|
|
747
|
-
}
|
|
748
|
-
var DEFAULT_WEEKS = 8;
|
|
749
|
-
var ADD_ACCOUNT_VALUE = "__add-account__";
|
|
750
|
-
var POST_DRAG_MIME = "application/x-mkt-post";
|
|
751
|
-
function PostCard({ post, onOpen, onDelete }) {
|
|
752
|
-
const t = core.useTranslation();
|
|
753
|
-
const [dragging, setDragging] = React8__default.default.useState(false);
|
|
754
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
755
|
-
"div",
|
|
756
|
-
{
|
|
757
|
-
role: "button",
|
|
758
|
-
tabIndex: 0,
|
|
759
|
-
onClick: onOpen,
|
|
760
|
-
onKeyDown: (e) => {
|
|
761
|
-
if (e.key === "Enter") onOpen();
|
|
762
|
-
},
|
|
763
|
-
draggable: true,
|
|
764
|
-
onDragStart: (e) => {
|
|
765
|
-
e.dataTransfer.setData(POST_DRAG_MIME, post.id);
|
|
766
|
-
e.dataTransfer.effectAllowed = "move";
|
|
767
|
-
setDragging(true);
|
|
768
|
-
},
|
|
769
|
-
onDragEnd: () => setDragging(false),
|
|
770
|
-
className: ui.cn(
|
|
771
|
-
"group/post relative w-full cursor-pointer rounded-card border border-border bg-card p-3 pl-7 text-left transition-colors hover:bg-muted/40",
|
|
772
|
-
dragging && "opacity-50"
|
|
773
|
-
),
|
|
774
|
-
children: [
|
|
775
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
776
|
-
lucideReact.GripVertical,
|
|
777
|
-
{
|
|
778
|
-
className: "absolute left-1.5 top-3 h-4 w-4 cursor-grab text-muted-foreground/50 opacity-0 transition-opacity group-hover/post:opacity-100",
|
|
779
|
-
"aria-hidden": true
|
|
780
|
-
}
|
|
781
|
-
),
|
|
782
|
-
onDelete && /* @__PURE__ */ jsxRuntime.jsx(
|
|
783
|
-
"button",
|
|
784
|
-
{
|
|
785
|
-
type: "button",
|
|
786
|
-
"aria-label": t("marketing.content.deletePost"),
|
|
787
|
-
onClick: (e) => {
|
|
788
|
-
e.stopPropagation();
|
|
789
|
-
onDelete();
|
|
790
|
-
},
|
|
791
|
-
className: "absolute right-1.5 top-2 rounded p-1 text-muted-foreground/60 opacity-0 transition-opacity hover:bg-destructive/10 hover:text-destructive group-hover/post:opacity-100",
|
|
792
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5" })
|
|
793
|
-
}
|
|
794
|
-
),
|
|
795
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "pr-5 text-sm font-medium text-foreground", children: post.title || t("marketing.content.untitled") }),
|
|
796
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-1.5", children: [
|
|
797
|
-
/* @__PURE__ */ jsxRuntime.jsx(FormatBadge, { format: post.format }),
|
|
798
|
-
/* @__PURE__ */ jsxRuntime.jsx(PostStatusBadge, { status: post.status }),
|
|
799
|
-
post.scheduledDate && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: post.scheduledDate }),
|
|
800
|
-
/* @__PURE__ */ jsxRuntime.jsx(PlatformBadges, { platforms: post.platforms })
|
|
801
|
-
] })
|
|
802
|
-
]
|
|
803
|
-
}
|
|
804
|
-
);
|
|
805
|
-
}
|
|
806
|
-
function WeekColumn({ week, rangeLabel, posts, onOpenPost, onNewPost, onDeletePost, onDropPost, disabled }) {
|
|
807
|
-
const t = core.useTranslation();
|
|
808
|
-
const [isDropTarget, setIsDropTarget] = React8__default.default.useState(false);
|
|
809
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
810
|
-
"div",
|
|
811
|
-
{
|
|
812
|
-
className: ui.cn(
|
|
813
|
-
"flex flex-col gap-2 rounded-card border border-border bg-muted/20 p-2.5 transition-colors",
|
|
814
|
-
isDropTarget && "border-primary bg-primary/5 ring-1 ring-primary/40"
|
|
815
|
-
),
|
|
816
|
-
onDragOver: (e) => {
|
|
817
|
-
if (!onDropPost || !e.dataTransfer.types.includes(POST_DRAG_MIME)) return;
|
|
818
|
-
e.preventDefault();
|
|
819
|
-
e.dataTransfer.dropEffect = "move";
|
|
820
|
-
setIsDropTarget(true);
|
|
821
|
-
},
|
|
822
|
-
onDragLeave: () => setIsDropTarget(false),
|
|
823
|
-
onDrop: (e) => {
|
|
824
|
-
if (!onDropPost) return;
|
|
825
|
-
e.preventDefault();
|
|
826
|
-
setIsDropTarget(false);
|
|
827
|
-
const postId = e.dataTransfer.getData(POST_DRAG_MIME);
|
|
828
|
-
if (postId) onDropPost(postId, week);
|
|
829
|
-
},
|
|
830
|
-
children: [
|
|
831
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline justify-between px-1", children: [
|
|
832
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: [
|
|
833
|
-
t("marketing.content.week"),
|
|
834
|
-
" ",
|
|
835
|
-
week
|
|
836
|
-
] }),
|
|
837
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground/80", children: rangeLabel })
|
|
838
|
-
] }),
|
|
839
|
-
posts.map((post) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
840
|
-
PostCard,
|
|
841
|
-
{
|
|
842
|
-
post,
|
|
843
|
-
onOpen: () => onOpenPost(post.id),
|
|
844
|
-
onDelete: onDeletePost ? () => onDeletePost(post.id) : void 0
|
|
845
|
-
},
|
|
846
|
-
post.id
|
|
847
|
-
)),
|
|
848
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
849
|
-
"button",
|
|
850
|
-
{
|
|
851
|
-
type: "button",
|
|
852
|
-
onClick: () => onNewPost(week),
|
|
853
|
-
disabled,
|
|
854
|
-
className: "flex items-center justify-center gap-1 rounded-card border border-dashed border-border p-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted/40 hover:text-foreground disabled:pointer-events-none disabled:opacity-50",
|
|
855
|
-
children: [
|
|
856
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" }),
|
|
857
|
-
t("marketing.content.newPost")
|
|
858
|
-
]
|
|
859
|
-
}
|
|
860
|
-
)
|
|
861
|
-
]
|
|
862
|
-
}
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
function AccountModal({ open, account, onClose }) {
|
|
866
|
-
const t = core.useTranslation();
|
|
867
|
-
const saveAccount = useContentPlannerStore((s) => s.saveAccount);
|
|
868
|
-
const deleteAccount = useContentPlannerStore((s) => s.deleteAccount);
|
|
869
|
-
const isEdit = !!account;
|
|
870
|
-
const [name, setName] = React8__default.default.useState("");
|
|
871
|
-
const [handle, setHandle] = React8__default.default.useState("");
|
|
872
|
-
const [platforms, setPlatforms] = React8__default.default.useState(["instagram"]);
|
|
873
|
-
const [saving, setSaving] = React8__default.default.useState(false);
|
|
874
|
-
const [confirmDelete, setConfirmDelete] = React8__default.default.useState(false);
|
|
875
|
-
React8__default.default.useEffect(() => {
|
|
876
|
-
if (open) {
|
|
877
|
-
setName(account?.name ?? "");
|
|
878
|
-
setHandle(account?.handle ?? "");
|
|
879
|
-
setPlatforms(account?.platforms?.length ? account.platforms : ["instagram"]);
|
|
880
|
-
setSaving(false);
|
|
881
|
-
setConfirmDelete(false);
|
|
882
|
-
}
|
|
883
|
-
}, [open, account]);
|
|
884
|
-
const create = async () => {
|
|
885
|
-
if (!name.trim() || platforms.length === 0 || saving) return;
|
|
886
|
-
setSaving(true);
|
|
887
|
-
try {
|
|
888
|
-
await saveAccount({
|
|
889
|
-
...isEdit ? { id: account.id } : {},
|
|
890
|
-
name: name.trim(),
|
|
891
|
-
handle: handle.trim() || void 0,
|
|
892
|
-
platforms
|
|
893
|
-
});
|
|
894
|
-
onClose();
|
|
895
|
-
} finally {
|
|
896
|
-
setSaving(false);
|
|
897
|
-
}
|
|
898
|
-
};
|
|
899
|
-
const remove = async () => {
|
|
900
|
-
if (!account || saving) return;
|
|
901
|
-
setSaving(true);
|
|
902
|
-
try {
|
|
903
|
-
await deleteAccount(account.id);
|
|
904
|
-
onClose();
|
|
905
|
-
} finally {
|
|
906
|
-
setSaving(false);
|
|
907
|
-
}
|
|
908
|
-
};
|
|
909
|
-
return /* @__PURE__ */ jsxRuntime.jsx(ui.Modal, { open, onOpenChange: (o) => {
|
|
910
|
-
if (!o) onClose();
|
|
911
|
-
}, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.ModalContent, { size: "sm", children: [
|
|
912
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.ModalHeader, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.ModalTitle, { children: isEdit ? t("marketing.content.editAccountTitle") : t("marketing.content.addAccountTitle") }) }),
|
|
913
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.ModalBody, { className: "space-y-4", children: [
|
|
914
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
915
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.nameLabel") }),
|
|
916
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
917
|
-
ui.Input,
|
|
918
|
-
{
|
|
919
|
-
autoFocus: true,
|
|
920
|
-
value: name,
|
|
921
|
-
onChange: (e) => setName(e.target.value),
|
|
922
|
-
placeholder: t("marketing.content.accountNamePlaceholder"),
|
|
923
|
-
onKeyDown: (e) => {
|
|
924
|
-
if (e.key === "Enter") void create();
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
)
|
|
928
|
-
] }),
|
|
929
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
930
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.handleLabel") }),
|
|
931
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
932
|
-
ui.Input,
|
|
933
|
-
{
|
|
934
|
-
value: handle,
|
|
935
|
-
onChange: (e) => setHandle(e.target.value),
|
|
936
|
-
placeholder: t("marketing.content.handlePlaceholder")
|
|
937
|
-
}
|
|
938
|
-
)
|
|
939
|
-
] }),
|
|
940
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
941
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.platformsLabel") }),
|
|
942
|
-
/* @__PURE__ */ jsxRuntime.jsx(PlatformPicker, { value: platforms, onChange: setPlatforms })
|
|
943
|
-
] })
|
|
944
|
-
] }),
|
|
945
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.ModalFooter, { children: confirmDelete && isEdit ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex w-full items-center gap-3 rounded-card border border-destructive/30 bg-destructive/5 p-3", children: [
|
|
946
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "flex-1 text-xs text-destructive", children: t("marketing.content.deleteAccountConfirm") }),
|
|
947
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", size: "sm", onClick: () => setConfirmDelete(false), disabled: saving, children: t("marketing.content.cancel") }),
|
|
948
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "destructive", size: "sm", onClick: () => void remove(), disabled: saving, children: t("marketing.content.deleteAccount") })
|
|
949
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
950
|
-
isEdit && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
951
|
-
ui.Button,
|
|
952
|
-
{
|
|
953
|
-
variant: "outline",
|
|
954
|
-
onClick: () => setConfirmDelete(true),
|
|
955
|
-
disabled: saving,
|
|
956
|
-
className: "mr-auto text-destructive hover:text-destructive",
|
|
957
|
-
children: [
|
|
958
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "mr-1.5 h-4 w-4" }),
|
|
959
|
-
t("marketing.content.deleteAccount")
|
|
960
|
-
]
|
|
961
|
-
}
|
|
962
|
-
),
|
|
963
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", onClick: onClose, disabled: saving, children: t("marketing.content.cancel") }),
|
|
964
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: () => void create(), disabled: saving || !name.trim() || platforms.length === 0, children: isEdit ? t("marketing.content.save") : t("marketing.content.createAccount") })
|
|
965
|
-
] }) })
|
|
966
|
-
] }) });
|
|
967
|
-
}
|
|
968
|
-
function AccountSelector() {
|
|
969
|
-
const t = core.useTranslation();
|
|
970
|
-
const loaded = useContentPlannerStore((s) => s.loaded);
|
|
971
|
-
const accounts = useContentPlannerStore((s) => s.accounts);
|
|
972
|
-
const activeAccountId = useContentPlannerStore((s) => s.activeAccountId);
|
|
973
|
-
const setAccount = useContentPlannerStore((s) => s.setAccount);
|
|
974
|
-
const [open, setOpen] = React8__default.default.useState(false);
|
|
975
|
-
const [modal, setModal] = React8__default.default.useState(null);
|
|
976
|
-
const activeAccount = accounts.find((a) => a.id === activeAccountId) ?? null;
|
|
977
|
-
if (!loaded) {
|
|
978
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
979
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-9 w-64" }),
|
|
980
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-9 w-9" })
|
|
981
|
-
] });
|
|
982
|
-
}
|
|
983
|
-
const openSettings = (account) => {
|
|
984
|
-
setOpen(false);
|
|
985
|
-
setModal({ mode: "edit", account });
|
|
986
|
-
};
|
|
987
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
988
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
989
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
990
|
-
ui.Select,
|
|
991
|
-
{
|
|
992
|
-
open,
|
|
993
|
-
onOpenChange: setOpen,
|
|
994
|
-
value: activeAccountId ?? void 0,
|
|
995
|
-
onValueChange: (id) => {
|
|
996
|
-
if (id === ADD_ACCOUNT_VALUE) {
|
|
997
|
-
setModal({ mode: "create" });
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
void setAccount(id);
|
|
1001
|
-
},
|
|
1002
|
-
children: [
|
|
1003
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectTrigger, { className: "w-64", children: /* @__PURE__ */ jsxRuntime.jsx(ui.SelectValue, { placeholder: t("marketing.content.account") }) }),
|
|
1004
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.SelectContent, { children: [
|
|
1005
|
-
accounts.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: "__none__", disabled: true, children: t("marketing.content.noAccountsAvailable") }) : accounts.map((acc) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
1006
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: acc.id, className: "pr-9", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [
|
|
1007
|
-
acc.name,
|
|
1008
|
-
/* @__PURE__ */ jsxRuntime.jsx(PlatformBadges, { platforms: acc.platforms })
|
|
1009
|
-
] }) }),
|
|
1010
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1011
|
-
"button",
|
|
1012
|
-
{
|
|
1013
|
-
type: "button",
|
|
1014
|
-
"aria-label": t("marketing.content.accountSettings"),
|
|
1015
|
-
onPointerDown: (e) => {
|
|
1016
|
-
e.stopPropagation();
|
|
1017
|
-
e.preventDefault();
|
|
1018
|
-
},
|
|
1019
|
-
onPointerUp: (e) => {
|
|
1020
|
-
e.stopPropagation();
|
|
1021
|
-
e.preventDefault();
|
|
1022
|
-
},
|
|
1023
|
-
onClick: (e) => {
|
|
1024
|
-
e.stopPropagation();
|
|
1025
|
-
openSettings(acc);
|
|
1026
|
-
},
|
|
1027
|
-
className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-muted-foreground/70 transition-colors hover:bg-muted/60 hover:text-foreground",
|
|
1028
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings2, { className: "h-3.5 w-3.5" })
|
|
1029
|
-
}
|
|
1030
|
-
)
|
|
1031
|
-
] }, acc.id)),
|
|
1032
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectSeparator, {}),
|
|
1033
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: ADD_ACCOUNT_VALUE, children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1034
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" }),
|
|
1035
|
-
t("marketing.content.addAccount")
|
|
1036
|
-
] }) })
|
|
1037
|
-
] })
|
|
1038
|
-
]
|
|
1039
|
-
}
|
|
1040
|
-
),
|
|
1041
|
-
activeAccount && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1042
|
-
ui.Button,
|
|
1043
|
-
{
|
|
1044
|
-
variant: "ghost",
|
|
1045
|
-
size: "sm",
|
|
1046
|
-
className: "h-9 w-9 p-0 text-muted-foreground",
|
|
1047
|
-
"aria-label": t("marketing.content.accountSettings"),
|
|
1048
|
-
onClick: () => setModal({ mode: "edit", account: activeAccount }),
|
|
1049
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings2, { className: "h-4 w-4" })
|
|
1050
|
-
}
|
|
1051
|
-
)
|
|
1052
|
-
] }),
|
|
1053
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1054
|
-
AccountModal,
|
|
1055
|
-
{
|
|
1056
|
-
open: !!modal,
|
|
1057
|
-
account: modal?.mode === "edit" ? modal.account : null,
|
|
1058
|
-
onClose: () => setModal(null)
|
|
1059
|
-
}
|
|
1060
|
-
)
|
|
1061
|
-
] });
|
|
1062
|
-
}
|
|
1063
|
-
function ViewSwitcher({ mode, onChange }) {
|
|
1064
|
-
const t = core.useTranslation();
|
|
1065
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1066
|
-
ui.SegmentedControl,
|
|
1067
|
-
{
|
|
1068
|
-
size: "md",
|
|
1069
|
-
"aria-label": t("marketing.content.viewBoard"),
|
|
1070
|
-
value: mode,
|
|
1071
|
-
onChange,
|
|
1072
|
-
options: [
|
|
1073
|
-
{ value: "board", label: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1074
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Columns3, { className: "h-3.5 w-3.5" }),
|
|
1075
|
-
t("marketing.content.viewBoard")
|
|
1076
|
-
] }) },
|
|
1077
|
-
{ value: "calendar", label: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1078
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.CalendarDays, { className: "h-3.5 w-3.5" }),
|
|
1079
|
-
t("marketing.content.viewCalendar")
|
|
1080
|
-
] }) },
|
|
1081
|
-
{ value: "list", label: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1082
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.List, { className: "h-3.5 w-3.5" }),
|
|
1083
|
-
t("marketing.content.viewList")
|
|
1084
|
-
] }) }
|
|
1085
|
-
]
|
|
1086
|
-
}
|
|
1087
|
-
);
|
|
1088
|
-
}
|
|
1089
|
-
function shortDay(d) {
|
|
1090
|
-
return d.toLocaleDateString(void 0, { day: "numeric", month: "short" });
|
|
1091
|
-
}
|
|
1092
|
-
function buildBoardWeeks(startDate, weeksCount) {
|
|
1093
|
-
return Array.from({ length: weeksCount }, (_, i) => {
|
|
1094
|
-
const start = new Date(startDate);
|
|
1095
|
-
start.setDate(start.getDate() + i * 7);
|
|
1096
|
-
const end = new Date(start);
|
|
1097
|
-
end.setDate(start.getDate() + 6);
|
|
1098
|
-
return {
|
|
1099
|
-
week: i + 1,
|
|
1100
|
-
rangeLabel: `${shortDay(start)} \u2013 ${shortDay(end)}`,
|
|
1101
|
-
monthLabel: start.toLocaleDateString(void 0, { month: "long", year: "numeric" })
|
|
1102
|
-
};
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1105
|
-
function BoardView({ startDate, weeksCount, postsByWeek, onOpenPost, onNewPost, onDeletePost, onDropPost, disabled }) {
|
|
1106
|
-
const months = [];
|
|
1107
|
-
for (const bw of buildBoardWeeks(startDate, weeksCount)) {
|
|
1108
|
-
const current = months[months.length - 1];
|
|
1109
|
-
if (current && current.label === bw.monthLabel) current.weeks.push(bw);
|
|
1110
|
-
else months.push({ label: bw.monthLabel, weeks: [bw] });
|
|
1111
|
-
}
|
|
1112
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-8", children: months.map(({ label, weeks }) => /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "space-y-3", children: [
|
|
1113
|
-
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: label }),
|
|
1114
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4", children: weeks.map(({ week, rangeLabel }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1115
|
-
WeekColumn,
|
|
1116
|
-
{
|
|
1117
|
-
week,
|
|
1118
|
-
rangeLabel,
|
|
1119
|
-
posts: postsByWeek.get(week) ?? [],
|
|
1120
|
-
onOpenPost,
|
|
1121
|
-
onNewPost,
|
|
1122
|
-
onDeletePost,
|
|
1123
|
-
onDropPost,
|
|
1124
|
-
disabled
|
|
1125
|
-
},
|
|
1126
|
-
week
|
|
1127
|
-
)) })
|
|
1128
|
-
] }, label)) });
|
|
1129
|
-
}
|
|
1130
|
-
function toISODate(d) {
|
|
1131
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1132
|
-
}
|
|
1133
|
-
function mondayOfCurrentWeek() {
|
|
1134
|
-
const now = /* @__PURE__ */ new Date();
|
|
1135
|
-
const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1136
|
-
monday.setDate(monday.getDate() - (monday.getDay() + 6) % 7);
|
|
1137
|
-
return monday;
|
|
1138
|
-
}
|
|
1139
|
-
function CalendarView({ posts, onOpenPost, onNewPostAt, disabled }) {
|
|
1140
|
-
const t = core.useTranslation();
|
|
1141
|
-
const [cursor, setCursor] = React8__default.default.useState(() => {
|
|
1142
|
-
const first = posts.find((p) => p.scheduledDate)?.scheduledDate;
|
|
1143
|
-
const base = first ? /* @__PURE__ */ new Date(`${first}T00:00:00`) : /* @__PURE__ */ new Date();
|
|
1144
|
-
return new Date(base.getFullYear(), base.getMonth(), 1);
|
|
1145
|
-
});
|
|
1146
|
-
const [expandedDay, setExpandedDay] = React8__default.default.useState(null);
|
|
1147
|
-
const MAX_VISIBLE = 3;
|
|
1148
|
-
const postsByDate = React8__default.default.useMemo(() => {
|
|
1149
|
-
const map = /* @__PURE__ */ new Map();
|
|
1150
|
-
for (const post of posts) {
|
|
1151
|
-
if (!post.scheduledDate) continue;
|
|
1152
|
-
const list = map.get(post.scheduledDate) ?? [];
|
|
1153
|
-
list.push(post);
|
|
1154
|
-
map.set(post.scheduledDate, list);
|
|
1155
|
-
}
|
|
1156
|
-
return map;
|
|
1157
|
-
}, [posts]);
|
|
1158
|
-
const unscheduled = posts.filter((p) => !p.scheduledDate);
|
|
1159
|
-
const days = React8__default.default.useMemo(() => {
|
|
1160
|
-
const firstOfMonth = new Date(cursor.getFullYear(), cursor.getMonth(), 1);
|
|
1161
|
-
const start = new Date(firstOfMonth);
|
|
1162
|
-
start.setDate(start.getDate() - (firstOfMonth.getDay() + 6) % 7);
|
|
1163
|
-
const cells = [];
|
|
1164
|
-
for (let i = 0; i < 42; i++) {
|
|
1165
|
-
const d = new Date(start);
|
|
1166
|
-
d.setDate(start.getDate() + i);
|
|
1167
|
-
cells.push(d);
|
|
1168
|
-
}
|
|
1169
|
-
while (cells.length > 7 && cells[cells.length - 7].getMonth() !== cursor.getMonth()) {
|
|
1170
|
-
cells.splice(cells.length - 7, 7);
|
|
1171
|
-
}
|
|
1172
|
-
return cells;
|
|
1173
|
-
}, [cursor]);
|
|
1174
|
-
const monthLabel = cursor.toLocaleDateString(void 0, { month: "long", year: "numeric" });
|
|
1175
|
-
const weekdayLabels = days.slice(0, 7).map(
|
|
1176
|
-
(d) => d.toLocaleDateString(void 0, { weekday: "short" })
|
|
1177
|
-
);
|
|
1178
|
-
const todayISO = toISODate(/* @__PURE__ */ new Date());
|
|
1179
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
1180
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1181
|
-
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold capitalize text-foreground", children: monthLabel }),
|
|
1182
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
1183
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", size: "sm", onClick: () => setCursor(new Date(cursor.getFullYear(), cursor.getMonth() - 1, 1)), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "h-4 w-4" }) }),
|
|
1184
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", size: "sm", onClick: () => setCursor(new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1)), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4" }) })
|
|
1185
|
-
] })
|
|
1186
|
-
] }),
|
|
1187
|
-
unscheduled.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2 rounded-card border border-dashed border-border bg-muted/20 p-2.5", children: [
|
|
1188
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: t("marketing.content.unscheduled") }),
|
|
1189
|
-
unscheduled.map((post) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1190
|
-
"button",
|
|
1191
|
-
{
|
|
1192
|
-
type: "button",
|
|
1193
|
-
onClick: () => onOpenPost(post.id),
|
|
1194
|
-
className: "inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-2.5 py-1 text-xs font-medium text-foreground transition-colors hover:bg-muted/40",
|
|
1195
|
-
children: [
|
|
1196
|
-
post.title || t("marketing.content.untitled"),
|
|
1197
|
-
/* @__PURE__ */ jsxRuntime.jsx(FormatBadge, { format: post.format })
|
|
1198
|
-
]
|
|
1199
|
-
},
|
|
1200
|
-
post.id
|
|
1201
|
-
))
|
|
1202
|
-
] }),
|
|
1203
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "overflow-hidden rounded-card border border-border", children: [
|
|
1204
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-7 border-b border-border bg-muted/30", children: weekdayLabels.map((label, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-2 py-1.5 text-center text-xs font-medium capitalize text-muted-foreground", children: label }, i)) }),
|
|
1205
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-7", children: days.map((day, i) => {
|
|
1206
|
-
const iso = toISODate(day);
|
|
1207
|
-
const inMonth = day.getMonth() === cursor.getMonth();
|
|
1208
|
-
const dayPosts = postsByDate.get(iso) ?? [];
|
|
1209
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1210
|
-
"div",
|
|
1211
|
-
{
|
|
1212
|
-
className: ui.cn(
|
|
1213
|
-
"group min-h-24 border-b border-r border-border p-1.5 [&:nth-child(7n)]:border-r-0",
|
|
1214
|
-
!inMonth && "bg-muted/20"
|
|
1215
|
-
),
|
|
1216
|
-
children: [
|
|
1217
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1218
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1219
|
-
"span",
|
|
1220
|
-
{
|
|
1221
|
-
className: ui.cn(
|
|
1222
|
-
"text-xs",
|
|
1223
|
-
inMonth ? "text-foreground" : "text-muted-foreground/60",
|
|
1224
|
-
iso === todayISO && "flex h-5 w-5 items-center justify-center rounded-full bg-primary font-semibold text-primary-foreground"
|
|
1225
|
-
),
|
|
1226
|
-
children: day.getDate()
|
|
1227
|
-
}
|
|
1228
|
-
),
|
|
1229
|
-
!disabled && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1230
|
-
"button",
|
|
1231
|
-
{
|
|
1232
|
-
type: "button",
|
|
1233
|
-
onClick: () => onNewPostAt(iso),
|
|
1234
|
-
className: "rounded p-0.5 text-muted-foreground opacity-0 transition-opacity hover:bg-muted/60 hover:text-foreground group-hover:opacity-100",
|
|
1235
|
-
"aria-label": t("marketing.content.newPost"),
|
|
1236
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" })
|
|
1237
|
-
}
|
|
1238
|
-
)
|
|
1239
|
-
] }),
|
|
1240
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 space-y-1", children: [
|
|
1241
|
-
(expandedDay === iso ? dayPosts : dayPosts.slice(0, MAX_VISIBLE)).map((post) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1242
|
-
"button",
|
|
1243
|
-
{
|
|
1244
|
-
type: "button",
|
|
1245
|
-
onClick: () => onOpenPost(post.id),
|
|
1246
|
-
className: "block w-full truncate rounded border border-border bg-card px-1.5 py-1 text-left text-xs text-foreground transition-colors hover:bg-muted/40",
|
|
1247
|
-
children: post.title || t("marketing.content.untitled")
|
|
1248
|
-
},
|
|
1249
|
-
post.id
|
|
1250
|
-
)),
|
|
1251
|
-
dayPosts.length > MAX_VISIBLE && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1252
|
-
"button",
|
|
1253
|
-
{
|
|
1254
|
-
type: "button",
|
|
1255
|
-
onClick: () => setExpandedDay(expandedDay === iso ? null : iso),
|
|
1256
|
-
className: "block w-full px-1.5 py-0.5 text-left text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
|
|
1257
|
-
children: expandedDay === iso ? t("marketing.content.showLess") : `+${dayPosts.length - MAX_VISIBLE}`
|
|
1258
|
-
}
|
|
1259
|
-
)
|
|
1260
|
-
] })
|
|
1261
|
-
]
|
|
1262
|
-
},
|
|
1263
|
-
i
|
|
1264
|
-
);
|
|
1265
|
-
}) })
|
|
1266
|
-
] })
|
|
1267
|
-
] });
|
|
1268
|
-
}
|
|
1269
|
-
function ListView({ posts, onOpenPost }) {
|
|
1270
|
-
const t = core.useTranslation();
|
|
1271
|
-
if (posts.length === 0) {
|
|
1272
|
-
return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.content.noPosts") });
|
|
1273
|
-
}
|
|
1274
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-card border border-border", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full text-sm", children: [
|
|
1275
|
-
/* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "border-b border-border bg-muted/30 text-left text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [
|
|
1276
|
-
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2", children: t("marketing.content.listWeek") }),
|
|
1277
|
-
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2", children: t("marketing.content.listTitle") }),
|
|
1278
|
-
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2", children: t("marketing.content.listFormat") }),
|
|
1279
|
-
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2", children: t("marketing.content.listStatus") }),
|
|
1280
|
-
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2", children: t("marketing.content.listDate") })
|
|
1281
|
-
] }) }),
|
|
1282
|
-
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: posts.map((post) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1283
|
-
"tr",
|
|
1284
|
-
{
|
|
1285
|
-
onClick: () => onOpenPost(post.id),
|
|
1286
|
-
className: "cursor-pointer border-b border-border last:border-b-0 transition-colors hover:bg-muted/30",
|
|
1287
|
-
children: [
|
|
1288
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-muted-foreground", children: post.weekNumber }),
|
|
1289
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 font-medium text-foreground", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [
|
|
1290
|
-
post.title || t("marketing.content.untitled"),
|
|
1291
|
-
/* @__PURE__ */ jsxRuntime.jsx(PlatformBadges, { platforms: post.platforms })
|
|
1292
|
-
] }) }),
|
|
1293
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsxRuntime.jsx(FormatBadge, { format: post.format }) }),
|
|
1294
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsxRuntime.jsx(PostStatusBadge, { status: post.status }) }),
|
|
1295
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-muted-foreground", children: post.scheduledDate ?? "\u2014" })
|
|
1296
|
-
]
|
|
1297
|
-
},
|
|
1298
|
-
post.id
|
|
1299
|
-
)) })
|
|
1300
|
-
] }) });
|
|
1301
|
-
}
|
|
1302
|
-
function ContentView({ onOpenPost }) {
|
|
1303
|
-
const t = core.useTranslation();
|
|
1304
|
-
const loaded = useContentPlannerStore((s) => s.loaded);
|
|
1305
|
-
const loading = useContentPlannerStore((s) => s.loading);
|
|
1306
|
-
const accounts = useContentPlannerStore((s) => s.accounts);
|
|
1307
|
-
const activeAccountId = useContentPlannerStore((s) => s.activeAccountId);
|
|
1308
|
-
const plan = useContentPlannerStore((s) => s.activePlan);
|
|
1309
|
-
const posts = useContentPlannerStore((s) => s.posts);
|
|
1310
|
-
const load = useContentPlannerStore((s) => s.load);
|
|
1311
|
-
const savePlan = useContentPlannerStore((s) => s.savePlan);
|
|
1312
|
-
const savePost = useContentPlannerStore((s) => s.savePost);
|
|
1313
|
-
const deletePost = useContentPlannerStore((s) => s.deletePost);
|
|
1314
|
-
const viewMode = useContentPlannerStore((s) => s.viewMode);
|
|
1315
|
-
const setViewMode = useContentPlannerStore((s) => s.setViewMode);
|
|
1316
|
-
const loadedTenantId = useContentPlannerStore((s) => s.loadedTenantId);
|
|
1317
|
-
const [briefOpen, setBriefOpen] = React8__default.default.useState(false);
|
|
1318
|
-
const [pendingDeleteId, setPendingDeleteId] = React8__default.default.useState(null);
|
|
1319
|
-
const tenantId = saas.useOrganizationStore((state) => state.currentOrg?.id);
|
|
1320
|
-
React8__default.default.useEffect(() => {
|
|
1321
|
-
if (!loading && (!loaded || tenantId && tenantId !== loadedTenantId)) void load();
|
|
1322
|
-
}, [tenantId, loadedTenantId, loaded, loading, load]);
|
|
1323
|
-
const postsByWeek = React8__default.default.useMemo(() => {
|
|
1324
|
-
const map = /* @__PURE__ */ new Map();
|
|
1325
|
-
for (const post of posts) {
|
|
1326
|
-
const list = map.get(post.weekNumber) ?? [];
|
|
1327
|
-
list.push(post);
|
|
1328
|
-
map.set(post.weekNumber, list);
|
|
1329
|
-
}
|
|
1330
|
-
return map;
|
|
1331
|
-
}, [posts]);
|
|
1332
|
-
const ensurePlan = async () => {
|
|
1333
|
-
if (plan) return plan;
|
|
1334
|
-
if (!activeAccountId) return null;
|
|
1335
|
-
return savePlan({
|
|
1336
|
-
accountId: activeAccountId,
|
|
1337
|
-
name: t("marketing.content.defaultPlanName"),
|
|
1338
|
-
status: "active",
|
|
1339
|
-
weeksCount: DEFAULT_WEEKS,
|
|
1340
|
-
startDate: toISODate(mondayOfCurrentWeek())
|
|
1341
|
-
});
|
|
1342
|
-
};
|
|
1343
|
-
const createPost = async (week) => {
|
|
1344
|
-
const targetPlan = await ensurePlan();
|
|
1345
|
-
if (!targetPlan) return;
|
|
1346
|
-
const start = targetPlan.startDate ? /* @__PURE__ */ new Date(`${targetPlan.startDate}T00:00:00`) : mondayOfCurrentWeek();
|
|
1347
|
-
start.setDate(start.getDate() + (week - 1) * 7);
|
|
1348
|
-
const post = await savePost({
|
|
1349
|
-
planId: targetPlan.id,
|
|
1350
|
-
weekNumber: week,
|
|
1351
|
-
title: "",
|
|
1352
|
-
format: "reel",
|
|
1353
|
-
status: "idea",
|
|
1354
|
-
scheduledDate: toISODate(start)
|
|
1355
|
-
});
|
|
1356
|
-
onOpenPost(post.id);
|
|
1357
|
-
};
|
|
1358
|
-
const movePost = async (postId, week) => {
|
|
1359
|
-
const post = posts.find((p) => p.id === postId);
|
|
1360
|
-
if (!post || post.weekNumber === week) return;
|
|
1361
|
-
await savePost({ id: postId, weekNumber: week });
|
|
1362
|
-
};
|
|
1363
|
-
const createPostAt = async (isoDate) => {
|
|
1364
|
-
const targetPlan = await ensurePlan();
|
|
1365
|
-
if (!targetPlan) return;
|
|
1366
|
-
let week = 1;
|
|
1367
|
-
if (targetPlan.startDate) {
|
|
1368
|
-
const start = /* @__PURE__ */ new Date(`${targetPlan.startDate}T00:00:00`);
|
|
1369
|
-
const day = /* @__PURE__ */ new Date(`${isoDate}T00:00:00`);
|
|
1370
|
-
const diff = Math.floor((day.getTime() - start.getTime()) / (7 * 24 * 3600 * 1e3)) + 1;
|
|
1371
|
-
week = Math.min(Math.max(diff, 1), targetPlan.weeksCount);
|
|
1372
|
-
}
|
|
1373
|
-
const post = await savePost({
|
|
1374
|
-
planId: targetPlan.id,
|
|
1375
|
-
weekNumber: week,
|
|
1376
|
-
title: "",
|
|
1377
|
-
format: "reel",
|
|
1378
|
-
status: "idea",
|
|
1379
|
-
scheduledDate: isoDate
|
|
1380
|
-
});
|
|
1381
|
-
onOpenPost(post.id);
|
|
1382
|
-
};
|
|
1383
|
-
const busy = !loaded || loading;
|
|
1384
|
-
const hasAccount = accounts.length > 0;
|
|
1385
|
-
const weeksCount = plan?.weeksCount ?? DEFAULT_WEEKS;
|
|
1386
|
-
const boardStart = plan?.startDate ? /* @__PURE__ */ new Date(`${plan.startDate}T00:00:00`) : mondayOfCurrentWeek();
|
|
1387
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl space-y-6", children: [
|
|
1388
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
|
|
1389
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
|
|
1390
|
-
/* @__PURE__ */ jsxRuntime.jsx(AccountSelector, {}),
|
|
1391
|
-
plan && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-muted-foreground", children: plan.name })
|
|
1392
|
-
] }),
|
|
1393
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1394
|
-
/* @__PURE__ */ jsxRuntime.jsx(ViewSwitcher, { mode: viewMode, onChange: setViewMode }),
|
|
1395
|
-
plan && /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: () => setBriefOpen(true), children: [
|
|
1396
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.NotebookPen, { className: "mr-1.5 h-4 w-4" }),
|
|
1397
|
-
t("marketing.content.brief")
|
|
1398
|
-
] })
|
|
1399
|
-
] })
|
|
1400
|
-
] }),
|
|
1401
|
-
!busy && !hasAccount && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.content.noAccountsHint") }),
|
|
1402
|
-
busy && viewMode === "board" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4", children: Array.from({ length: 8 }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-44" }, i)) }),
|
|
1403
|
-
busy && viewMode === "calendar" && /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-96 w-full" }),
|
|
1404
|
-
busy && viewMode === "list" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: Array.from({ length: 6 }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-10 w-full" }, i)) }),
|
|
1405
|
-
!busy && viewMode === "board" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1406
|
-
BoardView,
|
|
1407
|
-
{
|
|
1408
|
-
startDate: boardStart,
|
|
1409
|
-
weeksCount,
|
|
1410
|
-
postsByWeek,
|
|
1411
|
-
onOpenPost,
|
|
1412
|
-
onNewPost: (w) => void createPost(w),
|
|
1413
|
-
onDeletePost: setPendingDeleteId,
|
|
1414
|
-
onDropPost: (id, w) => void movePost(id, w),
|
|
1415
|
-
disabled: !hasAccount
|
|
1416
|
-
}
|
|
1417
|
-
),
|
|
1418
|
-
!busy && viewMode === "calendar" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1419
|
-
CalendarView,
|
|
1420
|
-
{
|
|
1421
|
-
posts,
|
|
1422
|
-
onOpenPost,
|
|
1423
|
-
onNewPostAt: (iso) => void createPostAt(iso),
|
|
1424
|
-
disabled: !hasAccount
|
|
1425
|
-
}
|
|
1426
|
-
),
|
|
1427
|
-
!busy && viewMode === "list" && /* @__PURE__ */ jsxRuntime.jsx(ListView, { posts, onOpenPost }),
|
|
1428
|
-
/* @__PURE__ */ jsxRuntime.jsx(PlanBriefSheet, { open: briefOpen, onClose: () => setBriefOpen(false) }),
|
|
1429
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1430
|
-
ui.ConfirmDialog,
|
|
1431
|
-
{
|
|
1432
|
-
open: !!pendingDeleteId,
|
|
1433
|
-
variant: "destructive",
|
|
1434
|
-
title: t("marketing.content.deletePost"),
|
|
1435
|
-
description: t("marketing.content.deletePostConfirm"),
|
|
1436
|
-
confirmLabel: t("marketing.content.deletePost"),
|
|
1437
|
-
cancelLabel: t("marketing.content.cancel"),
|
|
1438
|
-
onCancel: () => setPendingDeleteId(null),
|
|
1439
|
-
onConfirm: () => {
|
|
1440
|
-
const id = pendingDeleteId;
|
|
1441
|
-
setPendingDeleteId(null);
|
|
1442
|
-
if (id) void deletePost(id);
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
)
|
|
1446
|
-
] });
|
|
1447
|
-
}
|
|
1448
|
-
function canUploadMedia() {
|
|
1449
|
-
return !!core.getSupabaseClientOptional();
|
|
1450
|
-
}
|
|
1451
|
-
async function uploadPostMedia(postId, file) {
|
|
1452
|
-
const supabase = core.getSupabaseClientOptional();
|
|
1453
|
-
if (!supabase) throw new Error("Supabase not initialized");
|
|
1454
|
-
const tenantId = core.getActiveTenantId();
|
|
1455
|
-
if (!tenantId) throw new Error("No active tenant");
|
|
1456
|
-
const ext = file.name.split(".").pop()?.toLowerCase() ?? "bin";
|
|
1457
|
-
const path = `content/${tenantId}/${postId}-${Date.now()}.${ext}`;
|
|
1458
|
-
const { error } = await supabase.storage.from("mkt-media").upload(path, file, { upsert: true });
|
|
1459
|
-
if (error) throw error;
|
|
1460
|
-
const { data } = supabase.storage.from("mkt-media").getPublicUrl(path);
|
|
1461
|
-
return data.publicUrl;
|
|
1462
|
-
}
|
|
1463
|
-
function newId() {
|
|
1464
|
-
return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
1465
|
-
}
|
|
1466
|
-
function RecordingChecklist({ post }) {
|
|
1467
|
-
const t = core.useTranslation();
|
|
1468
|
-
const savePost = useContentPlannerStore((s) => s.savePost);
|
|
1469
|
-
const [items, setItems] = React8__default.default.useState(post.checklist);
|
|
1470
|
-
const [text, setText] = React8__default.default.useState("");
|
|
1471
|
-
React8__default.default.useEffect(() => {
|
|
1472
|
-
setItems(post.checklist);
|
|
1473
|
-
}, [post.id]);
|
|
1474
|
-
const persist = (next) => {
|
|
1475
|
-
setItems(next);
|
|
1476
|
-
void savePost({ id: post.id, checklist: next });
|
|
1477
|
-
};
|
|
1478
|
-
const toggle = (id) => persist(items.map((i) => i.id === id ? { ...i, done: !i.done } : i));
|
|
1479
|
-
const remove = (id) => persist(items.filter((i) => i.id !== id));
|
|
1480
|
-
const add = () => {
|
|
1481
|
-
const value = text.trim();
|
|
1482
|
-
if (!value) return;
|
|
1483
|
-
persist([...items, { id: newId(), text: value, done: false }]);
|
|
1484
|
-
setText("");
|
|
1485
|
-
};
|
|
1486
|
-
const generateFromScript = () => {
|
|
1487
|
-
const takes = [...post.contentMd.matchAll(/^##\s+(TAKE\s+.+)$/gim)].map(
|
|
1488
|
-
(m) => m[1].trim()
|
|
1489
|
-
);
|
|
1490
|
-
const existing = new Set(items.map((i) => i.text));
|
|
1491
|
-
const fresh = takes.filter((take) => !existing.has(take)).map((take) => ({ id: newId(), text: take, done: false }));
|
|
1492
|
-
if (fresh.length > 0) persist([...items, ...fresh]);
|
|
1493
|
-
};
|
|
1494
|
-
const hasTakes = /^##\s+TAKE\s+/im.test(post.contentMd);
|
|
1495
|
-
const done = items.filter((i) => i.done).length;
|
|
1496
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
1497
|
-
items.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1.5", children: [
|
|
1498
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [
|
|
1499
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
1500
|
-
done,
|
|
1501
|
-
"/",
|
|
1502
|
-
items.length
|
|
1503
|
-
] }),
|
|
1504
|
-
hasTakes && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1505
|
-
"button",
|
|
1506
|
-
{
|
|
1507
|
-
type: "button",
|
|
1508
|
-
onClick: generateFromScript,
|
|
1509
|
-
className: "flex items-center gap-1 font-medium text-muted-foreground transition-colors hover:text-foreground",
|
|
1510
|
-
children: [
|
|
1511
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "h-3 w-3" }),
|
|
1512
|
-
t("marketing.content.checklistGenerate")
|
|
1513
|
-
]
|
|
1514
|
-
}
|
|
1515
|
-
)
|
|
1516
|
-
] }),
|
|
1517
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-1 overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1518
|
-
"div",
|
|
1519
|
-
{
|
|
1520
|
-
className: "h-full rounded-full bg-primary transition-[width]",
|
|
1521
|
-
style: { width: items.length ? `${done / items.length * 100}%` : 0 }
|
|
1522
|
-
}
|
|
1523
|
-
) })
|
|
1524
|
-
] }),
|
|
1525
|
-
items.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3 rounded-card border border-dashed border-border p-3 text-center", children: [
|
|
1526
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.content.checklistEmpty") }),
|
|
1527
|
-
hasTakes && /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: generateFromScript, children: [
|
|
1528
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
1529
|
-
t("marketing.content.checklistGenerate")
|
|
1530
|
-
] })
|
|
1531
|
-
] }),
|
|
1532
|
-
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "space-y-1", children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "group/item flex items-start gap-2.5 rounded-button px-1 py-2 transition-colors hover:bg-muted/40", children: [
|
|
1533
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex min-w-0 flex-1 cursor-pointer items-start gap-2.5", children: [
|
|
1534
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Checkbox, { checked: item.done, onChange: () => toggle(item.id), className: "mt-0.5" }),
|
|
1535
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1536
|
-
"span",
|
|
1537
|
-
{
|
|
1538
|
-
className: ui.cn(
|
|
1539
|
-
"min-w-0 flex-1 text-sm leading-snug",
|
|
1540
|
-
item.done ? "text-muted-foreground line-through" : "text-foreground"
|
|
1541
|
-
),
|
|
1542
|
-
children: item.text
|
|
1543
|
-
}
|
|
1544
|
-
)
|
|
1545
|
-
] }),
|
|
1546
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1547
|
-
"button",
|
|
1548
|
-
{
|
|
1549
|
-
type: "button",
|
|
1550
|
-
"aria-label": t("marketing.content.cancel"),
|
|
1551
|
-
onClick: () => remove(item.id),
|
|
1552
|
-
className: "rounded p-0.5 text-muted-foreground/50 opacity-0 transition-opacity hover:text-destructive group-hover/item:opacity-100 focus:opacity-100",
|
|
1553
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-3.5 w-3.5" })
|
|
1554
|
-
}
|
|
1555
|
-
)
|
|
1556
|
-
] }, item.id)) }),
|
|
1557
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1558
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1559
|
-
ui.Input,
|
|
1560
|
-
{
|
|
1561
|
-
value: text,
|
|
1562
|
-
onChange: (e) => setText(e.target.value),
|
|
1563
|
-
placeholder: t("marketing.content.checklistAddPlaceholder"),
|
|
1564
|
-
onKeyDown: (e) => {
|
|
1565
|
-
if (e.key === "Enter") add();
|
|
1566
|
-
},
|
|
1567
|
-
className: "h-8 text-sm"
|
|
1568
|
-
}
|
|
1569
|
-
),
|
|
1570
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", size: "sm", onClick: add, disabled: !text.trim(), "aria-label": t("marketing.content.checklistAdd"), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4" }) })
|
|
1571
|
-
] })
|
|
1572
|
-
] });
|
|
1573
|
-
}
|
|
1574
|
-
function toCaption(md) {
|
|
1575
|
-
return md.replace(/^#+\s*/gm, "").replace(/\*\*/g, "").replace(/^[-*]\s+/gm, "").trim();
|
|
1576
|
-
}
|
|
1577
|
-
function isVideo(url) {
|
|
1578
|
-
return !!url && /\.(mp4|webm|mov)($|\?)/i.test(url);
|
|
1579
|
-
}
|
|
1580
|
-
function Media({ mediaUrl, className }) {
|
|
1581
|
-
if (!mediaUrl) {
|
|
1582
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: ui.cn("flex aspect-square w-full items-center justify-center bg-muted/40", className), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Image, { className: "h-8 w-8 text-muted-foreground/40" }) });
|
|
1583
|
-
}
|
|
1584
|
-
return isVideo(mediaUrl) ? /* @__PURE__ */ jsxRuntime.jsx("video", { src: mediaUrl, controls: true, className: ui.cn("w-full bg-black object-contain", className) }) : /* @__PURE__ */ jsxRuntime.jsx("img", { src: mediaUrl, alt: "", className: ui.cn("w-full object-cover", className) });
|
|
1585
|
-
}
|
|
1586
|
-
function Avatar({ name, ring }) {
|
|
1587
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1588
|
-
"span",
|
|
1589
|
-
{
|
|
1590
|
-
className: ui.cn(
|
|
1591
|
-
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold text-foreground",
|
|
1592
|
-
ring && "ring-2 ring-offset-2 ring-pink-500/60"
|
|
1593
|
-
),
|
|
1594
|
-
children: (name.replace(/^@/, "")[0] ?? "?").toUpperCase()
|
|
1595
|
-
}
|
|
1596
|
-
);
|
|
1597
|
-
}
|
|
1598
|
-
function InstagramFrame({ caption, mediaUrl, accountName, accountHandle }) {
|
|
1599
|
-
const t = core.useTranslation();
|
|
1600
|
-
const handle = (accountHandle || accountName).replace(/^@/, "");
|
|
1601
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto w-full max-w-sm overflow-hidden rounded-card border border-border bg-card", children: [
|
|
1602
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2.5", children: [
|
|
1603
|
-
/* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: handle, ring: true }),
|
|
1604
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate text-sm font-semibold text-foreground", children: handle }),
|
|
1605
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoreHorizontal, { className: "h-4 w-4 text-muted-foreground" })
|
|
1606
|
-
] }),
|
|
1607
|
-
/* @__PURE__ */ jsxRuntime.jsx(Media, { mediaUrl }),
|
|
1608
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 px-3 py-2.5", children: [
|
|
1609
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 text-foreground", children: [
|
|
1610
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Heart, { className: "h-6 w-6" }),
|
|
1611
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageCircle, { className: "h-6 w-6" }),
|
|
1612
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Send, { className: "h-6 w-6" }),
|
|
1613
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Bookmark, { className: "ml-auto h-6 w-6" })
|
|
1614
|
-
] }),
|
|
1615
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "whitespace-pre-wrap text-sm leading-snug text-foreground", children: [
|
|
1616
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "mr-1.5 font-semibold", children: handle }),
|
|
1617
|
-
toCaption(caption)
|
|
1618
|
-
] }),
|
|
1619
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: t("marketing.content.previewNow") })
|
|
1620
|
-
] })
|
|
1621
|
-
] });
|
|
1622
|
-
}
|
|
1623
|
-
function FacebookFrame({ caption, mediaUrl, accountName }) {
|
|
1624
|
-
const t = core.useTranslation();
|
|
1625
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto w-full max-w-sm overflow-hidden rounded-card border border-border bg-card", children: [
|
|
1626
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2.5", children: [
|
|
1627
|
-
/* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: accountName }),
|
|
1628
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
1629
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: accountName.replace(/^@/, "") }),
|
|
1630
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
|
|
1631
|
-
t("marketing.content.previewNow"),
|
|
1632
|
-
" \xB7 ",
|
|
1633
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Globe, { className: "h-3 w-3" })
|
|
1634
|
-
] })
|
|
1635
|
-
] }),
|
|
1636
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoreHorizontal, { className: "h-4 w-4 text-muted-foreground" })
|
|
1637
|
-
] }),
|
|
1638
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "whitespace-pre-wrap px-3 pb-2.5 text-sm leading-snug text-foreground", children: toCaption(caption) }),
|
|
1639
|
-
/* @__PURE__ */ jsxRuntime.jsx(Media, { mediaUrl, className: "aspect-auto" }),
|
|
1640
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-around border-t border-border px-3 py-2 text-muted-foreground", children: [
|
|
1641
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium", children: [
|
|
1642
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ThumbsUp, { className: "h-4 w-4" }),
|
|
1643
|
-
" ",
|
|
1644
|
-
t("marketing.content.previewLike")
|
|
1645
|
-
] }),
|
|
1646
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium", children: [
|
|
1647
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageCircle, { className: "h-4 w-4" }),
|
|
1648
|
-
" ",
|
|
1649
|
-
t("marketing.content.previewComment")
|
|
1650
|
-
] }),
|
|
1651
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium", children: [
|
|
1652
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Share2, { className: "h-4 w-4" }),
|
|
1653
|
-
" ",
|
|
1654
|
-
t("marketing.content.previewShare")
|
|
1655
|
-
] })
|
|
1656
|
-
] })
|
|
1657
|
-
] });
|
|
1658
|
-
}
|
|
1659
|
-
function LinkedinFrame({ caption, mediaUrl, accountName }) {
|
|
1660
|
-
const t = core.useTranslation();
|
|
1661
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto w-full max-w-sm overflow-hidden rounded-card border border-border bg-card", children: [
|
|
1662
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2.5", children: [
|
|
1663
|
-
/* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: accountName }),
|
|
1664
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
1665
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: accountName.replace(/^@/, "") }),
|
|
1666
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.content.previewNow") })
|
|
1667
|
-
] })
|
|
1668
|
-
] }),
|
|
1669
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "whitespace-pre-wrap px-3 pb-2.5 text-sm leading-snug text-foreground", children: toCaption(caption) }),
|
|
1670
|
-
/* @__PURE__ */ jsxRuntime.jsx(Media, { mediaUrl, className: "aspect-auto" }),
|
|
1671
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-around border-t border-border px-3 py-2 text-muted-foreground", children: [
|
|
1672
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ThumbsUp, { className: "h-4 w-4" }),
|
|
1673
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageCircle, { className: "h-4 w-4" }),
|
|
1674
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Repeat2, { className: "h-4 w-4" }),
|
|
1675
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Send, { className: "h-4 w-4" })
|
|
1676
|
-
] })
|
|
1677
|
-
] });
|
|
1678
|
-
}
|
|
1679
|
-
function XFrame({ caption, mediaUrl, accountName, accountHandle }) {
|
|
1680
|
-
const t = core.useTranslation();
|
|
1681
|
-
const handle = (accountHandle || accountName).replace(/^@/, "");
|
|
1682
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto w-full max-w-sm rounded-card border border-border bg-card p-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2.5", children: [
|
|
1683
|
-
/* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: handle }),
|
|
1684
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
1685
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "truncate text-sm text-foreground", children: [
|
|
1686
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold", children: accountName.replace(/^@/, "") }),
|
|
1687
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
|
|
1688
|
-
" @",
|
|
1689
|
-
handle,
|
|
1690
|
-
" \xB7 ",
|
|
1691
|
-
t("marketing.content.previewNow")
|
|
1692
|
-
] })
|
|
1693
|
-
] }),
|
|
1694
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 whitespace-pre-wrap text-sm leading-snug text-foreground", children: toCaption(caption) }),
|
|
1695
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 overflow-hidden rounded-xl border border-border", children: /* @__PURE__ */ jsxRuntime.jsx(Media, { mediaUrl, className: "aspect-auto" }) }),
|
|
1696
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex items-center justify-between pr-6 text-muted-foreground", children: [
|
|
1697
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageCircle, { className: "h-4 w-4" }),
|
|
1698
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Repeat2, { className: "h-4 w-4" }),
|
|
1699
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Heart, { className: "h-4 w-4" }),
|
|
1700
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.BarChart2, { className: "h-4 w-4" }),
|
|
1701
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Share2, { className: "h-4 w-4" })
|
|
1702
|
-
] })
|
|
1703
|
-
] })
|
|
1704
|
-
] }) });
|
|
1705
|
-
}
|
|
1706
|
-
var FRAMES = {
|
|
1707
|
-
instagram: InstagramFrame,
|
|
1708
|
-
facebook: FacebookFrame,
|
|
1709
|
-
linkedin: LinkedinFrame,
|
|
1710
|
-
x: XFrame
|
|
1711
|
-
};
|
|
1712
|
-
function PostSocialPreview(props) {
|
|
1713
|
-
const platforms = props.platforms.length > 0 ? props.platforms : ["instagram"];
|
|
1714
|
-
const [selected, setSelected] = React8__default.default.useState(platforms[0]);
|
|
1715
|
-
const active = platforms.includes(selected) ? selected : platforms[0];
|
|
1716
|
-
const Frame = FRAMES[active] ?? InstagramFrame;
|
|
1717
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3 rounded-md border border-border bg-muted/20 p-4", children: [
|
|
1718
|
-
platforms.length > 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center gap-1", children: platforms.map((platform) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1719
|
-
"button",
|
|
1720
|
-
{
|
|
1721
|
-
type: "button",
|
|
1722
|
-
onClick: () => setSelected(platform),
|
|
1723
|
-
"aria-pressed": active === platform,
|
|
1724
|
-
className: ui.cn(
|
|
1725
|
-
"flex items-center gap-1.5 rounded-button px-2.5 py-1.5 text-xs font-medium capitalize transition-colors",
|
|
1726
|
-
active === platform ? "bg-card text-foreground shadow-button" : "text-muted-foreground hover:text-foreground"
|
|
1727
|
-
),
|
|
1728
|
-
children: [
|
|
1729
|
-
/* @__PURE__ */ jsxRuntime.jsx(PlatformIcon, { platform, className: "h-3.5 w-3.5" }),
|
|
1730
|
-
platform
|
|
1731
|
-
]
|
|
1732
|
-
},
|
|
1733
|
-
platform
|
|
1734
|
-
)) }),
|
|
1735
|
-
/* @__PURE__ */ jsxRuntime.jsx(Frame, { ...props })
|
|
1736
|
-
] });
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
// src/views/content/templates.ts
|
|
1740
|
-
var reel = (title) => `# ${title}
|
|
1741
|
-
|
|
1742
|
-
**TEMA:** ${title.toUpperCase()}
|
|
1743
|
-
**Formato:** Reel \xB7 **Dura\xE7\xE3o:** 45 a 60 segundos
|
|
1744
|
-
|
|
1745
|
-
---
|
|
1746
|
-
|
|
1747
|
-
## TAKE 1 \u2013 GANCHO (0s a 5s)
|
|
1748
|
-
|
|
1749
|
-
*Dire\xE7\xE3o de cena: olhando para a c\xE2mera, energia alta.*
|
|
1750
|
-
|
|
1751
|
-
**Texto na tela:** \u2026
|
|
1752
|
-
|
|
1753
|
-
**Fala:** "\u2026"
|
|
1754
|
-
|
|
1755
|
-
---
|
|
1756
|
-
|
|
1757
|
-
## TAKE 2 \u2013 DESENVOLVIMENTO (5s a 40s)
|
|
1758
|
-
|
|
1759
|
-
*Dire\xE7\xE3o de cena: \u2026*
|
|
1760
|
-
|
|
1761
|
-
**Texto na tela:** \u2026
|
|
1762
|
-
|
|
1763
|
-
**Fala:** "\u2026"
|
|
1764
|
-
|
|
1765
|
-
---
|
|
1766
|
-
|
|
1767
|
-
## TAKE 3 \u2013 CTA (40s a 60s)
|
|
1768
|
-
|
|
1769
|
-
**Fala:** "\u2026"
|
|
1770
|
-
|
|
1771
|
-
**CTA:** \u2026
|
|
1772
|
-
`;
|
|
1773
|
-
var staticPost = (title) => `# ${title}
|
|
1774
|
-
|
|
1775
|
-
**TEMA:** ${title.toUpperCase()}
|
|
1776
|
-
**Formato:** Est\xE1tico
|
|
1777
|
-
|
|
1778
|
-
## Texto da arte
|
|
1779
|
-
|
|
1780
|
-
\u2026
|
|
1781
|
-
|
|
1782
|
-
## Legenda
|
|
1783
|
-
|
|
1784
|
-
\u2026
|
|
1785
|
-
|
|
1786
|
-
**CTA:** \u2026
|
|
1787
|
-
`;
|
|
1788
|
-
var carousel = (title) => `# ${title}
|
|
1789
|
-
|
|
1790
|
-
**TEMA:** ${title.toUpperCase()}
|
|
1791
|
-
**Formato:** Carrossel
|
|
1792
|
-
|
|
1793
|
-
## Slide 1 \u2014 Capa (gancho)
|
|
1794
|
-
|
|
1795
|
-
\u2026
|
|
1796
|
-
|
|
1797
|
-
## Slide 2
|
|
1798
|
-
|
|
1799
|
-
\u2026
|
|
1800
|
-
|
|
1801
|
-
## Slide 3
|
|
1802
|
-
|
|
1803
|
-
\u2026
|
|
1804
|
-
|
|
1805
|
-
## \xDAltimo slide \u2014 CTA
|
|
1806
|
-
|
|
1807
|
-
\u2026
|
|
1808
|
-
`;
|
|
1809
|
-
var story = (title) => `# ${title}
|
|
1810
|
-
|
|
1811
|
-
**TEMA:** ${title.toUpperCase()}
|
|
1812
|
-
**Formato:** Story
|
|
1813
|
-
|
|
1814
|
-
## Frame 1
|
|
1815
|
-
|
|
1816
|
-
\u2026
|
|
1817
|
-
|
|
1818
|
-
## Frame 2
|
|
1819
|
-
|
|
1820
|
-
\u2026
|
|
1821
|
-
|
|
1822
|
-
## Frame final \u2014 CTA
|
|
1823
|
-
|
|
1824
|
-
*Sticker de link / caixa de pergunta / enquete.*
|
|
1825
|
-
|
|
1826
|
-
\u2026
|
|
1827
|
-
`;
|
|
1828
|
-
var video = (title) => `# ${title}
|
|
1829
|
-
|
|
1830
|
-
**TEMA:** ${title.toUpperCase()}
|
|
1831
|
-
**Formato:** V\xEDdeo longo (YouTube) \xB7 **Dura\xE7\xE3o:** 8 a 12 minutos
|
|
1832
|
-
|
|
1833
|
-
## Gancho (0s a 30s)
|
|
1834
|
-
|
|
1835
|
-
\u2026
|
|
1836
|
-
|
|
1837
|
-
## Roteiro por blocos
|
|
1838
|
-
|
|
1839
|
-
### Bloco 1 \u2014 Contexto
|
|
1840
|
-
|
|
1841
|
-
\u2026
|
|
1842
|
-
|
|
1843
|
-
### Bloco 2 \u2014 Desenvolvimento
|
|
1844
|
-
|
|
1845
|
-
\u2026
|
|
1846
|
-
|
|
1847
|
-
### Bloco 3 \u2014 Fechamento
|
|
1848
|
-
|
|
1849
|
-
\u2026
|
|
1850
|
-
|
|
1851
|
-
**CTA:** inscreva-se / comente / link na descri\xE7\xE3o
|
|
1852
|
-
`;
|
|
1853
|
-
var live = (title) => `# ${title}
|
|
1854
|
-
|
|
1855
|
-
**TEMA:** ${title.toUpperCase()}
|
|
1856
|
-
**Formato:** Live
|
|
1857
|
-
|
|
1858
|
-
## Pauta
|
|
1859
|
-
|
|
1860
|
-
1. Abertura e aquecimento (5 min)
|
|
1861
|
-
2. \u2026
|
|
1862
|
-
3. Perguntas e respostas (15 min)
|
|
1863
|
-
|
|
1864
|
-
## Avisos / CTAs durante a live
|
|
1865
|
-
|
|
1866
|
-
\u2026
|
|
1867
|
-
`;
|
|
1868
|
-
var article = (title) => `# ${title}
|
|
1869
|
-
|
|
1870
|
-
**TEMA:** ${title.toUpperCase()}
|
|
1871
|
-
**Formato:** Artigo / Newsletter
|
|
1872
|
-
|
|
1873
|
-
## T\xEDtulo de trabalho
|
|
1874
|
-
|
|
1875
|
-
\u2026
|
|
1876
|
-
|
|
1877
|
-
## Estrutura
|
|
1878
|
-
|
|
1879
|
-
- Abertura (dor / promessa)
|
|
1880
|
-
- Desenvolvimento em 3 pontos
|
|
1881
|
-
- Conclus\xE3o + CTA
|
|
1882
|
-
|
|
1883
|
-
## Rascunho
|
|
1884
|
-
|
|
1885
|
-
\u2026
|
|
1886
|
-
`;
|
|
1887
|
-
var POST_TEMPLATES = {
|
|
1888
|
-
reel,
|
|
1889
|
-
static: staticPost,
|
|
1890
|
-
carousel,
|
|
1891
|
-
story,
|
|
1892
|
-
video,
|
|
1893
|
-
live,
|
|
1894
|
-
article
|
|
1895
|
-
};
|
|
1896
|
-
function MediaAsset({ post, onChanged }) {
|
|
1897
|
-
const t = core.useTranslation();
|
|
1898
|
-
const inputRef = React8__default.default.useRef(null);
|
|
1899
|
-
const [busy, setBusy] = React8__default.default.useState(false);
|
|
1900
|
-
const isVideo2 = post.mediaUrl ? /\.(mp4|webm|mov)($|\?)/i.test(post.mediaUrl) : false;
|
|
1901
|
-
const pick = () => inputRef.current?.click();
|
|
1902
|
-
const upload = async (file) => {
|
|
1903
|
-
setBusy(true);
|
|
1904
|
-
try {
|
|
1905
|
-
const url = await uploadPostMedia(post.id, file);
|
|
1906
|
-
onChanged(url);
|
|
1907
|
-
} finally {
|
|
1908
|
-
setBusy(false);
|
|
1909
|
-
}
|
|
1910
|
-
};
|
|
1911
|
-
if (!canUploadMedia()) {
|
|
1912
|
-
return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.content.mediaUnavailable") });
|
|
1913
|
-
}
|
|
1914
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
|
|
1915
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1916
|
-
"input",
|
|
1917
|
-
{
|
|
1918
|
-
ref: inputRef,
|
|
1919
|
-
type: "file",
|
|
1920
|
-
accept: "image/*,video/*",
|
|
1921
|
-
className: "hidden",
|
|
1922
|
-
onChange: (e) => {
|
|
1923
|
-
const file = e.target.files?.[0];
|
|
1924
|
-
if (file) void upload(file);
|
|
1925
|
-
e.target.value = "";
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
),
|
|
1929
|
-
post.mediaUrl ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative overflow-hidden rounded-card border border-border bg-muted/20", children: [
|
|
1930
|
-
isVideo2 ? /* @__PURE__ */ jsxRuntime.jsx("video", { src: post.mediaUrl, controls: true, className: "max-h-72 w-full object-contain" }) : /* @__PURE__ */ jsxRuntime.jsx("img", { src: post.mediaUrl, alt: "", className: "max-h-72 w-full object-contain" }),
|
|
1931
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-border p-2", children: [
|
|
1932
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: pick, disabled: busy, children: [
|
|
1933
|
-
busy ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "mr-1.5 h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ImagePlus, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
1934
|
-
t("marketing.content.mediaReplace")
|
|
1935
|
-
] }),
|
|
1936
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "ghost", size: "sm", onClick: () => onChanged(null), disabled: busy, children: [
|
|
1937
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "mr-1 h-3.5 w-3.5" }),
|
|
1938
|
-
t("marketing.content.mediaRemove")
|
|
1939
|
-
] })
|
|
1940
|
-
] })
|
|
1941
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1942
|
-
"button",
|
|
1943
|
-
{
|
|
1944
|
-
type: "button",
|
|
1945
|
-
onClick: pick,
|
|
1946
|
-
disabled: busy,
|
|
1947
|
-
className: "flex w-full flex-col items-center justify-center gap-2 rounded-card border border-dashed border-border p-6 text-muted-foreground transition-colors hover:bg-muted/30 hover:text-foreground",
|
|
1948
|
-
children: [
|
|
1949
|
-
busy ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-5 w-5 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ImagePlus, { className: "h-5 w-5" }),
|
|
1950
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: t("marketing.content.mediaUpload") }),
|
|
1951
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs", children: t("marketing.content.mediaHint") })
|
|
1952
|
-
]
|
|
1953
|
-
}
|
|
1954
|
-
)
|
|
1955
|
-
] });
|
|
1956
|
-
}
|
|
1957
|
-
function toDraft2(post) {
|
|
1958
|
-
return {
|
|
1959
|
-
title: post.title,
|
|
1960
|
-
format: post.format,
|
|
1961
|
-
status: post.status,
|
|
1962
|
-
scheduledDate: post.scheduledDate ?? "",
|
|
1963
|
-
platforms: post.platforms,
|
|
1964
|
-
hook: post.hook ?? "",
|
|
1965
|
-
cta: post.cta ?? "",
|
|
1966
|
-
contentMd: post.contentMd
|
|
1967
|
-
};
|
|
1968
|
-
}
|
|
1969
|
-
function PostPage({ postId, onBack }) {
|
|
1970
|
-
const t = core.useTranslation();
|
|
1971
|
-
const provider2 = useContentPlannerProvider();
|
|
1972
|
-
const storePost = useContentPlannerStore((s) => s.posts.find((p) => p.id === postId));
|
|
1973
|
-
const savePost = useContentPlannerStore((s) => s.savePost);
|
|
1974
|
-
const deletePost = useContentPlannerStore((s) => s.deletePost);
|
|
1975
|
-
const accounts = useContentPlannerStore((s) => s.accounts);
|
|
1976
|
-
const activeAccountId = useContentPlannerStore((s) => s.activeAccountId);
|
|
1977
|
-
const [post, setPost] = React8__default.default.useState(storePost ?? null);
|
|
1978
|
-
const [draft, setDraft] = React8__default.default.useState(storePost ? toDraft2(storePost) : null);
|
|
1979
|
-
const [saving, setSaving] = React8__default.default.useState(false);
|
|
1980
|
-
const [confirmDelete, setConfirmDelete] = React8__default.default.useState(false);
|
|
1981
|
-
React8__default.default.useEffect(() => {
|
|
1982
|
-
if (post?.id === postId) return;
|
|
1983
|
-
if (storePost) {
|
|
1984
|
-
setPost(storePost);
|
|
1985
|
-
setDraft(toDraft2(storePost));
|
|
1986
|
-
return;
|
|
1987
|
-
}
|
|
1988
|
-
void provider2.getPost(postId).then((fetched) => {
|
|
1989
|
-
if (fetched) {
|
|
1990
|
-
setPost(fetched);
|
|
1991
|
-
setDraft(toDraft2(fetched));
|
|
1992
|
-
}
|
|
1993
|
-
});
|
|
1994
|
-
}, [postId, storePost, post?.id, provider2]);
|
|
1995
|
-
if (!post || !draft) {
|
|
1996
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-3xl space-y-5", children: [
|
|
1997
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1998
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-56" }),
|
|
1999
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-40" })
|
|
2000
|
-
] }),
|
|
2001
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-10 w-full" }),
|
|
2002
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-3", children: Array.from({ length: 3 }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-14" }, i)) }),
|
|
2003
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-64 w-full" })
|
|
2004
|
-
] });
|
|
2005
|
-
}
|
|
2006
|
-
const dirty = draft.title !== post.title || draft.format !== post.format || draft.status !== post.status || draft.scheduledDate !== (post.scheduledDate ?? "") || draft.platforms.join(",") !== post.platforms.join(",") || draft.hook !== (post.hook ?? "") || draft.cta !== (post.cta ?? "") || draft.contentMd !== post.contentMd;
|
|
2007
|
-
const patch = (partial) => setDraft((d) => d ? { ...d, ...partial } : d);
|
|
2008
|
-
const save = async () => {
|
|
2009
|
-
setSaving(true);
|
|
2010
|
-
try {
|
|
2011
|
-
const updated = await savePost({
|
|
2012
|
-
id: post.id,
|
|
2013
|
-
title: draft.title,
|
|
2014
|
-
format: draft.format,
|
|
2015
|
-
status: draft.status,
|
|
2016
|
-
scheduledDate: draft.scheduledDate || null,
|
|
2017
|
-
platforms: draft.platforms,
|
|
2018
|
-
hook: draft.hook,
|
|
2019
|
-
cta: draft.cta,
|
|
2020
|
-
contentMd: draft.contentMd
|
|
2021
|
-
});
|
|
2022
|
-
setPost(updated);
|
|
2023
|
-
setDraft(toDraft2(updated));
|
|
2024
|
-
} finally {
|
|
2025
|
-
setSaving(false);
|
|
2026
|
-
}
|
|
2027
|
-
};
|
|
2028
|
-
const insertTemplate = () => {
|
|
2029
|
-
patch({ contentMd: POST_TEMPLATES[draft.format](draft.title || t("marketing.content.untitled")) });
|
|
2030
|
-
};
|
|
2031
|
-
const isStatic = draft.format === "static";
|
|
2032
|
-
const account = accounts.find((a) => a.id === activeAccountId) ?? null;
|
|
2033
|
-
const now = /* @__PURE__ */ new Date();
|
|
2034
|
-
const todayISO = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
2035
|
-
const isReleaseDay = !!post.scheduledDate && post.scheduledDate === todayISO;
|
|
2036
|
-
const handleMediaChanged = (mediaUrl) => {
|
|
2037
|
-
void savePost({ id: post.id, mediaUrl }).then((updated) => {
|
|
2038
|
-
setPost(updated);
|
|
2039
|
-
setDraft((d) => d ? { ...d } : d);
|
|
2040
|
-
});
|
|
2041
|
-
};
|
|
2042
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2043
|
-
ui.ContentSplit,
|
|
2044
|
-
{
|
|
2045
|
-
className: "mx-auto max-w-5xl",
|
|
2046
|
-
aside: /* @__PURE__ */ jsxRuntime.jsx(RecordingChecklist, { post }),
|
|
2047
|
-
asideTitle: t("marketing.content.checklistTitle"),
|
|
2048
|
-
asideDescription: t("marketing.content.checklistDescription"),
|
|
2049
|
-
collapsible: true,
|
|
2050
|
-
defaultCollapsed: !isReleaseDay,
|
|
2051
|
-
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-5", children: [
|
|
2052
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2053
|
-
ui.SubpageHeader,
|
|
2054
|
-
{
|
|
2055
|
-
title: draft.title || t("marketing.content.untitled"),
|
|
2056
|
-
parentLabel: t("marketing.content.title"),
|
|
2057
|
-
onBack,
|
|
2058
|
-
actions: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2059
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.ContentSplitTrigger, { label: t("marketing.content.checklistOpen") }),
|
|
2060
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", size: "sm", onClick: () => setConfirmDelete(true), children: t("marketing.content.deletePost") }),
|
|
2061
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "sm", onClick: () => void save(), disabled: !dirty || saving, children: t("marketing.content.save") })
|
|
2062
|
-
] })
|
|
2063
|
-
}
|
|
2064
|
-
),
|
|
2065
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2066
|
-
ui.Input,
|
|
2067
|
-
{
|
|
2068
|
-
value: draft.title,
|
|
2069
|
-
onChange: (e) => patch({ title: e.target.value }),
|
|
2070
|
-
placeholder: t("marketing.content.titlePlaceholder"),
|
|
2071
|
-
className: "text-base font-medium"
|
|
2072
|
-
}
|
|
2073
|
-
),
|
|
2074
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-3", children: [
|
|
2075
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
2076
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.formatLabel") }),
|
|
2077
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: draft.format, onValueChange: (v) => patch({ format: v }), children: [
|
|
2078
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectTrigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.SelectValue, {}) }),
|
|
2079
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectContent, { children: POST_FORMATS.map((f) => /* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: f, children: t(`marketing.content.format.${f}`) }, f)) })
|
|
2080
|
-
] })
|
|
2081
|
-
] }),
|
|
2082
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
2083
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.statusLabel") }),
|
|
2084
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: draft.status, onValueChange: (v) => patch({ status: v }), children: [
|
|
2085
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectTrigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.SelectValue, {}) }),
|
|
2086
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectContent, { children: POST_STATUS_ORDER.map((s) => /* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: s, children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [
|
|
2087
|
-
/* @__PURE__ */ jsxRuntime.jsx(StatusDot, { status: s }),
|
|
2088
|
-
t(`marketing.content.status.${s}`)
|
|
2089
|
-
] }) }, s)) })
|
|
2090
|
-
] })
|
|
2091
|
-
] }),
|
|
2092
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
2093
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.scheduledDate") }),
|
|
2094
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2095
|
-
ui.Input,
|
|
2096
|
-
{
|
|
2097
|
-
type: "date",
|
|
2098
|
-
value: draft.scheduledDate,
|
|
2099
|
-
onChange: (e) => patch({ scheduledDate: e.target.value })
|
|
2100
|
-
}
|
|
2101
|
-
)
|
|
2102
|
-
] }),
|
|
2103
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1 sm:col-span-2", children: [
|
|
2104
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.hook") }),
|
|
2105
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2106
|
-
ui.Input,
|
|
2107
|
-
{
|
|
2108
|
-
value: draft.hook,
|
|
2109
|
-
onChange: (e) => patch({ hook: e.target.value }),
|
|
2110
|
-
placeholder: t("marketing.content.hookPlaceholder")
|
|
2111
|
-
}
|
|
2112
|
-
)
|
|
2113
|
-
] }),
|
|
2114
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
2115
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.cta") }),
|
|
2116
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2117
|
-
ui.Input,
|
|
2118
|
-
{
|
|
2119
|
-
value: draft.cta,
|
|
2120
|
-
onChange: (e) => patch({ cta: e.target.value }),
|
|
2121
|
-
placeholder: t("marketing.content.ctaPlaceholder")
|
|
2122
|
-
}
|
|
2123
|
-
)
|
|
2124
|
-
] }),
|
|
2125
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1 sm:col-span-3", children: [
|
|
2126
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.platformsLabel") }),
|
|
2127
|
-
/* @__PURE__ */ jsxRuntime.jsx(PlatformPicker, { value: draft.platforms, onChange: (platforms) => patch({ platforms }) })
|
|
2128
|
-
] })
|
|
2129
|
-
] }),
|
|
2130
|
-
isStatic && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
2131
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.mediaLabel") }),
|
|
2132
|
-
/* @__PURE__ */ jsxRuntime.jsx(MediaAsset, { post, onChanged: handleMediaChanged })
|
|
2133
|
-
] }),
|
|
2134
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
2135
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
2136
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: isStatic ? t("marketing.content.captionLabel") : t("marketing.content.scriptLabel") }),
|
|
2137
|
-
draft.contentMd.trim() === "" && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", size: "sm", onClick: insertTemplate, children: t("marketing.content.insertTemplate") })
|
|
2138
|
-
] }),
|
|
2139
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2140
|
-
ui.MarkdownEditor,
|
|
2141
|
-
{
|
|
2142
|
-
value: draft.contentMd,
|
|
2143
|
-
onChange: (contentMd) => patch({ contentMd }),
|
|
2144
|
-
placeholder: isStatic ? t("marketing.content.captionPlaceholder") : t("marketing.content.bodyPlaceholder"),
|
|
2145
|
-
editLabel: t("marketing.content.edit"),
|
|
2146
|
-
previewLabel: t("marketing.content.preview"),
|
|
2147
|
-
renderPreview: isStatic ? (value) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2148
|
-
PostSocialPreview,
|
|
2149
|
-
{
|
|
2150
|
-
caption: value,
|
|
2151
|
-
mediaUrl: post.mediaUrl,
|
|
2152
|
-
accountName: account?.name ?? "@conta",
|
|
2153
|
-
accountHandle: account?.handle,
|
|
2154
|
-
platforms: draft.platforms.length > 0 ? draft.platforms : account?.platforms ?? []
|
|
2155
|
-
}
|
|
2156
|
-
) : void 0
|
|
2157
|
-
}
|
|
2158
|
-
)
|
|
2159
|
-
] }),
|
|
2160
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2161
|
-
ui.ConfirmDialog,
|
|
2162
|
-
{
|
|
2163
|
-
open: confirmDelete,
|
|
2164
|
-
variant: "destructive",
|
|
2165
|
-
title: t("marketing.content.deletePost"),
|
|
2166
|
-
description: t("marketing.content.deletePostConfirm"),
|
|
2167
|
-
confirmLabel: t("marketing.content.deletePost"),
|
|
2168
|
-
cancelLabel: t("marketing.content.cancel"),
|
|
2169
|
-
onCancel: () => setConfirmDelete(false),
|
|
2170
|
-
onConfirm: () => {
|
|
2171
|
-
setConfirmDelete(false);
|
|
2172
|
-
void deletePost(post.id).then(onBack);
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
)
|
|
2176
|
-
] })
|
|
2177
|
-
},
|
|
2178
|
-
post.id
|
|
2179
|
-
);
|
|
2180
|
-
}
|
|
2181
|
-
|
|
2182
|
-
// src/data/blogEntities.tsx
|
|
2183
|
-
var BLOG_POSTS_TABLE = "plg_blog_posts";
|
|
2184
|
-
var BLOG_CATEGORIES_TABLE = "plg_blog_categories";
|
|
2185
|
-
function buildBlogPostEntity() {
|
|
2186
|
-
const fields = [
|
|
2187
|
-
{ key: "title", label: "Title", type: "text", required: true, searchable: true, showInTable: true, sortable: true },
|
|
2188
|
-
{ key: "slug", label: "Slug", type: "text", required: true, searchable: true, showInTable: false, hint: 'URL identifier, e.g. "cannabis-and-oral-health"' },
|
|
2189
|
-
{ key: "status", label: "Status", type: "select", options: ["draft", "published"], showInTable: true, defaultValue: "draft" },
|
|
2190
|
-
{ key: "categoryId", label: "Category", type: "relation", relation: { table: BLOG_CATEGORIES_TABLE, valueField: "id", labelField: "name", tenantScoped: true }, showInTable: false },
|
|
2191
|
-
{ key: "excerpt", label: "Excerpt", type: "textarea", span: 2, showInTable: false, placeholder: "Short summary shown on cards" },
|
|
2192
|
-
{ key: "body", label: "Body", type: "markdown", span: 2, showInTable: false },
|
|
2193
|
-
{ key: "coverImage", label: "Cover image", type: "image", showInTable: false, placeholder: "https://\u2026" },
|
|
2194
|
-
{ key: "readTime", label: "Read time", type: "text", showInTable: false, placeholder: "5 min" },
|
|
2195
|
-
{ key: "publishedAt", label: "Published at", type: "date", showInTable: true, sortable: true },
|
|
2196
|
-
{ key: "authorName", label: "Author name", type: "text", showInTable: false },
|
|
2197
|
-
{ key: "authorRole", label: "Author role", type: "text", showInTable: false, placeholder: "e.g. Cirurgi\xE3o-Dentista" },
|
|
2198
|
-
{ key: "authorAvatarUrl", label: "Author avatar", type: "image", showInTable: false, placeholder: "https://\u2026" },
|
|
2199
|
-
{ key: "authorBio", label: "Author bio", type: "textarea", span: 2, showInTable: false }
|
|
2200
|
-
];
|
|
2201
|
-
return {
|
|
2202
|
-
name: "Post",
|
|
2203
|
-
namePlural: "Posts",
|
|
2204
|
-
icon: "Newspaper",
|
|
2205
|
-
fields,
|
|
2206
|
-
displayField: "title",
|
|
2207
|
-
subtitleField: "slug",
|
|
2208
|
-
facets: [{ field: "status", allLabel: "All" }],
|
|
2209
|
-
data: {
|
|
2210
|
-
table: BLOG_POSTS_TABLE,
|
|
2211
|
-
tenantScoped: true,
|
|
2212
|
-
searchColumns: ["title", "slug", "excerpt"]
|
|
2213
|
-
}
|
|
2214
|
-
};
|
|
2215
|
-
}
|
|
2216
|
-
function buildBlogCategoryEntity() {
|
|
2217
|
-
const fields = [
|
|
2218
|
-
{ key: "name", label: "Name", type: "text", required: true, searchable: true, showInTable: true, sortable: true },
|
|
2219
|
-
{ key: "slug", label: "Slug", type: "text", required: true, searchable: true, showInTable: true, hint: "URL identifier" },
|
|
2220
|
-
{ key: "description", label: "Description", type: "textarea", span: 2, showInTable: false }
|
|
2221
|
-
];
|
|
2222
|
-
return {
|
|
2223
|
-
name: "Category",
|
|
2224
|
-
namePlural: "Categories",
|
|
2225
|
-
icon: "Tags",
|
|
2226
|
-
fields,
|
|
2227
|
-
displayField: "name",
|
|
2228
|
-
data: {
|
|
2229
|
-
table: BLOG_CATEGORIES_TABLE,
|
|
2230
|
-
tenantScoped: true,
|
|
2231
|
-
searchColumns: ["name", "slug"]
|
|
2232
|
-
}
|
|
2233
|
-
};
|
|
2234
|
-
}
|
|
2235
|
-
function useEntityCrud(entity) {
|
|
2236
|
-
const provider2 = React8.useMemo(() => core.resolveDataProvider(entity), [entity]);
|
|
2237
|
-
const [items, setItems] = React8.useState(null);
|
|
2238
|
-
const [total, setTotal] = React8.useState(0);
|
|
2239
|
-
const fetch = React8.useCallback(async (query = {}) => {
|
|
2240
|
-
try {
|
|
2241
|
-
const res = await provider2.list(query);
|
|
2242
|
-
setItems(res.data);
|
|
2243
|
-
setTotal(res.total);
|
|
2244
|
-
} catch (err) {
|
|
2245
|
-
console.warn("[blog] list failed; showing empty. Apply the plugin-blog migration to this pool.", err);
|
|
2246
|
-
setItems([]);
|
|
2247
|
-
setTotal(0);
|
|
2248
|
-
}
|
|
2249
|
-
}, [provider2]);
|
|
2250
|
-
const getById = React8.useCallback(async (id) => {
|
|
2251
|
-
const res = await provider2.list({ filters: { id }, page: 1, pageSize: 1 });
|
|
2252
|
-
return res.data[0] ?? null;
|
|
2253
|
-
}, [provider2]);
|
|
2254
|
-
const create = React8.useCallback((data) => provider2.create(data), [provider2]);
|
|
2255
|
-
const update = React8.useCallback((id, data) => provider2.update(id, data), [provider2]);
|
|
2256
|
-
const remove = React8.useCallback((id) => provider2.remove(id), [provider2]);
|
|
2257
|
-
return { items, total, fetch, getById, create, update, remove };
|
|
2258
|
-
}
|
|
2259
|
-
function PostsList({ navigate }) {
|
|
2260
|
-
const entity = React8.useMemo(() => buildBlogPostEntity(), []);
|
|
2261
|
-
const { items, total, fetch } = useEntityCrud(entity);
|
|
2262
|
-
const [search, setSearch] = React8.useState("");
|
|
2263
|
-
const [statusFilter, setStatusFilter] = React8.useState();
|
|
2264
|
-
const [loadedOnce, setLoadedOnce] = React8.useState(false);
|
|
2265
|
-
React8.useEffect(() => {
|
|
2266
|
-
let active = true;
|
|
2267
|
-
Promise.resolve(
|
|
2268
|
-
fetch({ search: search || void 0, filters: statusFilter ? { status: statusFilter } : void 0 })
|
|
2269
|
-
).finally(() => {
|
|
2270
|
-
if (active) setLoadedOnce(true);
|
|
2271
|
-
});
|
|
2272
|
-
return () => {
|
|
2273
|
-
active = false;
|
|
2274
|
-
};
|
|
2275
|
-
}, [search, statusFilter, fetch]);
|
|
2276
|
-
const facets = React8.useMemo(() => [{
|
|
2277
|
-
field: "status",
|
|
2278
|
-
allLabel: "All",
|
|
2279
|
-
options: [{ value: "draft", label: "Draft" }, { value: "published", label: "Published" }]
|
|
2280
|
-
}], []);
|
|
2281
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2282
|
-
saas.CrudListView,
|
|
2283
|
-
{
|
|
2284
|
-
entityDef: entity,
|
|
2285
|
-
items: loadedOnce ? items : null,
|
|
2286
|
-
total,
|
|
2287
|
-
search,
|
|
2288
|
-
onSearchChange: setSearch,
|
|
2289
|
-
searchPlaceholder: "Search posts\u2026",
|
|
2290
|
-
facets,
|
|
2291
|
-
activeFilters: { status: statusFilter },
|
|
2292
|
-
onFacetChange: (_field, value) => setStatusFilter(value),
|
|
2293
|
-
onNew: () => navigate("blog-new"),
|
|
2294
|
-
addLabel: "New post",
|
|
2295
|
-
onRowClick: (row) => navigate(`blog-post:${row.id}`)
|
|
2296
|
-
}
|
|
2297
|
-
);
|
|
2298
|
-
}
|
|
2299
|
-
function CategoriesList({ navigate }) {
|
|
2300
|
-
const entity = React8.useMemo(() => buildBlogCategoryEntity(), []);
|
|
2301
|
-
const { items, total, fetch } = useEntityCrud(entity);
|
|
2302
|
-
const [search, setSearch] = React8.useState("");
|
|
2303
|
-
const [loadedOnce, setLoadedOnce] = React8.useState(false);
|
|
2304
|
-
React8.useEffect(() => {
|
|
2305
|
-
let active = true;
|
|
2306
|
-
Promise.resolve(fetch({ search: search || void 0 })).finally(() => {
|
|
2307
|
-
if (active) setLoadedOnce(true);
|
|
2308
|
-
});
|
|
2309
|
-
return () => {
|
|
2310
|
-
active = false;
|
|
2311
|
-
};
|
|
2312
|
-
}, [search, fetch]);
|
|
2313
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2314
|
-
saas.CrudListView,
|
|
2315
|
-
{
|
|
2316
|
-
entityDef: entity,
|
|
2317
|
-
items: loadedOnce ? items : null,
|
|
2318
|
-
total,
|
|
2319
|
-
search,
|
|
2320
|
-
onSearchChange: setSearch,
|
|
2321
|
-
searchPlaceholder: "Search categories\u2026",
|
|
2322
|
-
onNew: () => navigate("blog-cat-new"),
|
|
2323
|
-
addLabel: "New category",
|
|
2324
|
-
onRowClick: (row) => navigate(`blog-cat:${row.id}`)
|
|
2325
|
-
}
|
|
2326
|
-
);
|
|
2327
|
-
}
|
|
2328
|
-
function BlogAdminView({ navigate }) {
|
|
2329
|
-
const { labels } = useMarketingConfig();
|
|
2330
|
-
const [lens, setLens] = React8.useState("posts");
|
|
2331
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
2332
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2333
|
-
ui.SegmentedControl,
|
|
2334
|
-
{
|
|
2335
|
-
"aria-label": "Blog section",
|
|
2336
|
-
options: [
|
|
2337
|
-
{ value: "posts", label: labels.blog },
|
|
2338
|
-
{ value: "categories", label: labels.blogCategories }
|
|
2339
|
-
],
|
|
2340
|
-
value: lens,
|
|
2341
|
-
onChange: (v) => setLens(v)
|
|
2342
|
-
}
|
|
2343
|
-
),
|
|
2344
|
-
lens === "posts" ? /* @__PURE__ */ jsxRuntime.jsx(PostsList, { navigate }) : /* @__PURE__ */ jsxRuntime.jsx(CategoriesList, { navigate })
|
|
2345
|
-
] });
|
|
2346
|
-
}
|
|
2347
|
-
function BlogEntityForm({
|
|
2348
|
-
entity,
|
|
2349
|
-
editId,
|
|
2350
|
-
parentLabel,
|
|
2351
|
-
onDone
|
|
2352
|
-
}) {
|
|
2353
|
-
const { getById, create, update, remove } = useEntityCrud(entity);
|
|
2354
|
-
const isEdit = !!editId;
|
|
2355
|
-
const namePlural = entity.namePlural ?? entity.name + "s";
|
|
2356
|
-
const [initialData, setInitialData] = React8__default.default.useState(isEdit ? null : {});
|
|
2357
|
-
React8__default.default.useEffect(() => {
|
|
2358
|
-
if (!editId) return;
|
|
2359
|
-
let cancelled = false;
|
|
2360
|
-
(async () => {
|
|
2361
|
-
const item = await getById(editId);
|
|
2362
|
-
if (!cancelled) setInitialData(item ?? {});
|
|
2363
|
-
})();
|
|
2364
|
-
return () => {
|
|
2365
|
-
cancelled = true;
|
|
2366
|
-
};
|
|
2367
|
-
}, [editId, getById]);
|
|
2368
|
-
async function save(values) {
|
|
2369
|
-
try {
|
|
2370
|
-
if (isEdit) await update(editId, values);
|
|
2371
|
-
else await create(values);
|
|
2372
|
-
ui.toast.success(isEdit ? `${entity.name} updated` : `${entity.name} created`);
|
|
2373
|
-
onDone();
|
|
2374
|
-
} catch (err) {
|
|
2375
|
-
ui.toast.error(`Failed to save ${entity.name.toLowerCase()}`);
|
|
2376
|
-
throw err;
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
async function del() {
|
|
2380
|
-
if (!editId) return;
|
|
2381
|
-
try {
|
|
2382
|
-
await remove(editId);
|
|
2383
|
-
ui.toast.success(`${entity.name} deleted`);
|
|
2384
|
-
onDone();
|
|
2385
|
-
} catch {
|
|
2386
|
-
ui.toast.error(`Failed to delete ${entity.name.toLowerCase()}`);
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
const title = isEdit ? initialData?.[entity.displayField ?? "name"] ?? entity.name : `New ${entity.name.toLowerCase()}`;
|
|
2390
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
2391
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2392
|
-
ui.SubpageHeader,
|
|
2393
|
-
{
|
|
2394
|
-
title: String(title),
|
|
2395
|
-
onBack: onDone,
|
|
2396
|
-
parentLabel,
|
|
2397
|
-
actions: isEdit ? /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", className: "text-destructive hover:text-destructive", onClick: del, children: "Delete" }) : void 0
|
|
2398
|
-
}
|
|
2399
|
-
),
|
|
2400
|
-
initialData !== null && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2401
|
-
saas.CrudFormPage,
|
|
2402
|
-
{
|
|
2403
|
-
entityDef: entity,
|
|
2404
|
-
mode: isEdit ? "edit" : "create",
|
|
2405
|
-
initialData,
|
|
2406
|
-
namePlural,
|
|
2407
|
-
onCancel: onDone,
|
|
2408
|
-
onSubmit: save,
|
|
2409
|
-
hideBreadcrumb: true
|
|
2410
|
-
}
|
|
2411
|
-
)
|
|
2412
|
-
] });
|
|
2413
|
-
}
|
|
2414
|
-
function buildNav(config, view, navigate) {
|
|
2415
|
-
const items = [
|
|
2416
|
-
{ id: "overview", label: config.labels.overview, icon: "BarChart3", active: view === "overview", onClick: () => navigate("overview") }
|
|
2417
|
-
];
|
|
2418
|
-
if (config.modules.channels) {
|
|
2419
|
-
items.push({
|
|
2420
|
-
id: "channels",
|
|
2421
|
-
label: config.labels.channels,
|
|
2422
|
-
icon: "Radio",
|
|
2423
|
-
active: view === "channels" || view.startsWith("channel-detail:"),
|
|
2424
|
-
onClick: () => navigate("channels")
|
|
2425
|
-
});
|
|
2426
|
-
}
|
|
2427
|
-
if (config.modules.campaigns) {
|
|
2428
|
-
items.push({ id: "campaigns", label: config.labels.campaigns, icon: "Megaphone", active: view === "campaigns", onClick: () => navigate("campaigns") });
|
|
2429
|
-
}
|
|
2430
|
-
if (config.modules.funnel) {
|
|
2431
|
-
items.push({ id: "funnel", label: config.labels.funnel, icon: "Filter", active: view === "funnel", onClick: () => navigate("funnel") });
|
|
2432
|
-
}
|
|
2433
|
-
if (config.modules.landingPages) {
|
|
2434
|
-
items.push({ id: "landing-pages", label: config.labels.landingPages, icon: "LayoutTemplate", active: view === "landing-pages", onClick: () => navigate("landing-pages") });
|
|
2435
|
-
}
|
|
2436
|
-
if (config.modules.contentPlanner) {
|
|
2437
|
-
items.push({
|
|
2438
|
-
id: "content",
|
|
2439
|
-
label: config.labels.content,
|
|
2440
|
-
icon: "Clapperboard",
|
|
2441
|
-
active: view === "content" || view === "content-brief" || view.startsWith("content-post:"),
|
|
2442
|
-
onClick: () => navigate("content")
|
|
2443
|
-
});
|
|
2444
|
-
}
|
|
2445
|
-
if (config.modules.blog) {
|
|
2446
|
-
items.push({
|
|
2447
|
-
id: "blog",
|
|
2448
|
-
label: config.labels.blog,
|
|
2449
|
-
icon: "Newspaper",
|
|
2450
|
-
active: view === "blog" || view === "blog-new" || view.startsWith("blog-post:") || view === "blog-cat-new" || view.startsWith("blog-cat:"),
|
|
2451
|
-
onClick: () => navigate("blog")
|
|
2452
|
-
});
|
|
2453
|
-
}
|
|
2454
|
-
return items;
|
|
2455
|
-
}
|
|
2456
|
-
function MarketingPage({ config, provider: provider2, store, contentProvider, contentStore }) {
|
|
2457
|
-
const t = core.useTranslation();
|
|
2458
|
-
const { view, direction, navigate } = saas.useModuleNavigation("/marketing", {
|
|
2459
|
-
overview: 0,
|
|
2460
|
-
channels: 0,
|
|
2461
|
-
campaigns: 0,
|
|
2462
|
-
funnel: 0,
|
|
2463
|
-
"landing-pages": 0,
|
|
2464
|
-
content: 0,
|
|
2465
|
-
blog: 0,
|
|
2466
|
-
"channel-detail": 1,
|
|
2467
|
-
"content-post": 1,
|
|
2468
|
-
"content-brief": 1,
|
|
2469
|
-
"blog-new": 1,
|
|
2470
|
-
"blog-post": 1,
|
|
2471
|
-
"blog-cat-new": 1,
|
|
2472
|
-
"blog-cat": 1
|
|
2473
|
-
}, "overview");
|
|
2474
|
-
React8__default.default.useEffect(() => {
|
|
2475
|
-
void store.getState().load();
|
|
2476
|
-
}, [store]);
|
|
2477
|
-
const isOverview = view === "overview";
|
|
2478
|
-
const nav = buildNav(config, view, navigate);
|
|
2479
|
-
const blogPostEntity = React8__default.default.useMemo(() => buildBlogPostEntity(), []);
|
|
2480
|
-
const blogCategoryEntity = React8__default.default.useMemo(() => buildBlogCategoryEntity(), []);
|
|
2481
|
-
const quickActions = React8__default.default.useMemo(() => [
|
|
2482
|
-
{ id: "new-campaign", label: t("marketing.campaigns.new"), icon: "Megaphone", description: config.labels.campaigns, action: () => navigate("campaigns") }
|
|
2483
|
-
], [t, config.labels.campaigns]);
|
|
2484
|
-
const renderView = saas.createViewRouter([
|
|
2485
|
-
{ id: "channels", render: () => /* @__PURE__ */ jsxRuntime.jsx(ChannelsView, { onOpen: (id) => navigate(`channel-detail:${id}`) }) },
|
|
2486
|
-
{ id: "channel-detail", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(ChannelDetailView, { channelId: id, onBack: () => navigate("channels") }) },
|
|
2487
|
-
{ id: "campaigns", render: () => /* @__PURE__ */ jsxRuntime.jsx(CampaignsView, {}) },
|
|
2488
|
-
{ id: "funnel", render: () => /* @__PURE__ */ jsxRuntime.jsx(FunnelView, {}) },
|
|
2489
|
-
{ id: "landing-pages", render: () => /* @__PURE__ */ jsxRuntime.jsx(LandingPagesView, {}) },
|
|
2490
|
-
{ id: "content", render: () => /* @__PURE__ */ jsxRuntime.jsx(ContentView, { onOpenPost: (id) => navigate(`content-post:${id}`) }) },
|
|
2491
|
-
{ id: "content-post", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(PostPage, { postId: id, onBack: () => navigate("content") }) },
|
|
2492
|
-
{ id: "content-brief", render: () => /* @__PURE__ */ jsxRuntime.jsx(PlanBriefView, { onBack: () => navigate("content") }) },
|
|
2493
|
-
{ id: "blog", render: () => /* @__PURE__ */ jsxRuntime.jsx(BlogAdminView, { navigate }) },
|
|
2494
|
-
{ id: "blog-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogPostEntity, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
|
|
2495
|
-
{ id: "blog-post", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogPostEntity, editId: id, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
|
|
2496
|
-
{ id: "blog-cat-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogCategoryEntity, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
|
|
2497
|
-
{ id: "blog-cat", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogCategoryEntity, editId: id, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
|
|
2498
|
-
{ id: "overview", render: () => /* @__PURE__ */ jsxRuntime.jsx(OverviewView, {}) }
|
|
2499
|
-
], "overview");
|
|
2500
|
-
return /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config, provider: provider2, store, children: /* @__PURE__ */ jsxRuntime.jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2501
|
-
ui.ModulePage,
|
|
2502
|
-
{
|
|
2503
|
-
title: config.labels.pageTitle,
|
|
2504
|
-
subtitle: config.labels.pageSubtitle,
|
|
2505
|
-
nav,
|
|
2506
|
-
showHeader: isOverview,
|
|
2507
|
-
viewKey: view,
|
|
2508
|
-
direction,
|
|
2509
|
-
headerAction: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2510
|
-
saas.ModuleActionBar,
|
|
2511
|
-
{
|
|
2512
|
-
quickActions,
|
|
2513
|
-
settingsPath: "/settings/marketing",
|
|
2514
|
-
settingsLabel: config.labels.pageTitle
|
|
2515
|
-
}
|
|
2516
|
-
),
|
|
2517
|
-
children: renderView(view)
|
|
2518
|
-
}
|
|
2519
|
-
) }) });
|
|
2520
|
-
}
|
|
2521
|
-
var SOURCE_OPTIONS = [
|
|
2522
|
-
{ value: "crm", label: "CRM" },
|
|
2523
|
-
{ value: "agenda", label: "Agenda" },
|
|
2524
|
-
{ value: "orders", label: "Orders" },
|
|
2525
|
-
{ value: "custom", label: "Custom" }
|
|
2526
|
-
];
|
|
2527
|
-
function SettingsView() {
|
|
2528
|
-
const t = core.useTranslation();
|
|
2529
|
-
const { conversion, channels } = useMarketingConfig();
|
|
2530
|
-
const [source, setSource] = React8__default.default.useState(conversion.source);
|
|
2531
|
-
const [trackValue, setTrackValue] = React8__default.default.useState(true);
|
|
2532
|
-
const [autoSync, setAutoSync] = React8__default.default.useState(true);
|
|
2533
|
-
const [assisted, setAssisted] = React8__default.default.useState(false);
|
|
2534
|
-
const [tracked, setTracked] = React8__default.default.useState(
|
|
2535
|
-
() => Object.fromEntries(channels.map((c) => [c.id, true]))
|
|
2536
|
-
);
|
|
2537
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
2538
|
-
/* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t("marketing.settings.conversionModel"), description: t("marketing.settings.conversionModelDesc"), children: [
|
|
2539
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2540
|
-
saas.SelectRow,
|
|
2541
|
-
{
|
|
2542
|
-
label: t("marketing.settings.attributedFrom"),
|
|
2543
|
-
description: conversion.label,
|
|
2544
|
-
value: source,
|
|
2545
|
-
options: SOURCE_OPTIONS,
|
|
2546
|
-
onChange: (v) => setSource(v)
|
|
2547
|
-
}
|
|
2548
|
-
),
|
|
2549
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2550
|
-
saas.ToggleRow,
|
|
2551
|
-
{
|
|
2552
|
-
label: t("marketing.settings.trackValue"),
|
|
2553
|
-
description: t("marketing.settings.trackValueDesc"),
|
|
2554
|
-
checked: trackValue,
|
|
2555
|
-
onChange: setTrackValue
|
|
2556
|
-
}
|
|
2557
|
-
)
|
|
2558
|
-
] }),
|
|
2559
|
-
/* @__PURE__ */ jsxRuntime.jsx(saas.SettingsGroup, { title: t("marketing.settings.channels"), description: t("marketing.settings.channelsDesc"), children: channels.map((c) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2560
|
-
saas.ToggleRow,
|
|
2561
|
-
{
|
|
2562
|
-
label: c.label,
|
|
2563
|
-
description: t("marketing.settings.channelTrackDesc"),
|
|
2564
|
-
checked: tracked[c.id] ?? true,
|
|
2565
|
-
onChange: (v) => setTracked((prev) => ({ ...prev, [c.id]: v }))
|
|
2566
|
-
},
|
|
2567
|
-
c.id
|
|
2568
|
-
)) }),
|
|
2569
|
-
/* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t("marketing.settings.attribution"), description: t("marketing.settings.attributionDesc"), children: [
|
|
2570
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2571
|
-
saas.ToggleRow,
|
|
2572
|
-
{
|
|
2573
|
-
label: t("marketing.settings.autoSync"),
|
|
2574
|
-
description: t("marketing.settings.autoSyncDesc"),
|
|
2575
|
-
checked: autoSync,
|
|
2576
|
-
onChange: setAutoSync
|
|
2577
|
-
}
|
|
2578
|
-
),
|
|
2579
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2580
|
-
saas.ToggleRow,
|
|
2581
|
-
{
|
|
2582
|
-
label: t("marketing.settings.assisted"),
|
|
2583
|
-
description: t("marketing.settings.assistedDesc"),
|
|
2584
|
-
checked: assisted,
|
|
2585
|
-
onChange: setAssisted
|
|
2586
|
-
}
|
|
2587
|
-
)
|
|
2588
|
-
] })
|
|
2589
|
-
] });
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2592
|
-
// src/registries.ts
|
|
2593
|
-
var KIND_OPTIONS = [
|
|
2594
|
-
{ value: "paid", label: "Paid" },
|
|
2595
|
-
{ value: "organic", label: "Organic" },
|
|
2596
|
-
{ value: "social", label: "Social" },
|
|
2597
|
-
{ value: "referral", label: "Referral" },
|
|
2598
|
-
{ value: "direct", label: "Direct" },
|
|
2599
|
-
{ value: "outbound", label: "Outbound" }
|
|
2600
|
-
];
|
|
2601
|
-
var channelEntity = {
|
|
2602
|
-
name: "Channel",
|
|
2603
|
-
namePlural: "Channels",
|
|
2604
|
-
icon: "Radio",
|
|
2605
|
-
displayField: "label",
|
|
2606
|
-
defaultSort: "label",
|
|
2607
|
-
fields: [
|
|
2608
|
-
{ key: "label", label: "Channel", type: "text", required: true, showInTable: true, searchable: true },
|
|
2609
|
-
{ key: "kind", label: "Type", type: "select", options: KIND_OPTIONS, showInTable: true },
|
|
2610
|
-
{ key: "icon", label: "Icon", type: "text", showInTable: true }
|
|
2611
|
-
],
|
|
2612
|
-
facets: [{ field: "kind" }]
|
|
2613
|
-
};
|
|
2614
|
-
function buildMarketingRegistries(channels) {
|
|
2615
|
-
return [
|
|
2616
|
-
{
|
|
2617
|
-
id: "channels",
|
|
2618
|
-
entity: channelEntity,
|
|
2619
|
-
icon: "Radio",
|
|
2620
|
-
description: "Acquisition channels tracked for this workspace",
|
|
2621
|
-
display: "table",
|
|
2622
|
-
readOnly: true,
|
|
2623
|
-
seedData: channels.map((c) => ({ id: c.id, label: c.label, kind: c.kind, icon: c.icon }))
|
|
2624
|
-
}
|
|
2625
|
-
];
|
|
2626
|
-
}
|
|
2627
|
-
function useEnsureMarketingData() {
|
|
2628
|
-
const overview = useMarketingStore((s) => s.overview);
|
|
2629
|
-
const loading = useMarketingStore((s) => s.loading);
|
|
2630
|
-
const load = useMarketingStore((s) => s.load);
|
|
2631
|
-
React8.useEffect(() => {
|
|
2632
|
-
if (!overview && !loading) void load();
|
|
2633
|
-
}, []);
|
|
2634
|
-
}
|
|
2635
|
-
function ConversionsKpi() {
|
|
2636
|
-
const { conversion } = useMarketingConfig();
|
|
2637
|
-
const overview = useMarketingStore((s) => s.overview);
|
|
2638
|
-
useEnsureMarketingData();
|
|
2639
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2640
|
-
ui.KpiCard,
|
|
2641
|
-
{
|
|
2642
|
-
icon: lucideReact.Target,
|
|
2643
|
-
label: conversion.labelPlural,
|
|
2644
|
-
value: formatNumber(overview?.conversions ?? 0),
|
|
2645
|
-
current: overview?.conversions,
|
|
2646
|
-
previous: overview?.conversionsPrev
|
|
2647
|
-
}
|
|
2648
|
-
);
|
|
2649
|
-
}
|
|
2650
|
-
function ConversionRateKpi() {
|
|
2651
|
-
const t = core.useTranslation();
|
|
2652
|
-
const overview = useMarketingStore((s) => s.overview);
|
|
2653
|
-
useEnsureMarketingData();
|
|
2654
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2655
|
-
ui.KpiCard,
|
|
2656
|
-
{
|
|
2657
|
-
icon: lucideReact.Percent,
|
|
2658
|
-
label: t("marketing.metric.conversionRate"),
|
|
2659
|
-
value: formatPercent(overview?.cvr ?? 0),
|
|
2660
|
-
current: overview?.cvr,
|
|
2661
|
-
previous: overview?.cvrPrev
|
|
2662
|
-
}
|
|
2663
|
-
);
|
|
2664
|
-
}
|
|
2665
|
-
function CpaKpi() {
|
|
2666
|
-
const t = core.useTranslation();
|
|
2667
|
-
const { currency } = useMarketingConfig();
|
|
2668
|
-
const overview = useMarketingStore((s) => s.overview);
|
|
2669
|
-
useEnsureMarketingData();
|
|
2670
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2671
|
-
ui.KpiCard,
|
|
2672
|
-
{
|
|
2673
|
-
icon: lucideReact.DollarSign,
|
|
2674
|
-
label: t("marketing.metric.cpa"),
|
|
2675
|
-
value: overview && overview.cpa > 0 ? formatCurrency(overview.cpa, currency) : "\u2014",
|
|
2676
|
-
sub: `${formatCurrency(overview?.spend ?? 0, currency)} ${t("marketing.metric.spend")}`
|
|
2677
|
-
}
|
|
2678
|
-
);
|
|
2679
|
-
}
|
|
2680
|
-
function TopChannelKpi() {
|
|
2681
|
-
const t = core.useTranslation();
|
|
2682
|
-
const { conversion, currency } = useMarketingConfig();
|
|
2683
|
-
const overview = useMarketingStore((s) => s.overview);
|
|
2684
|
-
const lookup = useChannelLookup();
|
|
2685
|
-
useEnsureMarketingData();
|
|
2686
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2687
|
-
ui.KpiCard,
|
|
2688
|
-
{
|
|
2689
|
-
icon: lucideReact.Trophy,
|
|
2690
|
-
label: t("marketing.metric.topChannel"),
|
|
2691
|
-
value: overview?.topChannelId ? lookup.get(overview.topChannelId)?.label ?? "\u2014" : "\u2014",
|
|
2692
|
-
sub: `${conversion.valueLabel}: ${formatCurrency(overview?.value ?? 0, currency)}`
|
|
2693
|
-
}
|
|
2694
|
-
);
|
|
2695
|
-
}
|
|
2696
|
-
function ChannelMixPanel() {
|
|
2697
|
-
const t = core.useTranslation();
|
|
2698
|
-
const { conversion } = useMarketingConfig();
|
|
2699
|
-
const overview = useMarketingStore((s) => s.overview);
|
|
2700
|
-
useEnsureMarketingData();
|
|
2701
|
-
const mix = overview?.channelMix ?? [];
|
|
2702
|
-
const maxMix = Math.max(...mix.map((m) => m.conversions), 1);
|
|
2703
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { children: [
|
|
2704
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.CardHeader, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.CardTitle, { children: [
|
|
2705
|
-
conversion.labelPlural,
|
|
2706
|
-
" ",
|
|
2707
|
-
t("marketing.overview.byChannelSuffix")
|
|
2708
|
-
] }) }),
|
|
2709
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.CardContent, { className: "space-y-2.5", children: mix.map((m) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2710
|
-
ProportionBar,
|
|
2711
|
-
{
|
|
2712
|
-
value: m.conversions,
|
|
2713
|
-
max: maxMix,
|
|
2714
|
-
color: "#6366f1",
|
|
2715
|
-
label: /* @__PURE__ */ jsxRuntime.jsx(ChannelCell, { channelId: m.channelId }),
|
|
2716
|
-
right: /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatNumber(m.conversions) })
|
|
2717
|
-
},
|
|
2718
|
-
m.channelId
|
|
2719
|
-
)) })
|
|
2720
|
-
] });
|
|
2721
|
-
}
|
|
2722
|
-
function CampaignsTable() {
|
|
2723
|
-
const t = core.useTranslation();
|
|
2724
|
-
const { conversion } = useMarketingConfig();
|
|
2725
|
-
const campaigns = useMarketingStore((s) => s.campaigns);
|
|
2726
|
-
useEnsureMarketingData();
|
|
2727
|
-
const columns = [
|
|
2728
|
-
{ accessorKey: "name", header: t("marketing.col.campaign"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
|
|
2729
|
-
{ accessorKey: "channelId", header: t("marketing.col.channel"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx(ChannelCell, { channelId: getValue() }) },
|
|
2730
|
-
{ accessorKey: "conversions", header: conversion.labelPlural, cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatCompact(getValue()) }) },
|
|
2731
|
-
{ id: "cvr", accessorFn: (c) => c.reach > 0 ? c.conversions / c.reach * 100 : 0, header: t("marketing.col.cvr"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatPercent(getValue()) }) },
|
|
2732
|
-
{ accessorKey: "status", header: t("marketing.col.status"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: getValue() }) }
|
|
2733
|
-
];
|
|
2734
|
-
const recent = campaigns.filter((c) => c.status !== "draft").slice(0, 5);
|
|
2735
|
-
return /* @__PURE__ */ jsxRuntime.jsx(ui.TableWidget, { title: t("marketing.overview.recent"), icon: "Megaphone", columns, data: recent, emptyMessage: t("marketing.campaigns.empty") });
|
|
2736
|
-
}
|
|
2737
|
-
function createMarketingDashboardWidgets(ctx3) {
|
|
2738
|
-
const withCtx = (Inner) => {
|
|
2739
|
-
const Wrapped = () => /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config: ctx3.config, provider: ctx3.provider, store: ctx3.store, children: /* @__PURE__ */ jsxRuntime.jsx(Inner, {}) });
|
|
2740
|
-
Wrapped.displayName = `MarketingWidget(${Inner.displayName ?? Inner.name})`;
|
|
2741
|
-
return Wrapped;
|
|
2742
|
-
};
|
|
2743
|
-
return [
|
|
2744
|
-
// Conversions is marketing's headline KPI on the global home; the rest are
|
|
2745
|
-
// home-hidden by default (shown on the marketing plugin-home, addable via Customize).
|
|
2746
|
-
ui.defineKpiWidget({ id: "marketing.kpi.conversions", title: "marketing.metric.conversions", domain: "marketing", defaultOrder: 0, component: withCtx(ConversionsKpi) }),
|
|
2747
|
-
ui.defineKpiWidget({ id: "marketing.kpi.conversion-rate", title: "marketing.metric.conversionRate", domain: "marketing", defaultOrder: 1, defaultVisible: false, component: withCtx(ConversionRateKpi) }),
|
|
2748
|
-
ui.defineKpiWidget({ id: "marketing.kpi.cpa", title: "marketing.metric.cpa", domain: "marketing", defaultOrder: 2, defaultVisible: false, component: withCtx(CpaKpi) }),
|
|
2749
|
-
ui.defineKpiWidget({ id: "marketing.kpi.top-channel", title: "marketing.metric.topChannel", domain: "marketing", defaultOrder: 3, defaultVisible: false, component: withCtx(TopChannelKpi) }),
|
|
2750
|
-
// Channel-mix + campaigns default to the marketing plugin-home only; the
|
|
2751
|
-
// global home stays a clean cross-domain KPI overview.
|
|
2752
|
-
ui.defineCustomWidget({ id: "marketing.panel.channel-mix", title: "marketing.overview.byChannelSuffix", domain: "marketing", span: 4, defaultOrder: 10, surfaces: ["plugin-home"], component: withCtx(ChannelMixPanel) }),
|
|
2753
|
-
ui.defineTableWidget({ id: "marketing.table.recent-campaigns", title: "marketing.overview.recent", domain: "marketing", span: 4, defaultOrder: 20, surfaces: ["plugin-home"], component: withCtx(CampaignsTable) })
|
|
2754
|
-
];
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
// src/presets.ts
|
|
2758
|
-
var AGENCY = {
|
|
2759
|
-
conversion: {
|
|
2760
|
-
id: "won-deal",
|
|
2761
|
-
label: "Won deal",
|
|
2762
|
-
labelPlural: "Won deals",
|
|
2763
|
-
valueLabel: "Pipeline value",
|
|
2764
|
-
source: "crm",
|
|
2765
|
-
avgValue: 3200
|
|
2766
|
-
},
|
|
2767
|
-
channels: [
|
|
2768
|
-
{ id: "paid-search", label: "Paid Search", icon: "Search", kind: "paid" },
|
|
2769
|
-
{ id: "paid-social", label: "Paid Social", icon: "Megaphone", kind: "paid" },
|
|
2770
|
-
{ id: "organic", label: "Organic / SEO", icon: "Globe", kind: "organic" },
|
|
2771
|
-
{ id: "email", label: "Email", icon: "Mail", kind: "outbound" },
|
|
2772
|
-
{ id: "referral", label: "Referral", icon: "Users", kind: "referral" },
|
|
2773
|
-
{ id: "direct", label: "Direct", icon: "MousePointerClick", kind: "direct" }
|
|
2774
|
-
],
|
|
2775
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
|
|
2776
|
-
};
|
|
2777
|
-
var BEAUTY = {
|
|
2778
|
-
conversion: {
|
|
2779
|
-
id: "booking",
|
|
2780
|
-
label: "Booking",
|
|
2781
|
-
labelPlural: "Bookings",
|
|
2782
|
-
valueLabel: "Booking revenue",
|
|
2783
|
-
source: "agenda",
|
|
2784
|
-
avgValue: 85
|
|
2785
|
-
},
|
|
2786
|
-
channels: [
|
|
2787
|
-
{ id: "instagram", label: "Instagram", icon: "Instagram", kind: "social" },
|
|
2788
|
-
{ id: "google", label: "Google", icon: "Globe", kind: "organic" },
|
|
2789
|
-
{ id: "referral", label: "Referral", icon: "Users", kind: "referral" },
|
|
2790
|
-
{ id: "whatsapp", label: "WhatsApp", icon: "MessageCircle", kind: "social" },
|
|
2791
|
-
{ id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
|
|
2792
|
-
],
|
|
2793
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, blog: false, broadcasts: false, journeys: false }
|
|
2794
|
-
};
|
|
2795
|
-
var RESTO = {
|
|
2796
|
-
conversion: {
|
|
2797
|
-
id: "order",
|
|
2798
|
-
label: "Order",
|
|
2799
|
-
labelPlural: "Orders",
|
|
2800
|
-
valueLabel: "Revenue",
|
|
2801
|
-
source: "orders",
|
|
2802
|
-
avgValue: 42
|
|
2803
|
-
},
|
|
2804
|
-
channels: [
|
|
2805
|
-
{ id: "ifood", label: "iFood", icon: "Utensils", kind: "paid" },
|
|
2806
|
-
{ id: "delivery", label: "Delivery apps", icon: "Bike", kind: "paid" },
|
|
2807
|
-
{ id: "google", label: "Google", icon: "Globe", kind: "organic" },
|
|
2808
|
-
{ id: "instagram", label: "Instagram", icon: "Instagram", kind: "social" },
|
|
2809
|
-
{ id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
|
|
2810
|
-
],
|
|
2811
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
|
|
2812
|
-
};
|
|
2813
|
-
var MARKETING_PRESETS = {
|
|
2814
|
-
agency: AGENCY,
|
|
2815
|
-
beauty: BEAUTY,
|
|
2816
|
-
resto: RESTO
|
|
2817
|
-
};
|
|
2818
|
-
var CAMPAIGN_NAME_SEEDS = {
|
|
2819
|
-
agency: [
|
|
2820
|
-
"Q2 Lead-gen \u2014 Search",
|
|
2821
|
-
"LinkedIn Retargeting",
|
|
2822
|
-
"Webinar Funnel",
|
|
2823
|
-
"Newsletter Nurture",
|
|
2824
|
-
"Partner Referral Push"
|
|
2825
|
-
],
|
|
2826
|
-
beauty: [
|
|
2827
|
-
"Summer Glow Promo",
|
|
2828
|
-
"New Client \u2014 Instagram",
|
|
2829
|
-
"Birthday Offer Blast",
|
|
2830
|
-
"Win-back 90 days",
|
|
2831
|
-
"Google Reviews Boost"
|
|
2832
|
-
],
|
|
2833
|
-
resto: [
|
|
2834
|
-
"iFood Weekend Combo",
|
|
2835
|
-
"Happy Hour Promo",
|
|
2836
|
-
"New Menu Launch",
|
|
2837
|
-
"Loyalty Double Points",
|
|
2838
|
-
"Instagram Stories Drop"
|
|
2839
|
-
]
|
|
2840
|
-
};
|
|
2841
|
-
|
|
2842
|
-
// src/data/mock.ts
|
|
2843
|
-
var RANGE_FACTOR = { "7d": 0.25, "30d": 1, "90d": 3.05 };
|
|
2844
|
-
var BASE_REACH = [4200, 3100, 2600, 1900, 1500, 1200, 1e3];
|
|
2845
|
-
var KIND_LEAD_RATE = {
|
|
2846
|
-
paid: 0.18,
|
|
2847
|
-
social: 0.22,
|
|
2848
|
-
organic: 0.14,
|
|
2849
|
-
referral: 0.3,
|
|
2850
|
-
direct: 0.1,
|
|
2851
|
-
outbound: 0.16
|
|
2852
|
-
};
|
|
2853
|
-
var KIND_CONV_RATE = {
|
|
2854
|
-
paid: 0.28,
|
|
2855
|
-
social: 0.25,
|
|
2856
|
-
organic: 0.32,
|
|
2857
|
-
referral: 0.45,
|
|
2858
|
-
direct: 0.22,
|
|
2859
|
-
outbound: 0.24
|
|
2860
|
-
};
|
|
2861
|
-
var KIND_SPEND30 = {
|
|
2862
|
-
paid: 2800,
|
|
2863
|
-
social: 900,
|
|
2864
|
-
outbound: 600,
|
|
2865
|
-
organic: 0,
|
|
2866
|
-
referral: 0,
|
|
2867
|
-
direct: 0
|
|
2868
|
-
};
|
|
2869
|
-
function channelBase30(channels, conversion) {
|
|
2870
|
-
return channels.map((ch, i) => {
|
|
2871
|
-
const reach = BASE_REACH[i % BASE_REACH.length];
|
|
2872
|
-
const leads = Math.round(reach * KIND_LEAD_RATE[ch.kind]);
|
|
2873
|
-
const conversions = Math.round(leads * KIND_CONV_RATE[ch.kind]);
|
|
2874
|
-
const spend = KIND_SPEND30[ch.kind];
|
|
2875
|
-
const value = Math.round(conversions * conversion.avgValue);
|
|
2876
|
-
return { channelId: ch.id, kind: ch.kind, reach, leads, conversions, spend, value };
|
|
2877
|
-
});
|
|
2878
|
-
}
|
|
2879
|
-
function scale(n, range) {
|
|
2880
|
-
return Math.round(n * RANGE_FACTOR[range]);
|
|
2881
|
-
}
|
|
2882
|
-
function toPerformance(base, range) {
|
|
2883
|
-
const reach = scale(base.reach, range);
|
|
2884
|
-
const leads = scale(base.leads, range);
|
|
2885
|
-
const conversions = scale(base.conversions, range);
|
|
2886
|
-
const spend = scale(base.spend, range);
|
|
2887
|
-
const value = scale(base.value, range);
|
|
2888
|
-
return {
|
|
2889
|
-
channelId: base.channelId,
|
|
2890
|
-
reach,
|
|
2891
|
-
leads,
|
|
2892
|
-
conversions,
|
|
2893
|
-
spend,
|
|
2894
|
-
value,
|
|
2895
|
-
cvr: reach > 0 ? conversions / reach * 100 : 0,
|
|
2896
|
-
cpa: conversions > 0 && spend > 0 ? spend / conversions : 0
|
|
2897
|
-
};
|
|
2898
|
-
}
|
|
2899
|
-
function seedCampaigns(cfg) {
|
|
2900
|
-
const names = CAMPAIGN_NAME_SEEDS[cfg.domain];
|
|
2901
|
-
const base = channelBase30(cfg.channels, cfg.conversion);
|
|
2902
|
-
const statuses = ["active", "active", "paused", "ended", "draft"];
|
|
2903
|
-
const startDays = [12, 26, 40, 8, 3];
|
|
2904
|
-
const baseDate = (/* @__PURE__ */ new Date("2026-06-16T00:00:00Z")).getTime();
|
|
2905
|
-
return names.map((name, i) => {
|
|
2906
|
-
const ch = cfg.channels[i % cfg.channels.length];
|
|
2907
|
-
const b = base[i % base.length];
|
|
2908
|
-
const status = statuses[i % statuses.length];
|
|
2909
|
-
const draft = status === "draft";
|
|
2910
|
-
const start = new Date(baseDate - startDays[i % startDays.length] * 864e5).toISOString();
|
|
2911
|
-
const share = 0.55 + i % 3 * 0.12;
|
|
2912
|
-
return {
|
|
2913
|
-
id: `cmp-${i + 1}`,
|
|
2914
|
-
name,
|
|
2915
|
-
channelId: ch.id,
|
|
2916
|
-
status,
|
|
2917
|
-
start,
|
|
2918
|
-
end: status === "ended" ? new Date(baseDate - 2 * 864e5).toISOString() : void 0,
|
|
2919
|
-
spend: draft ? 0 : Math.round(b.spend * share),
|
|
2920
|
-
reach: draft ? 0 : Math.round(b.reach * share),
|
|
2921
|
-
leads: draft ? 0 : Math.round(b.leads * share),
|
|
2922
|
-
conversions: draft ? 0 : Math.round(b.conversions * share),
|
|
2923
|
-
value: draft ? 0 : Math.round(b.value * share)
|
|
2924
|
-
};
|
|
2925
|
-
});
|
|
2926
|
-
}
|
|
2927
|
-
function scaleCampaign(c, range) {
|
|
2928
|
-
if (c.status === "draft") return c;
|
|
2929
|
-
return {
|
|
2930
|
-
...c,
|
|
2931
|
-
spend: scale(c.spend, range),
|
|
2932
|
-
reach: scale(c.reach, range),
|
|
2933
|
-
leads: scale(c.leads, range),
|
|
2934
|
-
conversions: scale(c.conversions, range),
|
|
2935
|
-
value: scale(c.value, range)
|
|
2936
|
-
};
|
|
2937
|
-
}
|
|
2938
|
-
function seedLandingPages() {
|
|
2939
|
-
return [
|
|
2940
|
-
{ id: "lp-1", name: "Free Consultation Funnel", type: "Funnel", visits: 3820, conversions: 474 },
|
|
2941
|
-
{ id: "lp-2", name: "Summer Promo Landing", type: "Landing page", visits: 1560, conversions: 126 },
|
|
2942
|
-
{ id: "lp-3", name: "Pricing Page", type: "Landing page", visits: 2240, conversions: 198 },
|
|
2943
|
-
{ id: "lp-4", name: "Company Website", type: "Website", visits: 9240, conversions: 296 }
|
|
2944
|
-
];
|
|
2945
|
-
}
|
|
2946
|
-
function createMockMarketingProvider(cfg) {
|
|
2947
|
-
const base = channelBase30(cfg.channels, cfg.conversion);
|
|
2948
|
-
const campaigns = seedCampaigns(cfg);
|
|
2949
|
-
const landing = seedLandingPages();
|
|
2950
|
-
let counter2 = 100;
|
|
2951
|
-
async function channelPerformance(query) {
|
|
2952
|
-
const list = base.map((b) => toPerformance(b, query.range));
|
|
2953
|
-
return query.channelId ? list.filter((c) => c.channelId === query.channelId) : list;
|
|
2954
|
-
}
|
|
2955
|
-
return {
|
|
2956
|
-
channelPerformance,
|
|
2957
|
-
async overview(query) {
|
|
2958
|
-
const perf = await channelPerformance(query);
|
|
2959
|
-
const reach = perf.reduce((s, c) => s + c.reach, 0);
|
|
2960
|
-
const conversions = perf.reduce((s, c) => s + c.conversions, 0);
|
|
2961
|
-
const spend = perf.reduce((s, c) => s + c.spend, 0);
|
|
2962
|
-
const value = perf.reduce((s, c) => s + c.value, 0);
|
|
2963
|
-
const cvr = reach > 0 ? conversions / reach * 100 : 0;
|
|
2964
|
-
const top = [...perf].sort((a, b) => b.conversions - a.conversions)[0];
|
|
2965
|
-
return {
|
|
2966
|
-
conversions,
|
|
2967
|
-
conversionsPrev: Math.round(conversions * 0.86),
|
|
2968
|
-
cvr,
|
|
2969
|
-
cvrPrev: cvr * 0.94,
|
|
2970
|
-
spend,
|
|
2971
|
-
cpa: conversions > 0 && spend > 0 ? spend / conversions : 0,
|
|
2972
|
-
value,
|
|
2973
|
-
topChannelId: top?.channelId ?? null,
|
|
2974
|
-
channelMix: perf.map((c) => ({ channelId: c.channelId, conversions: c.conversions }))
|
|
2975
|
-
};
|
|
2976
|
-
},
|
|
2977
|
-
async funnel(query) {
|
|
2978
|
-
const perf = await channelPerformance(query);
|
|
2979
|
-
const reach = perf.reduce((s, c) => s + c.reach, 0);
|
|
2980
|
-
const leads = perf.reduce((s, c) => s + c.leads, 0);
|
|
2981
|
-
const conversions = perf.reduce((s, c) => s + c.conversions, 0);
|
|
2982
|
-
const qualified = Math.round(leads * 0.62);
|
|
2983
|
-
return [
|
|
2984
|
-
{ id: "reach", label: "Reach / visits", count: reach, color: "#6366f1" },
|
|
2985
|
-
{ id: "leads", label: "Leads", count: leads, color: "#0ea5e9" },
|
|
2986
|
-
{ id: "qualified", label: "Qualified", count: qualified, color: "#14b8a6" },
|
|
2987
|
-
{ id: "converted", label: cfg.conversion.labelPlural, count: conversions, color: "#22c55e" }
|
|
2988
|
-
];
|
|
2989
|
-
},
|
|
2990
|
-
async listCampaigns(query) {
|
|
2991
|
-
const list = query.channelId ? campaigns.filter((c) => c.channelId === query.channelId) : campaigns;
|
|
2992
|
-
return list.map((c) => scaleCampaign(c, query.range));
|
|
2993
|
-
},
|
|
2994
|
-
async getCampaign(id) {
|
|
2995
|
-
return campaigns.find((c) => c.id === id) ?? null;
|
|
2996
|
-
},
|
|
2997
|
-
async saveCampaign(input) {
|
|
2998
|
-
if (input.id) {
|
|
2999
|
-
const existing = campaigns.find((c) => c.id === input.id);
|
|
3000
|
-
if (existing) {
|
|
3001
|
-
Object.assign(existing, {
|
|
3002
|
-
name: input.name,
|
|
3003
|
-
channelId: input.channelId,
|
|
3004
|
-
status: input.status,
|
|
3005
|
-
start: input.start,
|
|
3006
|
-
end: input.end,
|
|
3007
|
-
spend: input.spend
|
|
3008
|
-
});
|
|
3009
|
-
return existing;
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3012
|
-
const b = base.find((x) => x.channelId === input.channelId) ?? base[0];
|
|
3013
|
-
const active = input.status === "active";
|
|
3014
|
-
const created = {
|
|
3015
|
-
id: `cmp-${++counter2}`,
|
|
3016
|
-
name: input.name,
|
|
3017
|
-
channelId: input.channelId,
|
|
3018
|
-
status: input.status,
|
|
3019
|
-
start: input.start,
|
|
3020
|
-
end: input.end,
|
|
3021
|
-
spend: input.spend,
|
|
3022
|
-
// New campaigns ramp from a small slice of their channel's volume.
|
|
3023
|
-
reach: active ? Math.round(b.reach * 0.15) : 0,
|
|
3024
|
-
leads: active ? Math.round(b.leads * 0.15) : 0,
|
|
3025
|
-
conversions: active ? Math.round(b.conversions * 0.15) : 0,
|
|
3026
|
-
value: active ? Math.round(b.value * 0.15) : 0
|
|
3027
|
-
};
|
|
3028
|
-
campaigns.unshift(created);
|
|
3029
|
-
return created;
|
|
3030
|
-
},
|
|
3031
|
-
async deleteCampaign(id) {
|
|
3032
|
-
const idx = campaigns.findIndex((c) => c.id === id);
|
|
3033
|
-
if (idx >= 0) campaigns.splice(idx, 1);
|
|
3034
|
-
},
|
|
3035
|
-
async landingPages(query) {
|
|
3036
|
-
return landing.map((p) => {
|
|
3037
|
-
const visits = scale(p.visits, query.range);
|
|
3038
|
-
const conversions = scale(p.conversions, query.range);
|
|
3039
|
-
return { ...p, visits, conversions, cvr: visits > 0 ? conversions / visits * 100 : 0 };
|
|
3040
|
-
});
|
|
3041
|
-
}
|
|
3042
|
-
};
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
// src/data/contentMock.ts
|
|
3046
|
-
var counter = 0;
|
|
3047
|
-
var nextId = (prefix) => `${prefix}-${++counter}`;
|
|
3048
|
-
var SAMPLE_SCRIPT = `# 5 sinais de que sua queda capilar n\xE3o \xE9 normal
|
|
3049
|
-
|
|
3050
|
-
**TEMA:** 5 SINAIS DE QUE SUA QUEDA CAPILAR N\xC3O \xC9 NORMAL
|
|
3051
|
-
**Formato:** Reel \xB7 **Dura\xE7\xE3o:** 45 a 60 segundos
|
|
3052
|
-
|
|
3053
|
-
---
|
|
3054
|
-
|
|
3055
|
-
## TAKE 1 \u2013 GANCHO (0s a 5s)
|
|
3056
|
-
|
|
3057
|
-
*Dra. andando at\xE9 a c\xE2mera.*
|
|
3058
|
-
|
|
3059
|
-
**Texto na tela:** \u26A0\uFE0F 5 sinais de alerta
|
|
3060
|
-
|
|
3061
|
-
**Fala:** "Seu cabelo est\xE1 caindo e voc\xEA n\xE3o sabe se isso \xE9 normal? Existem sinais que merecem aten\xE7\xE3o."
|
|
3062
|
-
|
|
3063
|
-
---
|
|
3064
|
-
|
|
3065
|
-
## TAKE 2 \u2013 SINAL 1 (5s a 12s)
|
|
3066
|
-
|
|
3067
|
-
*Mostrar a Dra. segurando alguns fios de cabelo ou imagem ilustrativa.*
|
|
3068
|
-
|
|
3069
|
-
**Texto na tela:** Excesso de fios caindo.
|
|
3070
|
-
|
|
3071
|
-
**Fala:** "Primeiro sinal: voc\xEA percebe muitos fios no travesseiro, no banho ou ao pentear os cabelos."
|
|
3072
|
-
|
|
3073
|
-
---
|
|
3074
|
-
|
|
3075
|
-
## TAKE 3 \u2013 CTA (40s a 60s)
|
|
3076
|
-
|
|
3077
|
-
**Fala:** "Se voc\xEA se identificou com algum desses sinais, agende uma avalia\xE7\xE3o capilar."
|
|
3078
|
-
|
|
3079
|
-
**CTA:** Agende sua avalia\xE7\xE3o
|
|
3080
|
-
`;
|
|
3081
|
-
var WEEK_TITLES = [
|
|
3082
|
-
["5 sinais de que sua queda capilar n\xE3o \xE9 normal", "Antes e depois de tratamento capilar"],
|
|
3083
|
-
["O que \xE9 DHT?", "Tour pela cl\xEDnica"],
|
|
3084
|
-
["3 erros que aceleram a calv\xEDcie", "Depoimento de paciente"],
|
|
3085
|
-
["Como funciona uma avalia\xE7\xE3o capilar", "Mitos sobre queda de cabelo"],
|
|
3086
|
-
["Por que seu tratamento anterior n\xE3o funcionou?", "Ozonioterapia para gordura localizada"],
|
|
3087
|
-
["Diferen\xE7a entre dermatologista e tricologista", "Caso real de evolu\xE7\xE3o"],
|
|
3088
|
-
["Alopecia feminina explicada", "Bastidores da cl\xEDnica"],
|
|
3089
|
-
["Quando procurar ajuda para queda capilar", "Convite para avalia\xE7\xE3o gratuita"]
|
|
3090
|
-
];
|
|
3091
|
-
function buildSeed() {
|
|
3092
|
-
const account = {
|
|
3093
|
-
id: nextId("acc"),
|
|
3094
|
-
name: "Conta demo",
|
|
3095
|
-
handle: "@clinica.demo",
|
|
3096
|
-
platforms: ["instagram"],
|
|
3097
|
-
isActive: true
|
|
3098
|
-
};
|
|
3099
|
-
const plan = {
|
|
3100
|
-
id: nextId("plan"),
|
|
3101
|
-
accountId: account.id,
|
|
3102
|
-
name: "Plano de conte\xFAdo \u2014 8 semanas",
|
|
3103
|
-
status: "active",
|
|
3104
|
-
weeksCount: 8,
|
|
3105
|
-
objective: "Atrair pacientes qualificados para avalia\xE7\xE3o capilar",
|
|
3106
|
-
tone: "Autoridade acess\xEDvel \u2014 t\xE9cnica, mas em linguagem simples",
|
|
3107
|
-
pillars: ["Educa\xE7\xE3o", "Autoridade", "Prova social", "Bastidores", "Convers\xE3o"],
|
|
3108
|
-
formats: ["reel", "static"],
|
|
3109
|
-
weeklyFrequency: 3,
|
|
3110
|
-
briefMd: "## Objetivo\n\nPosicionar a especialista como refer\xEAncia em sa\xFAde capilar e gerar agendamentos de avalia\xE7\xE3o.\n\n## Estrat\xE9gia\n\n- 2 reels educativos/prova social por semana\n- 1 post est\xE1tico por semana\n- CTA recorrente: agendar avalia\xE7\xE3o"
|
|
3111
|
-
};
|
|
3112
|
-
const posts = [];
|
|
3113
|
-
WEEK_TITLES.forEach(([reel1, reel2], i) => {
|
|
3114
|
-
const week = i + 1;
|
|
3115
|
-
posts.push(
|
|
3116
|
-
{
|
|
3117
|
-
id: nextId("post"),
|
|
3118
|
-
planId: plan.id,
|
|
3119
|
-
weekNumber: week,
|
|
3120
|
-
position: 0,
|
|
3121
|
-
title: reel1,
|
|
3122
|
-
format: "reel",
|
|
3123
|
-
status: week === 1 ? "script" : "idea",
|
|
3124
|
-
platforms: [],
|
|
3125
|
-
checklist: [],
|
|
3126
|
-
contentMd: week === 1 ? SAMPLE_SCRIPT : ""
|
|
3127
|
-
},
|
|
3128
|
-
{
|
|
3129
|
-
id: nextId("post"),
|
|
3130
|
-
planId: plan.id,
|
|
3131
|
-
weekNumber: week,
|
|
3132
|
-
position: 1,
|
|
3133
|
-
title: reel2,
|
|
3134
|
-
format: "reel",
|
|
3135
|
-
status: "idea",
|
|
3136
|
-
platforms: [],
|
|
3137
|
-
checklist: [],
|
|
3138
|
-
contentMd: ""
|
|
3139
|
-
},
|
|
3140
|
-
{
|
|
3141
|
-
id: nextId("post"),
|
|
3142
|
-
planId: plan.id,
|
|
3143
|
-
weekNumber: week,
|
|
3144
|
-
position: 2,
|
|
3145
|
-
title: "Est\xE1tico",
|
|
3146
|
-
format: "static",
|
|
3147
|
-
status: "idea",
|
|
3148
|
-
platforms: [],
|
|
3149
|
-
checklist: [],
|
|
3150
|
-
contentMd: ""
|
|
3151
|
-
}
|
|
3152
|
-
);
|
|
3153
|
-
});
|
|
3154
|
-
return { accounts: [account], plans: [plan], posts };
|
|
3155
|
-
}
|
|
3156
|
-
function createMockContentPlannerProvider() {
|
|
3157
|
-
const db = buildSeed();
|
|
3158
|
-
return {
|
|
3159
|
-
async listAccounts() {
|
|
3160
|
-
return [...db.accounts];
|
|
3161
|
-
},
|
|
3162
|
-
async saveAccount(input) {
|
|
3163
|
-
if (input.id) {
|
|
3164
|
-
const acc2 = db.accounts.find((a) => a.id === input.id);
|
|
3165
|
-
if (!acc2) throw new Error("Account not found");
|
|
3166
|
-
Object.assign(acc2, {
|
|
3167
|
-
name: input.name ?? acc2.name,
|
|
3168
|
-
handle: input.handle ?? acc2.handle,
|
|
3169
|
-
platforms: input.platforms ?? acc2.platforms,
|
|
3170
|
-
isActive: input.isActive ?? acc2.isActive
|
|
3171
|
-
});
|
|
3172
|
-
return { ...acc2 };
|
|
3173
|
-
}
|
|
3174
|
-
const acc = {
|
|
3175
|
-
id: nextId("acc"),
|
|
3176
|
-
name: input.name,
|
|
3177
|
-
handle: input.handle,
|
|
3178
|
-
platforms: input.platforms ?? ["instagram"],
|
|
3179
|
-
isActive: input.isActive ?? true
|
|
3180
|
-
};
|
|
3181
|
-
db.accounts.push(acc);
|
|
3182
|
-
return { ...acc };
|
|
3183
|
-
},
|
|
3184
|
-
async deleteAccount(id) {
|
|
3185
|
-
db.accounts = db.accounts.filter((a) => a.id !== id);
|
|
3186
|
-
const planIds = new Set(db.plans.filter((p) => p.accountId === id).map((p) => p.id));
|
|
3187
|
-
db.plans = db.plans.filter((p) => p.accountId !== id);
|
|
3188
|
-
db.posts = db.posts.filter((p) => !planIds.has(p.planId));
|
|
3189
|
-
},
|
|
3190
|
-
async listPlans(accountId) {
|
|
3191
|
-
return db.plans.filter((p) => p.accountId === accountId).map((p) => ({ ...p }));
|
|
3192
|
-
},
|
|
3193
|
-
async getPlan(id) {
|
|
3194
|
-
const plan = db.plans.find((p) => p.id === id);
|
|
3195
|
-
return plan ? { ...plan } : null;
|
|
3196
|
-
},
|
|
3197
|
-
async savePlan(input) {
|
|
3198
|
-
if (input.id) {
|
|
3199
|
-
const plan2 = db.plans.find((p) => p.id === input.id);
|
|
3200
|
-
if (!plan2) throw new Error("Plan not found");
|
|
3201
|
-
Object.assign(plan2, {
|
|
3202
|
-
...input.name !== void 0 && { name: input.name },
|
|
3203
|
-
...input.status !== void 0 && { status: input.status },
|
|
3204
|
-
...input.weeksCount !== void 0 && { weeksCount: input.weeksCount },
|
|
3205
|
-
...input.startDate !== void 0 && { startDate: input.startDate ?? void 0 },
|
|
3206
|
-
...input.objective !== void 0 && { objective: input.objective },
|
|
3207
|
-
...input.tone !== void 0 && { tone: input.tone },
|
|
3208
|
-
...input.pillars !== void 0 && { pillars: input.pillars },
|
|
3209
|
-
...input.formats !== void 0 && { formats: input.formats },
|
|
3210
|
-
...input.weeklyFrequency !== void 0 && { weeklyFrequency: input.weeklyFrequency },
|
|
3211
|
-
...input.briefMd !== void 0 && { briefMd: input.briefMd }
|
|
3212
|
-
});
|
|
3213
|
-
return { ...plan2 };
|
|
3214
|
-
}
|
|
3215
|
-
if (!input.accountId) throw new Error("accountId is required");
|
|
3216
|
-
const plan = {
|
|
3217
|
-
id: nextId("plan"),
|
|
3218
|
-
accountId: input.accountId,
|
|
3219
|
-
name: input.name ?? "Novo plano",
|
|
3220
|
-
status: input.status ?? "draft",
|
|
3221
|
-
weeksCount: input.weeksCount ?? 4,
|
|
3222
|
-
startDate: input.startDate ?? void 0,
|
|
3223
|
-
objective: input.objective,
|
|
3224
|
-
tone: input.tone,
|
|
3225
|
-
pillars: input.pillars ?? [],
|
|
3226
|
-
formats: input.formats ?? ["reel", "static"],
|
|
3227
|
-
weeklyFrequency: input.weeklyFrequency ?? 3,
|
|
3228
|
-
briefMd: input.briefMd ?? ""
|
|
3229
|
-
};
|
|
3230
|
-
db.plans.push(plan);
|
|
3231
|
-
return { ...plan };
|
|
3232
|
-
},
|
|
3233
|
-
async deletePlan(id) {
|
|
3234
|
-
db.plans = db.plans.filter((p) => p.id !== id);
|
|
3235
|
-
db.posts = db.posts.filter((p) => p.planId !== id);
|
|
3236
|
-
},
|
|
3237
|
-
async listPosts(planId) {
|
|
3238
|
-
return db.posts.filter((p) => p.planId === planId).sort((a, b) => a.weekNumber - b.weekNumber || a.position - b.position).map((p) => ({ ...p }));
|
|
3239
|
-
},
|
|
3240
|
-
async getPost(id) {
|
|
3241
|
-
const post = db.posts.find((p) => p.id === id);
|
|
3242
|
-
return post ? { ...post } : null;
|
|
3243
|
-
},
|
|
3244
|
-
async savePost(input) {
|
|
3245
|
-
if (input.id) {
|
|
3246
|
-
const post2 = db.posts.find((p) => p.id === input.id);
|
|
3247
|
-
if (!post2) throw new Error("Post not found");
|
|
3248
|
-
Object.assign(post2, {
|
|
3249
|
-
...input.weekNumber !== void 0 && { weekNumber: input.weekNumber },
|
|
3250
|
-
...input.position !== void 0 && { position: input.position },
|
|
3251
|
-
...input.title !== void 0 && { title: input.title },
|
|
3252
|
-
...input.format !== void 0 && { format: input.format },
|
|
3253
|
-
...input.status !== void 0 && { status: input.status },
|
|
3254
|
-
...input.scheduledDate !== void 0 && { scheduledDate: input.scheduledDate ?? void 0 },
|
|
3255
|
-
...input.platforms !== void 0 && { platforms: input.platforms },
|
|
3256
|
-
...input.checklist !== void 0 && { checklist: input.checklist },
|
|
3257
|
-
...input.mediaUrl !== void 0 && { mediaUrl: input.mediaUrl ?? void 0 },
|
|
3258
|
-
...input.hook !== void 0 && { hook: input.hook },
|
|
3259
|
-
...input.cta !== void 0 && { cta: input.cta },
|
|
3260
|
-
...input.contentMd !== void 0 && { contentMd: input.contentMd }
|
|
3261
|
-
});
|
|
3262
|
-
return { ...post2 };
|
|
3263
|
-
}
|
|
3264
|
-
if (!input.planId) throw new Error("planId is required");
|
|
3265
|
-
const week = input.weekNumber ?? 1;
|
|
3266
|
-
const siblings = db.posts.filter((p) => p.planId === input.planId && p.weekNumber === week);
|
|
3267
|
-
const post = {
|
|
3268
|
-
id: nextId("post"),
|
|
3269
|
-
planId: input.planId,
|
|
3270
|
-
weekNumber: week,
|
|
3271
|
-
position: input.position ?? siblings.length,
|
|
3272
|
-
title: input.title ?? "",
|
|
3273
|
-
format: input.format ?? "reel",
|
|
3274
|
-
status: input.status ?? "idea",
|
|
3275
|
-
scheduledDate: input.scheduledDate ?? void 0,
|
|
3276
|
-
platforms: input.platforms ?? [],
|
|
3277
|
-
checklist: input.checklist ?? [],
|
|
3278
|
-
mediaUrl: input.mediaUrl ?? void 0,
|
|
3279
|
-
hook: input.hook,
|
|
3280
|
-
cta: input.cta,
|
|
3281
|
-
contentMd: input.contentMd ?? ""
|
|
3282
|
-
};
|
|
3283
|
-
db.posts.push(post);
|
|
3284
|
-
return { ...post };
|
|
3285
|
-
},
|
|
3286
|
-
async deletePost(id) {
|
|
3287
|
-
db.posts = db.posts.filter((p) => p.id !== id);
|
|
3288
|
-
}
|
|
3289
|
-
};
|
|
3290
|
-
}
|
|
3291
|
-
|
|
3292
|
-
// src/data/tables.ts
|
|
3293
|
-
var T = {
|
|
3294
|
-
socialAccounts: "plg_marketing_social_accounts",
|
|
3295
|
-
contentPlans: "plg_marketing_content_plans",
|
|
3296
|
-
contentPosts: "plg_marketing_content_posts"
|
|
3297
|
-
};
|
|
3298
|
-
|
|
3299
|
-
// src/data/contentSupabase.ts
|
|
3300
|
-
function getClient() {
|
|
3301
|
-
const supabase = core.getSupabaseClientOptional();
|
|
3302
|
-
if (!supabase) throw new Error("Supabase not initialized");
|
|
3303
|
-
return supabase;
|
|
3304
|
-
}
|
|
3305
|
-
function requireTenantId() {
|
|
3306
|
-
const tenantId = core.getActiveTenantId();
|
|
3307
|
-
if (!tenantId) throw new Error("No active tenant");
|
|
3308
|
-
return tenantId;
|
|
3309
|
-
}
|
|
3310
|
-
function snakeToCamel(obj) {
|
|
3311
|
-
const result = {};
|
|
3312
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
3313
|
-
result[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = value ?? void 0;
|
|
3314
|
-
}
|
|
3315
|
-
return result;
|
|
3316
|
-
}
|
|
3317
|
-
function toAccount(row) {
|
|
3318
|
-
const account = snakeToCamel(row);
|
|
3319
|
-
if (!Array.isArray(account.platforms)) account.platforms = [];
|
|
3320
|
-
return account;
|
|
3321
|
-
}
|
|
3322
|
-
function toPlan(row) {
|
|
3323
|
-
const plan = snakeToCamel(row);
|
|
3324
|
-
if (!Array.isArray(plan.pillars)) plan.pillars = [];
|
|
3325
|
-
if (!Array.isArray(plan.formats)) plan.formats = [];
|
|
3326
|
-
plan.briefMd = plan.briefMd ?? "";
|
|
3327
|
-
return plan;
|
|
3328
|
-
}
|
|
3329
|
-
function toPost(row) {
|
|
3330
|
-
const post = snakeToCamel(row);
|
|
3331
|
-
post.contentMd = post.contentMd ?? "";
|
|
3332
|
-
if (!Array.isArray(post.platforms)) post.platforms = [];
|
|
3333
|
-
if (!Array.isArray(post.checklist)) post.checklist = [];
|
|
3334
|
-
return post;
|
|
3335
|
-
}
|
|
3336
|
-
function createSupabaseContentPlannerProvider() {
|
|
3337
|
-
return {
|
|
3338
|
-
async listAccounts() {
|
|
3339
|
-
const tenantId = core.getActiveTenantId();
|
|
3340
|
-
if (!tenantId) return [];
|
|
3341
|
-
const { data } = await getClient().from(T.socialAccounts).select("*").eq("tenant_id", tenantId).eq("is_active", true).order("created_at", { ascending: true });
|
|
3342
|
-
return (data ?? []).map(toAccount);
|
|
3343
|
-
},
|
|
3344
|
-
async saveAccount(input) {
|
|
3345
|
-
if (input.id) {
|
|
3346
|
-
const payload = {};
|
|
3347
|
-
if (input.name !== void 0) payload.name = input.name;
|
|
3348
|
-
if (input.handle !== void 0) payload.handle = input.handle;
|
|
3349
|
-
if (input.platforms !== void 0) payload.platforms = input.platforms;
|
|
3350
|
-
if (input.isActive !== void 0) payload.is_active = input.isActive;
|
|
3351
|
-
const { data: data2, error: error2 } = await getClient().from(T.socialAccounts).update(payload).eq("id", input.id).select("*").single();
|
|
3352
|
-
if (error2) throw error2;
|
|
3353
|
-
return toAccount(data2);
|
|
3354
|
-
}
|
|
3355
|
-
const { data, error } = await getClient().from(T.socialAccounts).insert({
|
|
3356
|
-
tenant_id: requireTenantId(),
|
|
3357
|
-
name: input.name,
|
|
3358
|
-
handle: input.handle ?? null,
|
|
3359
|
-
platforms: input.platforms ?? ["instagram"],
|
|
3360
|
-
is_active: input.isActive ?? true
|
|
3361
|
-
}).select("*").single();
|
|
3362
|
-
if (error) throw error;
|
|
3363
|
-
return toAccount(data);
|
|
3364
|
-
},
|
|
3365
|
-
async deleteAccount(id) {
|
|
3366
|
-
const { error } = await getClient().from(T.socialAccounts).delete().eq("id", id);
|
|
3367
|
-
if (error) throw error;
|
|
3368
|
-
},
|
|
3369
|
-
async listPlans(accountId) {
|
|
3370
|
-
const tenantId = core.getActiveTenantId();
|
|
3371
|
-
if (!tenantId) return [];
|
|
3372
|
-
const { data } = await getClient().from(T.contentPlans).select("*").eq("tenant_id", tenantId).eq("account_id", accountId).order("created_at", { ascending: false });
|
|
3373
|
-
return (data ?? []).map(toPlan);
|
|
3374
|
-
},
|
|
3375
|
-
async getPlan(id) {
|
|
3376
|
-
const { data } = await getClient().from(T.contentPlans).select("*").eq("id", id).maybeSingle();
|
|
3377
|
-
return data ? toPlan(data) : null;
|
|
3378
|
-
},
|
|
3379
|
-
async savePlan(input) {
|
|
3380
|
-
if (input.id) {
|
|
3381
|
-
const payload = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3382
|
-
if (input.name !== void 0) payload.name = input.name;
|
|
3383
|
-
if (input.status !== void 0) payload.status = input.status;
|
|
3384
|
-
if (input.weeksCount !== void 0) payload.weeks_count = input.weeksCount;
|
|
3385
|
-
if (input.startDate !== void 0) payload.start_date = input.startDate;
|
|
3386
|
-
if (input.objective !== void 0) payload.objective = input.objective;
|
|
3387
|
-
if (input.tone !== void 0) payload.tone = input.tone;
|
|
3388
|
-
if (input.pillars !== void 0) payload.pillars = input.pillars;
|
|
3389
|
-
if (input.formats !== void 0) payload.formats = input.formats;
|
|
3390
|
-
if (input.weeklyFrequency !== void 0) payload.weekly_frequency = input.weeklyFrequency;
|
|
3391
|
-
if (input.briefMd !== void 0) payload.brief_md = input.briefMd;
|
|
3392
|
-
const { data: data2, error: error2 } = await getClient().from(T.contentPlans).update(payload).eq("id", input.id).select("*").single();
|
|
3393
|
-
if (error2) throw error2;
|
|
3394
|
-
return toPlan(data2);
|
|
3395
|
-
}
|
|
3396
|
-
if (!input.accountId) throw new Error("accountId is required");
|
|
3397
|
-
const { data, error } = await getClient().from(T.contentPlans).insert({
|
|
3398
|
-
tenant_id: requireTenantId(),
|
|
3399
|
-
account_id: input.accountId,
|
|
3400
|
-
name: input.name ?? "Novo plano",
|
|
3401
|
-
status: input.status ?? "draft",
|
|
3402
|
-
weeks_count: input.weeksCount ?? 4,
|
|
3403
|
-
start_date: input.startDate ?? null,
|
|
3404
|
-
objective: input.objective ?? null,
|
|
3405
|
-
tone: input.tone ?? null,
|
|
3406
|
-
pillars: input.pillars ?? [],
|
|
3407
|
-
formats: input.formats ?? ["reel", "static"],
|
|
3408
|
-
weekly_frequency: input.weeklyFrequency ?? 3,
|
|
3409
|
-
brief_md: input.briefMd ?? ""
|
|
3410
|
-
}).select("*").single();
|
|
3411
|
-
if (error) throw error;
|
|
3412
|
-
return toPlan(data);
|
|
3413
|
-
},
|
|
3414
|
-
async deletePlan(id) {
|
|
3415
|
-
await getClient().from(T.contentPlans).delete().eq("id", id);
|
|
3416
|
-
},
|
|
3417
|
-
async listPosts(planId) {
|
|
3418
|
-
const { data } = await getClient().from(T.contentPosts).select("*").eq("plan_id", planId).order("week_number", { ascending: true }).order("position", { ascending: true });
|
|
3419
|
-
return (data ?? []).map(toPost);
|
|
3420
|
-
},
|
|
3421
|
-
async getPost(id) {
|
|
3422
|
-
const { data } = await getClient().from(T.contentPosts).select("*").eq("id", id).maybeSingle();
|
|
3423
|
-
return data ? toPost(data) : null;
|
|
3424
|
-
},
|
|
3425
|
-
async savePost(input) {
|
|
3426
|
-
if (input.id) {
|
|
3427
|
-
const payload = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3428
|
-
if (input.weekNumber !== void 0) payload.week_number = input.weekNumber;
|
|
3429
|
-
if (input.position !== void 0) payload.position = input.position;
|
|
3430
|
-
if (input.title !== void 0) payload.title = input.title;
|
|
3431
|
-
if (input.format !== void 0) payload.format = input.format;
|
|
3432
|
-
if (input.status !== void 0) payload.status = input.status;
|
|
3433
|
-
if (input.scheduledDate !== void 0) payload.scheduled_date = input.scheduledDate;
|
|
3434
|
-
if (input.platforms !== void 0) payload.platforms = input.platforms;
|
|
3435
|
-
if (input.checklist !== void 0) payload.checklist = input.checklist;
|
|
3436
|
-
if (input.mediaUrl !== void 0) payload.media_url = input.mediaUrl;
|
|
3437
|
-
if (input.hook !== void 0) payload.hook = input.hook;
|
|
3438
|
-
if (input.cta !== void 0) payload.cta = input.cta;
|
|
3439
|
-
if (input.contentMd !== void 0) payload.content_md = input.contentMd;
|
|
3440
|
-
const { data: data2, error: error2 } = await getClient().from(T.contentPosts).update(payload).eq("id", input.id).select("*").single();
|
|
3441
|
-
if (error2) throw error2;
|
|
3442
|
-
return toPost(data2);
|
|
3443
|
-
}
|
|
3444
|
-
if (!input.planId) throw new Error("planId is required");
|
|
3445
|
-
const week = input.weekNumber ?? 1;
|
|
3446
|
-
const { data: maxData } = await getClient().from(T.contentPosts).select("position").eq("plan_id", input.planId).eq("week_number", week).order("position", { ascending: false }).limit(1);
|
|
3447
|
-
const maxPos = maxData?.[0]?.position ?? -1;
|
|
3448
|
-
const { data, error } = await getClient().from(T.contentPosts).insert({
|
|
3449
|
-
tenant_id: requireTenantId(),
|
|
3450
|
-
plan_id: input.planId,
|
|
3451
|
-
week_number: week,
|
|
3452
|
-
position: input.position ?? maxPos + 1,
|
|
3453
|
-
title: input.title ?? "",
|
|
3454
|
-
format: input.format ?? "reel",
|
|
3455
|
-
status: input.status ?? "idea",
|
|
3456
|
-
scheduled_date: input.scheduledDate ?? null,
|
|
3457
|
-
platforms: input.platforms ?? [],
|
|
3458
|
-
checklist: input.checklist ?? [],
|
|
3459
|
-
media_url: input.mediaUrl ?? null,
|
|
3460
|
-
hook: input.hook ?? null,
|
|
3461
|
-
cta: input.cta ?? null,
|
|
3462
|
-
content_md: input.contentMd ?? ""
|
|
3463
|
-
}).select("*").single();
|
|
3464
|
-
if (error) throw error;
|
|
3465
|
-
return toPost(data);
|
|
3466
|
-
},
|
|
3467
|
-
async deletePost(id) {
|
|
3468
|
-
await getClient().from(T.contentPosts).delete().eq("id", id);
|
|
3469
|
-
}
|
|
3470
|
-
};
|
|
3471
|
-
}
|
|
3472
|
-
function createMarketingStore(provider2) {
|
|
3473
|
-
return vanilla.createStore((set, get) => ({
|
|
3474
|
-
range: "30d",
|
|
3475
|
-
overview: null,
|
|
3476
|
-
channels: [],
|
|
3477
|
-
campaigns: [],
|
|
3478
|
-
funnel: [],
|
|
3479
|
-
landingPages: [],
|
|
3480
|
-
loading: false,
|
|
3481
|
-
async load() {
|
|
3482
|
-
const range = get().range;
|
|
3483
|
-
set({ loading: true });
|
|
3484
|
-
const [overview, channels, campaigns, funnel, landingPages] = await Promise.all([
|
|
3485
|
-
provider2.overview({ range }),
|
|
3486
|
-
provider2.channelPerformance({ range }),
|
|
3487
|
-
provider2.listCampaigns({ range }),
|
|
3488
|
-
provider2.funnel({ range }),
|
|
3489
|
-
provider2.landingPages({ range })
|
|
3490
|
-
]);
|
|
3491
|
-
set({ overview, channels, campaigns, funnel, landingPages, loading: false });
|
|
3492
|
-
},
|
|
3493
|
-
async setRange(range) {
|
|
3494
|
-
set({ range });
|
|
3495
|
-
await get().load();
|
|
3496
|
-
},
|
|
3497
|
-
async saveCampaign(input) {
|
|
3498
|
-
const created = await provider2.saveCampaign(input);
|
|
3499
|
-
await get().load();
|
|
3500
|
-
return created;
|
|
3501
|
-
},
|
|
3502
|
-
async deleteCampaign(id) {
|
|
3503
|
-
await provider2.deleteCampaign(id);
|
|
3504
|
-
await get().load();
|
|
3505
|
-
}
|
|
3506
|
-
}));
|
|
3507
|
-
}
|
|
3508
|
-
function createContentPlannerStore(provider2) {
|
|
3509
|
-
async function loadPlansFor(accountId) {
|
|
3510
|
-
const plans = await provider2.listPlans(accountId);
|
|
3511
|
-
const activePlan = plans.find((p) => p.status === "active") ?? plans[0] ?? null;
|
|
3512
|
-
const posts = activePlan ? await provider2.listPosts(activePlan.id) : [];
|
|
3513
|
-
return { plans, activePlan, posts };
|
|
3514
|
-
}
|
|
3515
|
-
return vanilla.createStore((set, get) => ({
|
|
3516
|
-
loaded: false,
|
|
3517
|
-
loading: false,
|
|
3518
|
-
loadedTenantId: null,
|
|
3519
|
-
viewMode: "board",
|
|
3520
|
-
setViewMode(mode) {
|
|
3521
|
-
set({ viewMode: mode });
|
|
3522
|
-
},
|
|
3523
|
-
accounts: [],
|
|
3524
|
-
activeAccountId: null,
|
|
3525
|
-
plans: [],
|
|
3526
|
-
activePlan: null,
|
|
3527
|
-
posts: [],
|
|
3528
|
-
async load() {
|
|
3529
|
-
if (get().loading) return;
|
|
3530
|
-
set({ loading: true });
|
|
3531
|
-
try {
|
|
3532
|
-
const accounts = await provider2.listAccounts();
|
|
3533
|
-
const current = get().activeAccountId;
|
|
3534
|
-
const activeAccountId = (current && accounts.some((a) => a.id === current) ? current : accounts[0]?.id) ?? null;
|
|
3535
|
-
const { plans, activePlan, posts } = activeAccountId ? await loadPlansFor(activeAccountId) : { plans: [], activePlan: null, posts: [] };
|
|
3536
|
-
set({
|
|
3537
|
-
accounts,
|
|
3538
|
-
activeAccountId,
|
|
3539
|
-
plans,
|
|
3540
|
-
activePlan,
|
|
3541
|
-
posts,
|
|
3542
|
-
loaded: true,
|
|
3543
|
-
loading: false,
|
|
3544
|
-
loadedTenantId: core.getActiveTenantId() ?? null
|
|
3545
|
-
});
|
|
3546
|
-
} catch (err) {
|
|
3547
|
-
set({ loading: false, loaded: true });
|
|
3548
|
-
throw err;
|
|
3549
|
-
}
|
|
3550
|
-
},
|
|
3551
|
-
async setAccount(accountId) {
|
|
3552
|
-
set({ activeAccountId: accountId, loading: true });
|
|
3553
|
-
try {
|
|
3554
|
-
const { plans, activePlan, posts } = await loadPlansFor(accountId);
|
|
3555
|
-
set({ plans, activePlan, posts, loading: false });
|
|
3556
|
-
} catch (err) {
|
|
3557
|
-
set({ loading: false });
|
|
3558
|
-
throw err;
|
|
3559
|
-
}
|
|
3560
|
-
},
|
|
3561
|
-
async selectPlan(planId) {
|
|
3562
|
-
const plan = get().plans.find((p) => p.id === planId) ?? null;
|
|
3563
|
-
const posts = plan ? await provider2.listPosts(plan.id) : [];
|
|
3564
|
-
set({ activePlan: plan, posts });
|
|
3565
|
-
},
|
|
3566
|
-
async saveAccount(input) {
|
|
3567
|
-
const account = await provider2.saveAccount(input);
|
|
3568
|
-
const accounts = await provider2.listAccounts();
|
|
3569
|
-
set({ accounts });
|
|
3570
|
-
if (!get().activeAccountId) await get().setAccount(account.id);
|
|
3571
|
-
return account;
|
|
3572
|
-
},
|
|
3573
|
-
async deleteAccount(id) {
|
|
3574
|
-
await provider2.deleteAccount(id);
|
|
3575
|
-
const accounts = await provider2.listAccounts();
|
|
3576
|
-
const wasActive = get().activeAccountId === id;
|
|
3577
|
-
set({ accounts });
|
|
3578
|
-
if (wasActive) {
|
|
3579
|
-
const next = accounts[0]?.id ?? null;
|
|
3580
|
-
if (next) {
|
|
3581
|
-
await get().setAccount(next);
|
|
3582
|
-
} else {
|
|
3583
|
-
set({ activeAccountId: null, plans: [], activePlan: null, posts: [] });
|
|
3584
|
-
}
|
|
3585
|
-
}
|
|
3586
|
-
},
|
|
3587
|
-
async savePlan(input) {
|
|
3588
|
-
const plan = await provider2.savePlan(input);
|
|
3589
|
-
const accountId = get().activeAccountId;
|
|
3590
|
-
if (accountId) {
|
|
3591
|
-
const plans = await provider2.listPlans(accountId);
|
|
3592
|
-
const activePlan = plans.find((p) => p.id === plan.id) ?? get().activePlan;
|
|
3593
|
-
const posts = activePlan ? await provider2.listPosts(activePlan.id) : [];
|
|
3594
|
-
set({ plans, activePlan, posts });
|
|
3595
|
-
}
|
|
3596
|
-
return plan;
|
|
3597
|
-
},
|
|
3598
|
-
async savePost(input) {
|
|
3599
|
-
const post = await provider2.savePost(input);
|
|
3600
|
-
const planId = get().activePlan?.id;
|
|
3601
|
-
if (planId && post.planId === planId) {
|
|
3602
|
-
const posts = await provider2.listPosts(planId);
|
|
3603
|
-
set({ posts });
|
|
3604
|
-
}
|
|
3605
|
-
return post;
|
|
3606
|
-
},
|
|
3607
|
-
async deletePost(id) {
|
|
3608
|
-
await provider2.deletePost(id);
|
|
3609
|
-
const planId = get().activePlan?.id;
|
|
3610
|
-
if (planId) {
|
|
3611
|
-
const posts = await provider2.listPosts(planId);
|
|
3612
|
-
set({ posts });
|
|
3613
|
-
}
|
|
3614
|
-
}
|
|
3615
|
-
}));
|
|
3616
|
-
}
|
|
3617
|
-
|
|
3618
|
-
// src/migrations/index.ts
|
|
3619
|
-
var MIGRATION_000_PLG_RENAME = `-- 000_plg_rename.sql \u2014 rename legacy marketing tables to plg_marketing_* for pools
|
|
3620
|
-
-- provisioned before the industry-pool rename. Guarded: fires only when legacy
|
|
3621
|
-
-- name exists and target does not, so fresh pools skip every branch.
|
|
3622
|
-
DO $$
|
|
3623
|
-
BEGIN
|
|
3624
|
-
IF to_regclass('public.mkt_social_accounts') IS NOT NULL AND to_regclass('public.plg_marketing_social_accounts') IS NULL THEN
|
|
3625
|
-
ALTER TABLE public.mkt_social_accounts RENAME TO plg_marketing_social_accounts;
|
|
3626
|
-
END IF;
|
|
3627
|
-
IF to_regclass('public.mkt_content_plans') IS NOT NULL AND to_regclass('public.plg_marketing_content_plans') IS NULL THEN
|
|
3628
|
-
ALTER TABLE public.mkt_content_plans RENAME TO plg_marketing_content_plans;
|
|
3629
|
-
END IF;
|
|
3630
|
-
IF to_regclass('public.mkt_content_posts') IS NOT NULL AND to_regclass('public.plg_marketing_content_posts') IS NULL THEN
|
|
3631
|
-
ALTER TABLE public.mkt_content_posts RENAME TO plg_marketing_content_posts;
|
|
3632
|
-
END IF;
|
|
3633
|
-
END $$;
|
|
3634
|
-
`;
|
|
3635
|
-
var MIGRATION_001_CONTENT_PLANNER = `-- Marketing Plugin: Content Planner
|
|
3636
|
-
-- Prefix: plg_marketing_
|
|
3637
|
-
-- Social accounts + content plans + posts (Notion-style markdown pages).
|
|
3638
|
-
|
|
3639
|
-
CREATE TABLE IF NOT EXISTS public.plg_marketing_social_accounts (
|
|
3640
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3641
|
-
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3642
|
-
name text NOT NULL,
|
|
3643
|
-
handle text,
|
|
3644
|
-
platform text NOT NULL DEFAULT 'instagram'
|
|
3645
|
-
CHECK (platform IN ('instagram', 'tiktok', 'youtube', 'facebook', 'linkedin')),
|
|
3646
|
-
is_active boolean NOT NULL DEFAULT true,
|
|
3647
|
-
created_at timestamptz NOT NULL DEFAULT now()
|
|
3648
|
-
);
|
|
3649
|
-
ALTER TABLE public.plg_marketing_social_accounts ENABLE ROW LEVEL SECURITY;
|
|
3650
|
-
CREATE INDEX IF NOT EXISTS idx_plg_marketing_social_accounts_tenant ON public.plg_marketing_social_accounts(tenant_id);
|
|
3651
|
-
|
|
3652
|
-
CREATE TABLE IF NOT EXISTS public.plg_marketing_content_plans (
|
|
3653
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3654
|
-
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3655
|
-
account_id uuid NOT NULL REFERENCES public.plg_marketing_social_accounts(id) ON DELETE CASCADE,
|
|
3656
|
-
name text NOT NULL,
|
|
3657
|
-
status text NOT NULL DEFAULT 'active'
|
|
3658
|
-
CHECK (status IN ('draft', 'active', 'archived')),
|
|
3659
|
-
weeks_count integer NOT NULL DEFAULT 8,
|
|
3660
|
-
start_date date,
|
|
3661
|
-
objective text,
|
|
3662
|
-
tone text,
|
|
3663
|
-
pillars jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
3664
|
-
formats jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
3665
|
-
weekly_frequency integer NOT NULL DEFAULT 3,
|
|
3666
|
-
brief_md text NOT NULL DEFAULT '',
|
|
3667
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3668
|
-
updated_at timestamptz NOT NULL DEFAULT now()
|
|
3669
|
-
);
|
|
3670
|
-
ALTER TABLE public.plg_marketing_content_plans ENABLE ROW LEVEL SECURITY;
|
|
3671
|
-
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_tenant ON public.plg_marketing_content_plans(tenant_id);
|
|
3672
|
-
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_account ON public.plg_marketing_content_plans(account_id);
|
|
3673
|
-
|
|
3674
|
-
CREATE TABLE IF NOT EXISTS public.plg_marketing_content_posts (
|
|
3675
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3676
|
-
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3677
|
-
plan_id uuid NOT NULL REFERENCES public.plg_marketing_content_plans(id) ON DELETE CASCADE,
|
|
3678
|
-
week_number integer NOT NULL DEFAULT 1,
|
|
3679
|
-
position integer NOT NULL DEFAULT 0,
|
|
3680
|
-
title text NOT NULL,
|
|
3681
|
-
format text NOT NULL DEFAULT 'reel'
|
|
3682
|
-
CHECK (format IN ('reel', 'static', 'carousel', 'story')),
|
|
3683
|
-
status text NOT NULL DEFAULT 'idea'
|
|
3684
|
-
CHECK (status IN ('idea', 'script', 'recording', 'editing', 'scheduled', 'published')),
|
|
3685
|
-
scheduled_date date,
|
|
3686
|
-
hook text,
|
|
3687
|
-
cta text,
|
|
3688
|
-
content_md text NOT NULL DEFAULT '',
|
|
3689
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3690
|
-
updated_at timestamptz NOT NULL DEFAULT now()
|
|
3691
|
-
);
|
|
3692
|
-
ALTER TABLE public.plg_marketing_content_posts ENABLE ROW LEVEL SECURITY;
|
|
3693
|
-
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_tenant ON public.plg_marketing_content_posts(tenant_id);
|
|
3694
|
-
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_plan ON public.plg_marketing_content_posts(plan_id, week_number, position);
|
|
3695
|
-
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_status ON public.plg_marketing_content_posts(tenant_id, status);
|
|
3696
|
-
|
|
3697
|
-
-- RLS policies for plg_marketing_social_accounts
|
|
3698
|
-
DROP POLICY IF EXISTS plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts;
|
|
3699
|
-
DROP POLICY IF EXISTS plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts;
|
|
3700
|
-
DROP POLICY IF EXISTS plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts;
|
|
3701
|
-
DROP POLICY IF EXISTS plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts;
|
|
3702
|
-
CREATE POLICY plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3703
|
-
CREATE POLICY plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3704
|
-
CREATE POLICY plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3705
|
-
CREATE POLICY plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3706
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_social_accounts TO authenticated;
|
|
3707
|
-
|
|
3708
|
-
-- RLS policies for plg_marketing_content_plans
|
|
3709
|
-
DROP POLICY IF EXISTS plg_marketing_content_plans_select ON public.plg_marketing_content_plans;
|
|
3710
|
-
DROP POLICY IF EXISTS plg_marketing_content_plans_insert ON public.plg_marketing_content_plans;
|
|
3711
|
-
DROP POLICY IF EXISTS plg_marketing_content_plans_update ON public.plg_marketing_content_plans;
|
|
3712
|
-
DROP POLICY IF EXISTS plg_marketing_content_plans_delete ON public.plg_marketing_content_plans;
|
|
3713
|
-
CREATE POLICY plg_marketing_content_plans_select ON public.plg_marketing_content_plans FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3714
|
-
CREATE POLICY plg_marketing_content_plans_insert ON public.plg_marketing_content_plans FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3715
|
-
CREATE POLICY plg_marketing_content_plans_update ON public.plg_marketing_content_plans FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3716
|
-
CREATE POLICY plg_marketing_content_plans_delete ON public.plg_marketing_content_plans FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3717
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_plans TO authenticated;
|
|
3718
|
-
|
|
3719
|
-
-- RLS policies for plg_marketing_content_posts
|
|
3720
|
-
DROP POLICY IF EXISTS plg_marketing_content_posts_select ON public.plg_marketing_content_posts;
|
|
3721
|
-
DROP POLICY IF EXISTS plg_marketing_content_posts_insert ON public.plg_marketing_content_posts;
|
|
3722
|
-
DROP POLICY IF EXISTS plg_marketing_content_posts_update ON public.plg_marketing_content_posts;
|
|
3723
|
-
DROP POLICY IF EXISTS plg_marketing_content_posts_delete ON public.plg_marketing_content_posts;
|
|
3724
|
-
CREATE POLICY plg_marketing_content_posts_select ON public.plg_marketing_content_posts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3725
|
-
CREATE POLICY plg_marketing_content_posts_insert ON public.plg_marketing_content_posts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3726
|
-
CREATE POLICY plg_marketing_content_posts_update ON public.plg_marketing_content_posts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3727
|
-
CREATE POLICY plg_marketing_content_posts_delete ON public.plg_marketing_content_posts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3728
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_posts TO authenticated;
|
|
3729
|
-
`;
|
|
3730
|
-
var MIGRATION_002_MULTI_PLATFORM = `-- Marketing Plugin: multi-platform accounts + per-post targets.
|
|
3731
|
-
-- An account is a brand/profile that publishes to MANY platforms (Instagram,
|
|
3732
|
-
-- YouTube, TikTok, ...). Posts optionally narrow the target platforms; an
|
|
3733
|
-
-- empty array means "inherit the account's platforms".
|
|
3734
|
-
|
|
3735
|
-
ALTER TABLE public.plg_marketing_social_accounts
|
|
3736
|
-
ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{instagram}';
|
|
3737
|
-
|
|
3738
|
-
-- Backfill from the legacy single-platform column, then drop it (also drops
|
|
3739
|
-
-- its CHECK constraint \u2014 the platform list is UI-driven from here on).
|
|
3740
|
-
DO $$
|
|
3741
|
-
BEGIN
|
|
3742
|
-
IF EXISTS (
|
|
3743
|
-
SELECT 1 FROM information_schema.columns
|
|
3744
|
-
WHERE table_schema = 'public' AND table_name = 'plg_marketing_social_accounts' AND column_name = 'platform'
|
|
3745
|
-
) THEN
|
|
3746
|
-
UPDATE public.plg_marketing_social_accounts
|
|
3747
|
-
SET platforms = ARRAY[platform]
|
|
3748
|
-
WHERE platform IS NOT NULL AND platforms = '{instagram}';
|
|
3749
|
-
ALTER TABLE public.plg_marketing_social_accounts DROP COLUMN platform;
|
|
3750
|
-
END IF;
|
|
3751
|
-
END $$;
|
|
3752
|
-
|
|
3753
|
-
ALTER TABLE public.plg_marketing_content_posts
|
|
3754
|
-
ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{}';
|
|
3755
|
-
`;
|
|
3756
|
-
var MIGRATION_003_RECORDING_OPS = `-- Marketing Plugin: recording-day ops on posts.
|
|
3757
|
-
-- checklist: shooting checklist items [{ id, text, done }] \u2014 the on-set
|
|
3758
|
-
-- companion for recording day. media_url: uploaded asset (static posts get a
|
|
3759
|
-
-- caption + final art instead of a script).
|
|
3760
|
-
|
|
3761
|
-
ALTER TABLE public.plg_marketing_content_posts
|
|
3762
|
-
ADD COLUMN IF NOT EXISTS checklist jsonb NOT NULL DEFAULT '[]';
|
|
3763
|
-
|
|
3764
|
-
ALTER TABLE public.plg_marketing_content_posts
|
|
3765
|
-
ADD COLUMN IF NOT EXISTS media_url text;
|
|
3766
|
-
|
|
3767
|
-
-- Public media bucket for content assets (mirrors the avatars-bucket pattern:
|
|
3768
|
-
-- public read, authenticated write). Path convention: content/{tenantId}/{postId}-{ts}.{ext}
|
|
3769
|
-
INSERT INTO storage.buckets (id, name, public)
|
|
3770
|
-
VALUES ('mkt-media', 'mkt-media', true)
|
|
3771
|
-
ON CONFLICT (id) DO NOTHING;
|
|
3772
|
-
|
|
3773
|
-
DROP POLICY IF EXISTS plg_marketing_media_read ON storage.objects;
|
|
3774
|
-
DROP POLICY IF EXISTS plg_marketing_media_insert ON storage.objects;
|
|
3775
|
-
DROP POLICY IF EXISTS plg_marketing_media_update ON storage.objects;
|
|
3776
|
-
DROP POLICY IF EXISTS plg_marketing_media_delete ON storage.objects;
|
|
3777
|
-
CREATE POLICY plg_marketing_media_read ON storage.objects FOR SELECT TO public USING (bucket_id = 'mkt-media');
|
|
3778
|
-
CREATE POLICY plg_marketing_media_insert ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'mkt-media');
|
|
3779
|
-
CREATE POLICY plg_marketing_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
|
|
3780
|
-
CREATE POLICY plg_marketing_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');
|
|
3781
|
-
`;
|
|
3782
|
-
var MIGRATION_004_RANKLAYER = `-- Marketing Plugin: RankLayer integration (SEO content publishing).
|
|
3783
|
-
-- Control-plane state for the RankLayer connector rendered in Marketing \u2192
|
|
3784
|
-
-- Integra\xE7\xF5es. Prefix: plg_marketing_ranklayer_. Scaffold: the real sync
|
|
3785
|
-
-- (edge function + RankLayer API calls) lands via an external PR \u2014 see
|
|
3786
|
-
-- src/integrations/ranklayer/RANKLAYER.md.
|
|
3787
|
-
|
|
3788
|
-
CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_integrations (
|
|
3789
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3790
|
-
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3791
|
-
api_key text,
|
|
3792
|
-
site_domain text,
|
|
3793
|
-
active boolean NOT NULL DEFAULT true,
|
|
3794
|
-
last_sync_at timestamptz,
|
|
3795
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3796
|
-
updated_at timestamptz NOT NULL DEFAULT now()
|
|
3797
|
-
);
|
|
3798
|
-
ALTER TABLE public.plg_marketing_ranklayer_integrations ENABLE ROW LEVEL SECURITY;
|
|
3799
|
-
CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_marketing_ranklayer_integrations_tenant
|
|
3800
|
-
ON public.plg_marketing_ranklayer_integrations(tenant_id);
|
|
3801
|
-
|
|
3802
|
-
CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_sync_log (
|
|
3803
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3804
|
-
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3805
|
-
integration_id uuid REFERENCES public.plg_marketing_ranklayer_integrations(id) ON DELETE CASCADE,
|
|
3806
|
-
status text NOT NULL DEFAULT 'success' CHECK (status IN ('success', 'partial', 'error')),
|
|
3807
|
-
message text,
|
|
3808
|
-
fetched integer NOT NULL DEFAULT 0,
|
|
3809
|
-
written integer NOT NULL DEFAULT 0,
|
|
3810
|
-
created_at timestamptz NOT NULL DEFAULT now()
|
|
3811
|
-
);
|
|
3812
|
-
ALTER TABLE public.plg_marketing_ranklayer_sync_log ENABLE ROW LEVEL SECURITY;
|
|
3813
|
-
CREATE INDEX IF NOT EXISTS idx_plg_marketing_ranklayer_sync_log_tenant
|
|
3814
|
-
ON public.plg_marketing_ranklayer_sync_log(tenant_id, created_at DESC);
|
|
3815
|
-
|
|
3816
|
-
-- RLS: authenticated tenant CRUD
|
|
3817
|
-
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations;
|
|
3818
|
-
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations;
|
|
3819
|
-
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations;
|
|
3820
|
-
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations;
|
|
3821
|
-
CREATE POLICY plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3822
|
-
CREATE POLICY plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3823
|
-
CREATE POLICY plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3824
|
-
CREATE POLICY plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3825
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_ranklayer_integrations TO authenticated;
|
|
3826
|
-
|
|
3827
|
-
DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log;
|
|
3828
|
-
DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log;
|
|
3829
|
-
CREATE POLICY plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3830
|
-
CREATE POLICY plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3831
|
-
GRANT SELECT, INSERT ON public.plg_marketing_ranklayer_sync_log TO authenticated;
|
|
3832
|
-
`;
|
|
3833
|
-
var INTEGRATIONS = "plg_marketing_ranklayer_integrations";
|
|
3834
|
-
var SYNC_LOG = "plg_marketing_ranklayer_sync_log";
|
|
3835
|
-
function getClient2() {
|
|
3836
|
-
const supabase = core.getSupabaseClientOptional();
|
|
3837
|
-
if (!supabase) throw new Error("[ranklayer] Supabase not initialized");
|
|
3838
|
-
return supabase;
|
|
3839
|
-
}
|
|
3840
|
-
function requireTenantId2() {
|
|
3841
|
-
const tenantId = core.getActiveTenantId();
|
|
3842
|
-
if (!tenantId) throw new Error("[ranklayer] No active tenant");
|
|
3843
|
-
return tenantId;
|
|
3844
|
-
}
|
|
3845
|
-
function toIntegration(row) {
|
|
3846
|
-
return {
|
|
3847
|
-
id: row.id,
|
|
3848
|
-
apiKey: row.api_key ?? void 0,
|
|
3849
|
-
siteDomain: row.site_domain ?? void 0,
|
|
3850
|
-
active: row.active ?? true,
|
|
3851
|
-
lastSyncAt: row.last_sync_at ?? void 0
|
|
3852
|
-
};
|
|
3853
|
-
}
|
|
3854
|
-
function createRankLayerProvider() {
|
|
3855
|
-
return {
|
|
3856
|
-
async getIntegration() {
|
|
3857
|
-
const tenantId = core.getActiveTenantId();
|
|
3858
|
-
if (!tenantId) return null;
|
|
3859
|
-
const { data } = await getClient2().from(INTEGRATIONS).select("*").eq("tenant_id", tenantId).maybeSingle();
|
|
3860
|
-
return data ? toIntegration(data) : null;
|
|
3861
|
-
},
|
|
3862
|
-
async saveIntegration(values) {
|
|
3863
|
-
const tenantId = requireTenantId2();
|
|
3864
|
-
const payload = {
|
|
3865
|
-
tenant_id: tenantId,
|
|
3866
|
-
api_key: values.apiKey,
|
|
3867
|
-
site_domain: values.siteDomain ?? null,
|
|
3868
|
-
active: true,
|
|
3869
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3870
|
-
};
|
|
3871
|
-
const { data, error } = await getClient2().from(INTEGRATIONS).upsert(payload, { onConflict: "tenant_id" }).select("*").single();
|
|
3872
|
-
if (error) throw error;
|
|
3873
|
-
return toIntegration(data);
|
|
3874
|
-
},
|
|
3875
|
-
async disconnect() {
|
|
3876
|
-
const tenantId = core.getActiveTenantId();
|
|
3877
|
-
if (!tenantId) return;
|
|
3878
|
-
await getClient2().from(INTEGRATIONS).delete().eq("tenant_id", tenantId);
|
|
3879
|
-
},
|
|
3880
|
-
// SCAFFOLD: no RankLayer API yet — accept any non-empty key so the connect
|
|
3881
|
-
// flow can be exercised. Replace with a real credential check in the PR.
|
|
3882
|
-
async testConnection(values) {
|
|
3883
|
-
if (!values.apiKey || values.apiKey.trim().length < 8) {
|
|
3884
|
-
return { ok: false, message: "Informe uma API Key v\xE1lida da RankLayer." };
|
|
3885
|
-
}
|
|
3886
|
-
return { ok: true, message: "Chave aceita (valida\xE7\xE3o real pendente \u2014 integra\xE7\xE3o em prepara\xE7\xE3o)." };
|
|
3887
|
-
},
|
|
3888
|
-
async getSyncLog(integrationId) {
|
|
3889
|
-
const { data } = await getClient2().from(SYNC_LOG).select("*").eq("integration_id", integrationId).order("created_at", { ascending: false }).limit(20);
|
|
3890
|
-
return (data ?? []).map((r) => ({
|
|
3891
|
-
id: r.id,
|
|
3892
|
-
createdAt: r.created_at,
|
|
3893
|
-
status: r.status,
|
|
3894
|
-
message: r.message ?? void 0,
|
|
3895
|
-
fetched: r.fetched ?? 0,
|
|
3896
|
-
written: r.written ?? 0
|
|
3897
|
-
}));
|
|
3898
|
-
}
|
|
3899
|
-
};
|
|
3900
|
-
}
|
|
3901
|
-
var provider = createRankLayerProvider();
|
|
3902
|
-
function RankLayerExtraPanel() {
|
|
3903
|
-
const [integration, setIntegration] = React8.useState(null);
|
|
3904
|
-
const [log, setLog] = React8.useState([]);
|
|
3905
|
-
const [loaded, setLoaded] = React8.useState(false);
|
|
3906
|
-
React8.useEffect(() => {
|
|
3907
|
-
let cancelled = false;
|
|
3908
|
-
(async () => {
|
|
3909
|
-
try {
|
|
3910
|
-
const found = await provider.getIntegration();
|
|
3911
|
-
if (cancelled) return;
|
|
3912
|
-
setIntegration(found);
|
|
3913
|
-
if (found) setLog(await provider.getSyncLog(found.id));
|
|
3914
|
-
} catch {
|
|
3915
|
-
} finally {
|
|
3916
|
-
if (!cancelled) setLoaded(true);
|
|
3917
|
-
}
|
|
3918
|
-
})();
|
|
3919
|
-
return () => {
|
|
3920
|
-
cancelled = true;
|
|
3921
|
-
};
|
|
3922
|
-
}, []);
|
|
3923
|
-
if (!loaded || !integration) return null;
|
|
3924
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4 border-t pt-4", children: [
|
|
3925
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3 rounded-md border border-dashed p-3", children: [
|
|
3926
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
3927
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
3928
|
-
/* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold", children: "Sincroniza\xE7\xE3o de conte\xFAdo \u2014 em breve" }),
|
|
3929
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
3930
|
-
"A publica\xE7\xE3o autom\xE1tica de p\xE1ginas e artigos de SEO com a RankLayer est\xE1 em prepara\xE7\xE3o.",
|
|
3931
|
-
integration.siteDomain ? ` Dom\xEDnio configurado: ${integration.siteDomain}.` : "",
|
|
3932
|
-
" Assim que a integra\xE7\xE3o estiver ativa, o hist\xF3rico de sincroniza\xE7\xF5es aparece aqui."
|
|
3933
|
-
] })
|
|
3934
|
-
] })
|
|
3935
|
-
] }),
|
|
3936
|
-
log.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
|
|
3937
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
3938
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.History, { className: "h-4 w-4 text-muted-foreground" }),
|
|
3939
|
-
/* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold", children: "Hist\xF3rico" })
|
|
3940
|
-
] }),
|
|
3941
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y rounded-md border text-sm", children: log.map((r) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [
|
|
3942
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: new Date(r.createdAt).toLocaleString("pt-BR") }),
|
|
3943
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-auto text-xs", children: [
|
|
3944
|
-
r.written,
|
|
3945
|
-
"/",
|
|
3946
|
-
r.fetched,
|
|
3947
|
-
" publicadas"
|
|
3948
|
-
] }),
|
|
3949
|
-
r.status === "success" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-3.5 w-3.5 text-warning" })
|
|
3950
|
-
] }, r.id)) })
|
|
3951
|
-
] })
|
|
3952
|
-
] });
|
|
3953
|
-
}
|
|
3954
|
-
var ranklayerConnectorDef = {
|
|
3955
|
-
id: "ranklayer",
|
|
3956
|
-
hostPluginId: "marketing",
|
|
3957
|
-
name: "RankLayer",
|
|
3958
|
-
description: "Publica\xE7\xE3o de conte\xFAdo e p\xE1ginas de SEO por servi\xE7o/bairro para ser encontrado no Google e em assistentes de IA.",
|
|
3959
|
-
icon: "Search",
|
|
3960
|
-
authKind: "api-key",
|
|
3961
|
-
fields: [
|
|
3962
|
-
{ key: "apiKey", label: "API Key", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" },
|
|
3963
|
-
{ key: "siteDomain", label: "Dom\xEDnio do site", type: "text", placeholder: "exemplo.com.br" }
|
|
3964
|
-
],
|
|
3965
|
-
async getStatus() {
|
|
3966
|
-
try {
|
|
3967
|
-
const found = await provider.getIntegration();
|
|
3968
|
-
return { connected: !!found?.apiKey, detail: found?.siteDomain };
|
|
3969
|
-
} catch {
|
|
3970
|
-
return { connected: false };
|
|
3971
|
-
}
|
|
3972
|
-
},
|
|
3973
|
-
testConnection: (values) => provider.testConnection({ apiKey: values.apiKey }),
|
|
3974
|
-
saveConnection: async (values) => {
|
|
3975
|
-
await provider.saveIntegration({ apiKey: values.apiKey, siteDomain: values.siteDomain });
|
|
3976
|
-
},
|
|
3977
|
-
disconnect: () => provider.disconnect(),
|
|
3978
|
-
ExtraPanel: RankLayerExtraPanel
|
|
3979
|
-
};
|
|
3980
|
-
|
|
3981
|
-
// src/locales/en.ts
|
|
3982
|
-
var en = {
|
|
3983
|
-
// metrics
|
|
3984
|
-
"marketing.metric.conversionRate": "Conversion rate",
|
|
3985
|
-
"marketing.metric.cpa": "Cost per acquisition",
|
|
3986
|
-
"marketing.metric.topChannel": "Top channel",
|
|
3987
|
-
"marketing.metric.spend": "spend",
|
|
3988
|
-
"marketing.metric.reach": "Reach",
|
|
3989
|
-
"marketing.metric.cvr": "CVR",
|
|
3990
|
-
"marketing.metric.totalVisits": "Total visits",
|
|
3991
|
-
"marketing.metric.pages": "Pages",
|
|
3992
|
-
"marketing.metric.avgConversion": "Avg. conversion",
|
|
3993
|
-
// columns
|
|
3994
|
-
"marketing.col.campaign": "Campaign",
|
|
3995
|
-
"marketing.col.channel": "Channel",
|
|
3996
|
-
"marketing.col.reach": "Reach",
|
|
3997
|
-
"marketing.col.leads": "Leads",
|
|
3998
|
-
"marketing.col.cvr": "CVR",
|
|
3999
|
-
"marketing.col.spend": "Spend",
|
|
4000
|
-
"marketing.col.cpa": "CPA",
|
|
4001
|
-
"marketing.col.status": "Status",
|
|
4002
|
-
"marketing.col.visits": "Visits",
|
|
4003
|
-
"marketing.col.conversions": "Conversions",
|
|
4004
|
-
"marketing.col.type": "Type",
|
|
4005
|
-
"marketing.col.page": "Page",
|
|
4006
|
-
// overview
|
|
4007
|
-
"marketing.overview.byChannelSuffix": "by channel",
|
|
4008
|
-
"marketing.overview.recent": "Recent campaigns",
|
|
4009
|
-
// channels
|
|
4010
|
-
"marketing.channels.subtitle": "Acquisition performance by channel",
|
|
4011
|
-
"marketing.channels.campaignsOn": "Campaigns on this channel",
|
|
4012
|
-
"marketing.channels.noCampaigns": "No campaigns yet on this channel.",
|
|
4013
|
-
// campaigns
|
|
4014
|
-
"marketing.campaigns.subtitle": "All acquisition campaigns and their conversion performance",
|
|
4015
|
-
"marketing.campaigns.new": "New campaign",
|
|
4016
|
-
"marketing.campaigns.empty": "No campaigns yet \u2014 create your first one.",
|
|
4017
|
-
"marketing.campaigns.delete": "Delete campaign",
|
|
4018
|
-
// composer
|
|
4019
|
-
"marketing.composer.title": "New campaign",
|
|
4020
|
-
"marketing.composer.name": "Campaign name",
|
|
4021
|
-
"marketing.composer.namePlaceholder": "e.g. Spring Promo \u2014 Paid Social",
|
|
4022
|
-
"marketing.composer.channel": "Channel",
|
|
4023
|
-
"marketing.composer.channelPlaceholder": "Select a channel",
|
|
4024
|
-
"marketing.composer.status": "Status",
|
|
4025
|
-
"marketing.composer.budget": "Budget / spend",
|
|
4026
|
-
"marketing.composer.cancel": "Cancel",
|
|
4027
|
-
"marketing.composer.create": "Create campaign",
|
|
4028
|
-
"marketing.status.draft": "draft",
|
|
4029
|
-
"marketing.status.active": "active",
|
|
4030
|
-
"marketing.status.paused": "paused",
|
|
4031
|
-
"marketing.status.ended": "ended",
|
|
4032
|
-
// funnel
|
|
4033
|
-
"marketing.funnel.subtitlePrefix": "Attribution funnel \u2014 reach to",
|
|
4034
|
-
"marketing.funnel.overall": "Overall conversion rate",
|
|
4035
|
-
"marketing.funnel.ofPrev": "of prev",
|
|
4036
|
-
// landing pages
|
|
4037
|
-
"marketing.landing.subtitle": "Per-page visits \u2192 conversions performance",
|
|
4038
|
-
// settings
|
|
4039
|
-
"marketing.settings.conversionModel": "Conversion model",
|
|
4040
|
-
"marketing.settings.conversionModelDesc": "What counts as a conversion in this workspace and where it's attributed from.",
|
|
4041
|
-
"marketing.settings.conversion": "Conversion",
|
|
4042
|
-
"marketing.settings.valueMetric": "Value metric",
|
|
4043
|
-
"marketing.settings.attributedFrom": "Attributed from",
|
|
4044
|
-
"marketing.settings.channels": "Acquisition channels",
|
|
4045
|
-
"marketing.settings.channelsDesc": "Sources tracked for this workspace.",
|
|
4046
|
-
"marketing.settings.trackValue": "Track monetary value",
|
|
4047
|
-
"marketing.settings.trackValueDesc": "Attribute revenue to channels and campaigns.",
|
|
4048
|
-
"marketing.settings.attribution": "Attribution",
|
|
4049
|
-
"marketing.settings.attributionDesc": "How conversions are matched to channels.",
|
|
4050
|
-
"marketing.settings.autoSync": "Auto-sync conversions",
|
|
4051
|
-
"marketing.settings.autoSyncDesc": "Pull conversions automatically from the source.",
|
|
4052
|
-
"marketing.settings.assisted": "Count assisted conversions",
|
|
4053
|
-
"marketing.settings.assistedDesc": "Credit channels that contributed to a conversion.",
|
|
4054
|
-
"marketing.settings.channelTrackDesc": "Track this acquisition source.",
|
|
4055
|
-
// content planner
|
|
4056
|
-
"marketing.content.title": "Content",
|
|
4057
|
-
"marketing.content.week": "Week",
|
|
4058
|
-
"marketing.content.month": "Month",
|
|
4059
|
-
"marketing.content.newPost": "New post",
|
|
4060
|
-
"marketing.content.untitled": "Untitled",
|
|
4061
|
-
"marketing.content.account": "Account",
|
|
4062
|
-
"marketing.content.addAccount": "Add account",
|
|
4063
|
-
"marketing.content.previewNow": "Just now",
|
|
4064
|
-
"marketing.content.previewLike": "Like",
|
|
4065
|
-
"marketing.content.previewComment": "Comment",
|
|
4066
|
-
"marketing.content.previewShare": "Share",
|
|
4067
|
-
"marketing.content.showLess": "show less",
|
|
4068
|
-
"marketing.content.checklistTitle": "Recording checklist",
|
|
4069
|
-
"marketing.content.checklistDescription": "Shooting-day operations",
|
|
4070
|
-
"marketing.content.checklistOpen": "Checklist",
|
|
4071
|
-
"marketing.content.checklistEmpty": "No items yet \u2014 generate them from the script or add your own below.",
|
|
4072
|
-
"marketing.content.checklistAddPlaceholder": "New item\u2026",
|
|
4073
|
-
"marketing.content.checklistAdd": "Add item",
|
|
4074
|
-
"marketing.content.checklistGenerate": "Generate from script",
|
|
4075
|
-
"marketing.content.scriptLabel": "Script",
|
|
4076
|
-
"marketing.content.captionLabel": "Caption",
|
|
4077
|
-
"marketing.content.captionPlaceholder": "Write the post caption\u2026",
|
|
4078
|
-
"marketing.content.mediaLabel": "Media asset",
|
|
4079
|
-
"marketing.content.mediaUpload": "Upload media",
|
|
4080
|
-
"marketing.content.mediaHint": "Image or video for this post",
|
|
4081
|
-
"marketing.content.mediaReplace": "Replace",
|
|
4082
|
-
"marketing.content.mediaRemove": "Remove",
|
|
4083
|
-
"marketing.content.mediaUnavailable": "Media upload requires a connected Supabase project.",
|
|
4084
|
-
"marketing.content.editAccountTitle": "Account settings",
|
|
4085
|
-
"marketing.content.accountSettings": "Account settings",
|
|
4086
|
-
"marketing.content.deleteAccount": "Delete account",
|
|
4087
|
-
"marketing.content.deleteAccountConfirm": "The account and all of its content plans and posts will be permanently deleted.",
|
|
4088
|
-
"marketing.content.noAccountsAvailable": "No accounts available",
|
|
4089
|
-
"marketing.content.addAccountTitle": "New account",
|
|
4090
|
-
"marketing.content.nameLabel": "Name",
|
|
4091
|
-
"marketing.content.handleLabel": "Handle",
|
|
4092
|
-
"marketing.content.handlePlaceholder": "@handle (optional)",
|
|
4093
|
-
"marketing.content.viewBoard": "Weekly",
|
|
4094
|
-
"marketing.content.viewCalendar": "Monthly",
|
|
4095
|
-
"marketing.content.viewList": "List",
|
|
4096
|
-
"marketing.content.unscheduled": "Unscheduled",
|
|
4097
|
-
"marketing.content.noPosts": "No posts yet \u2014 add your first one from any week.",
|
|
4098
|
-
"marketing.content.listTitle": "Title",
|
|
4099
|
-
"marketing.content.listWeek": "Week",
|
|
4100
|
-
"marketing.content.listDate": "Date",
|
|
4101
|
-
"marketing.content.listFormat": "Format",
|
|
4102
|
-
"marketing.content.listStatus": "Status",
|
|
4103
|
-
"marketing.content.noAccountsHint": "Create the account (e.g. your Instagram profile) to start planning content.",
|
|
4104
|
-
"marketing.content.accountNamePlaceholder": "e.g. @myclinic",
|
|
4105
|
-
"marketing.content.createAccount": "Create account",
|
|
4106
|
-
"marketing.content.defaultPlanName": "Content plan \u2014 8 weeks",
|
|
4107
|
-
"marketing.content.brief": "Brief",
|
|
4108
|
-
"marketing.content.planName": "Plan name",
|
|
4109
|
-
"marketing.content.objective": "Objective",
|
|
4110
|
-
"marketing.content.objectivePlaceholder": "e.g. generate evaluation bookings",
|
|
4111
|
-
"marketing.content.tone": "Tone of voice",
|
|
4112
|
-
"marketing.content.tonePlaceholder": "e.g. approachable authority, plain language",
|
|
4113
|
-
"marketing.content.pillars": "Content pillars",
|
|
4114
|
-
"marketing.content.pillarPlaceholder": "Add pillar\u2026",
|
|
4115
|
-
"marketing.content.formatsLabel": "Formats",
|
|
4116
|
-
"marketing.content.weeklyFrequency": "Posts per week",
|
|
4117
|
-
"marketing.content.weeksCount": "Weeks",
|
|
4118
|
-
"marketing.content.briefBody": "Brief (markdown)",
|
|
4119
|
-
"marketing.content.briefPlaceholder": "Describe the strategy, references and what the content should achieve\u2026",
|
|
4120
|
-
"marketing.content.formatLabel": "Format",
|
|
4121
|
-
"marketing.content.statusLabel": "Status",
|
|
4122
|
-
"marketing.content.scheduledDate": "Planned date",
|
|
4123
|
-
"marketing.content.hook": "Hook",
|
|
4124
|
-
"marketing.content.hookPlaceholder": "The first line that grabs attention\u2026",
|
|
4125
|
-
"marketing.content.cta": "CTA",
|
|
4126
|
-
"marketing.content.ctaPlaceholder": "e.g. Book your evaluation",
|
|
4127
|
-
"marketing.content.titlePlaceholder": "Post title\u2026",
|
|
4128
|
-
"marketing.content.bodyPlaceholder": "Post script / plan in markdown\u2026",
|
|
4129
|
-
"marketing.content.insertTemplate": "Insert script template",
|
|
4130
|
-
"marketing.content.save": "Save",
|
|
4131
|
-
"marketing.content.cancel": "Cancel",
|
|
4132
|
-
"marketing.content.edit": "Edit",
|
|
4133
|
-
"marketing.content.preview": "Preview",
|
|
4134
|
-
"marketing.content.deletePost": "Delete post",
|
|
4135
|
-
"marketing.content.deletePostConfirm": "This post and its script will be permanently removed.",
|
|
4136
|
-
"marketing.content.format.reel": "Reel",
|
|
4137
|
-
"marketing.content.format.static": "Static",
|
|
4138
|
-
"marketing.content.format.carousel": "Carousel",
|
|
4139
|
-
"marketing.content.format.story": "Story",
|
|
4140
|
-
"marketing.content.format.video": "Video",
|
|
4141
|
-
"marketing.content.format.live": "Live",
|
|
4142
|
-
"marketing.content.format.article": "Article",
|
|
4143
|
-
"marketing.content.platformsLabel": "Platforms",
|
|
4144
|
-
"marketing.content.status.idea": "idea",
|
|
4145
|
-
"marketing.content.status.script": "script",
|
|
4146
|
-
"marketing.content.status.recording": "recording",
|
|
4147
|
-
"marketing.content.status.editing": "editing",
|
|
4148
|
-
"marketing.content.status.scheduled": "scheduled",
|
|
4149
|
-
"marketing.content.status.published": "published"
|
|
4150
|
-
};
|
|
4151
|
-
|
|
4152
|
-
// src/locales/pt-BR.ts
|
|
4153
|
-
var ptBR = {
|
|
4154
|
-
// metrics
|
|
4155
|
-
"marketing.metric.conversionRate": "Taxa de convers\xE3o",
|
|
4156
|
-
"marketing.metric.cpa": "Custo por aquisi\xE7\xE3o",
|
|
4157
|
-
"marketing.metric.topChannel": "Principal canal",
|
|
4158
|
-
"marketing.metric.spend": "investido",
|
|
4159
|
-
"marketing.metric.reach": "Alcance",
|
|
4160
|
-
"marketing.metric.cvr": "CVR",
|
|
4161
|
-
"marketing.metric.totalVisits": "Total de visitas",
|
|
4162
|
-
"marketing.metric.pages": "P\xE1ginas",
|
|
4163
|
-
"marketing.metric.avgConversion": "Convers\xE3o m\xE9dia",
|
|
4164
|
-
// columns
|
|
4165
|
-
"marketing.col.campaign": "Campanha",
|
|
4166
|
-
"marketing.col.channel": "Canal",
|
|
4167
|
-
"marketing.col.reach": "Alcance",
|
|
4168
|
-
"marketing.col.leads": "Leads",
|
|
4169
|
-
"marketing.col.cvr": "CVR",
|
|
4170
|
-
"marketing.col.spend": "Investimento",
|
|
4171
|
-
"marketing.col.cpa": "CPA",
|
|
4172
|
-
"marketing.col.status": "Status",
|
|
4173
|
-
"marketing.col.visits": "Visitas",
|
|
4174
|
-
"marketing.col.conversions": "Convers\xF5es",
|
|
4175
|
-
"marketing.col.type": "Tipo",
|
|
4176
|
-
"marketing.col.page": "P\xE1gina",
|
|
4177
|
-
// overview
|
|
4178
|
-
"marketing.overview.byChannelSuffix": "por canal",
|
|
4179
|
-
"marketing.overview.recent": "Campanhas recentes",
|
|
4180
|
-
// channels
|
|
4181
|
-
"marketing.channels.subtitle": "Desempenho de aquisi\xE7\xE3o por canal",
|
|
4182
|
-
"marketing.channels.campaignsOn": "Campanhas neste canal",
|
|
4183
|
-
"marketing.channels.noCampaigns": "Nenhuma campanha ainda neste canal.",
|
|
4184
|
-
// campaigns
|
|
4185
|
-
"marketing.campaigns.subtitle": "Todas as campanhas de aquisi\xE7\xE3o e seu desempenho de convers\xE3o",
|
|
4186
|
-
"marketing.campaigns.new": "Nova campanha",
|
|
4187
|
-
"marketing.campaigns.empty": "Nenhuma campanha ainda \u2014 crie a primeira.",
|
|
4188
|
-
"marketing.campaigns.delete": "Excluir campanha",
|
|
4189
|
-
// composer
|
|
4190
|
-
"marketing.composer.title": "Nova campanha",
|
|
4191
|
-
"marketing.composer.name": "Nome da campanha",
|
|
4192
|
-
"marketing.composer.namePlaceholder": "ex.: Promo de Ver\xE3o \u2014 M\xEDdia Paga",
|
|
4193
|
-
"marketing.composer.channel": "Canal",
|
|
4194
|
-
"marketing.composer.channelPlaceholder": "Selecione um canal",
|
|
4195
|
-
"marketing.composer.status": "Status",
|
|
4196
|
-
"marketing.composer.budget": "Or\xE7amento / investimento",
|
|
4197
|
-
"marketing.composer.cancel": "Cancelar",
|
|
4198
|
-
"marketing.composer.create": "Criar campanha",
|
|
4199
|
-
"marketing.status.draft": "rascunho",
|
|
4200
|
-
"marketing.status.active": "ativa",
|
|
4201
|
-
"marketing.status.paused": "pausada",
|
|
4202
|
-
"marketing.status.ended": "encerrada",
|
|
4203
|
-
// funnel
|
|
4204
|
-
"marketing.funnel.subtitlePrefix": "Funil de atribui\xE7\xE3o \u2014 alcance at\xE9",
|
|
4205
|
-
"marketing.funnel.overall": "Taxa de convers\xE3o geral",
|
|
4206
|
-
"marketing.funnel.ofPrev": "do anterior",
|
|
4207
|
-
// landing pages
|
|
4208
|
-
"marketing.landing.subtitle": "Desempenho de visitas \u2192 convers\xF5es por p\xE1gina",
|
|
4209
|
-
// settings
|
|
4210
|
-
"marketing.settings.conversionModel": "Modelo de convers\xE3o",
|
|
4211
|
-
"marketing.settings.conversionModelDesc": "O que conta como convers\xE3o neste workspace e de onde \xE9 atribu\xEDda.",
|
|
4212
|
-
"marketing.settings.conversion": "Convers\xE3o",
|
|
4213
|
-
"marketing.settings.valueMetric": "M\xE9trica de valor",
|
|
4214
|
-
"marketing.settings.attributedFrom": "Atribu\xEDda de",
|
|
4215
|
-
"marketing.settings.channels": "Canais de aquisi\xE7\xE3o",
|
|
4216
|
-
"marketing.settings.channelsDesc": "Fontes monitoradas neste workspace.",
|
|
4217
|
-
"marketing.settings.trackValue": "Acompanhar valor monet\xE1rio",
|
|
4218
|
-
"marketing.settings.trackValueDesc": "Atribuir receita a canais e campanhas.",
|
|
4219
|
-
"marketing.settings.attribution": "Atribui\xE7\xE3o",
|
|
4220
|
-
"marketing.settings.attributionDesc": "Como as convers\xF5es s\xE3o associadas aos canais.",
|
|
4221
|
-
"marketing.settings.autoSync": "Sincronizar convers\xF5es automaticamente",
|
|
4222
|
-
"marketing.settings.autoSyncDesc": "Importar convers\xF5es automaticamente da fonte.",
|
|
4223
|
-
"marketing.settings.assisted": "Contar convers\xF5es assistidas",
|
|
4224
|
-
"marketing.settings.assistedDesc": "Creditar canais que contribu\xEDram para uma convers\xE3o.",
|
|
4225
|
-
"marketing.settings.channelTrackDesc": "Monitorar esta fonte de aquisi\xE7\xE3o.",
|
|
4226
|
-
// content planner
|
|
4227
|
-
"marketing.content.title": "Conte\xFAdo",
|
|
4228
|
-
"marketing.content.week": "Semana",
|
|
4229
|
-
"marketing.content.month": "M\xEAs",
|
|
4230
|
-
"marketing.content.newPost": "Novo post",
|
|
4231
|
-
"marketing.content.untitled": "Sem t\xEDtulo",
|
|
4232
|
-
"marketing.content.account": "Conta",
|
|
4233
|
-
"marketing.content.addAccount": "Adicionar conta",
|
|
4234
|
-
"marketing.content.previewNow": "Agora",
|
|
4235
|
-
"marketing.content.previewLike": "Curtir",
|
|
4236
|
-
"marketing.content.previewComment": "Comentar",
|
|
4237
|
-
"marketing.content.previewShare": "Compartilhar",
|
|
4238
|
-
"marketing.content.showLess": "mostrar menos",
|
|
4239
|
-
"marketing.content.checklistTitle": "Checklist de grava\xE7\xE3o",
|
|
4240
|
-
"marketing.content.checklistDescription": "Opera\xE7\xE3o do dia de grava\xE7\xE3o",
|
|
4241
|
-
"marketing.content.checklistOpen": "Checklist",
|
|
4242
|
-
"marketing.content.checklistEmpty": "Nenhum item ainda \u2014 gere a partir do roteiro ou adicione o seu abaixo.",
|
|
4243
|
-
"marketing.content.checklistAddPlaceholder": "Novo item\u2026",
|
|
4244
|
-
"marketing.content.checklistAdd": "Adicionar item",
|
|
4245
|
-
"marketing.content.checklistGenerate": "Gerar do roteiro",
|
|
4246
|
-
"marketing.content.scriptLabel": "Roteiro",
|
|
4247
|
-
"marketing.content.captionLabel": "Legenda",
|
|
4248
|
-
"marketing.content.captionPlaceholder": "Escreva a legenda do post\u2026",
|
|
4249
|
-
"marketing.content.mediaLabel": "Arte / m\xEDdia",
|
|
4250
|
-
"marketing.content.mediaUpload": "Enviar m\xEDdia",
|
|
4251
|
-
"marketing.content.mediaHint": "Imagem ou v\xEDdeo deste post",
|
|
4252
|
-
"marketing.content.mediaReplace": "Substituir",
|
|
4253
|
-
"marketing.content.mediaRemove": "Remover",
|
|
4254
|
-
"marketing.content.mediaUnavailable": "O upload de m\xEDdia requer um projeto Supabase conectado.",
|
|
4255
|
-
"marketing.content.editAccountTitle": "Configura\xE7\xF5es da conta",
|
|
4256
|
-
"marketing.content.accountSettings": "Configura\xE7\xF5es da conta",
|
|
4257
|
-
"marketing.content.deleteAccount": "Excluir conta",
|
|
4258
|
-
"marketing.content.deleteAccountConfirm": "A conta e todos os planos e posts de conte\xFAdo dela ser\xE3o exclu\xEDdos permanentemente.",
|
|
4259
|
-
"marketing.content.noAccountsAvailable": "Nenhuma conta dispon\xEDvel",
|
|
4260
|
-
"marketing.content.addAccountTitle": "Nova conta",
|
|
4261
|
-
"marketing.content.nameLabel": "Nome",
|
|
4262
|
-
"marketing.content.handleLabel": "Usu\xE1rio",
|
|
4263
|
-
"marketing.content.handlePlaceholder": "@usuario (opcional)",
|
|
4264
|
-
"marketing.content.viewBoard": "Semanal",
|
|
4265
|
-
"marketing.content.viewCalendar": "Mensal",
|
|
4266
|
-
"marketing.content.viewList": "Lista",
|
|
4267
|
-
"marketing.content.unscheduled": "Sem data",
|
|
4268
|
-
"marketing.content.noPosts": "Nenhum post ainda \u2014 adicione o primeiro em qualquer semana.",
|
|
4269
|
-
"marketing.content.listTitle": "T\xEDtulo",
|
|
4270
|
-
"marketing.content.listWeek": "Semana",
|
|
4271
|
-
"marketing.content.listDate": "Data",
|
|
4272
|
-
"marketing.content.listFormat": "Formato",
|
|
4273
|
-
"marketing.content.listStatus": "Status",
|
|
4274
|
-
"marketing.content.noAccountsHint": "Crie a conta (ex.: o perfil do Instagram) para come\xE7ar a planejar o conte\xFAdo.",
|
|
4275
|
-
"marketing.content.accountNamePlaceholder": "ex.: @minhaclinica",
|
|
4276
|
-
"marketing.content.createAccount": "Criar conta",
|
|
4277
|
-
"marketing.content.defaultPlanName": "Plano de conte\xFAdo \u2014 8 semanas",
|
|
4278
|
-
"marketing.content.brief": "Briefing",
|
|
4279
|
-
"marketing.content.planName": "Nome do plano",
|
|
4280
|
-
"marketing.content.objective": "Objetivo",
|
|
4281
|
-
"marketing.content.objectivePlaceholder": "ex.: gerar agendamentos de avalia\xE7\xE3o",
|
|
4282
|
-
"marketing.content.tone": "Tom de voz",
|
|
4283
|
-
"marketing.content.tonePlaceholder": "ex.: autoridade acess\xEDvel, linguagem simples",
|
|
4284
|
-
"marketing.content.pillars": "Pilares de conte\xFAdo",
|
|
4285
|
-
"marketing.content.pillarPlaceholder": "Adicionar pilar\u2026",
|
|
4286
|
-
"marketing.content.formatsLabel": "Formatos",
|
|
4287
|
-
"marketing.content.weeklyFrequency": "Posts por semana",
|
|
4288
|
-
"marketing.content.weeksCount": "Semanas",
|
|
4289
|
-
"marketing.content.briefBody": "Briefing (markdown)",
|
|
4290
|
-
"marketing.content.briefPlaceholder": "Descreva a estrat\xE9gia, refer\xEAncias e o que se quer com o conte\xFAdo\u2026",
|
|
4291
|
-
"marketing.content.formatLabel": "Formato",
|
|
4292
|
-
"marketing.content.statusLabel": "Status",
|
|
4293
|
-
"marketing.content.scheduledDate": "Data prevista",
|
|
4294
|
-
"marketing.content.hook": "Gancho",
|
|
4295
|
-
"marketing.content.hookPlaceholder": "A primeira frase que segura a aten\xE7\xE3o\u2026",
|
|
4296
|
-
"marketing.content.cta": "CTA",
|
|
4297
|
-
"marketing.content.ctaPlaceholder": "ex.: Agende sua avalia\xE7\xE3o",
|
|
4298
|
-
"marketing.content.titlePlaceholder": "T\xEDtulo do post\u2026",
|
|
4299
|
-
"marketing.content.bodyPlaceholder": "Roteiro / plano do post em markdown\u2026",
|
|
4300
|
-
"marketing.content.insertTemplate": "Inserir modelo de roteiro",
|
|
4301
|
-
"marketing.content.save": "Salvar",
|
|
4302
|
-
"marketing.content.cancel": "Cancelar",
|
|
4303
|
-
"marketing.content.edit": "Editar",
|
|
4304
|
-
"marketing.content.preview": "Visualizar",
|
|
4305
|
-
"marketing.content.deletePost": "Excluir post",
|
|
4306
|
-
"marketing.content.deletePostConfirm": "Este post e seu roteiro ser\xE3o removidos permanentemente.",
|
|
4307
|
-
"marketing.content.format.reel": "Reel",
|
|
4308
|
-
"marketing.content.format.static": "Est\xE1tico",
|
|
4309
|
-
"marketing.content.format.carousel": "Carrossel",
|
|
4310
|
-
"marketing.content.format.story": "Story",
|
|
4311
|
-
"marketing.content.format.video": "V\xEDdeo",
|
|
4312
|
-
"marketing.content.format.live": "Live",
|
|
4313
|
-
"marketing.content.format.article": "Artigo",
|
|
4314
|
-
"marketing.content.platformsLabel": "Plataformas",
|
|
4315
|
-
"marketing.content.status.idea": "ideia",
|
|
4316
|
-
"marketing.content.status.script": "roteiro",
|
|
4317
|
-
"marketing.content.status.recording": "grava\xE7\xE3o",
|
|
4318
|
-
"marketing.content.status.editing": "edi\xE7\xE3o",
|
|
4319
|
-
"marketing.content.status.scheduled": "agendado",
|
|
4320
|
-
"marketing.content.status.published": "publicado"
|
|
4321
|
-
};
|
|
4322
|
-
|
|
4323
|
-
// src/locales/index.ts
|
|
4324
|
-
var marketingLocales = {
|
|
4325
|
-
en,
|
|
4326
|
-
"pt-BR": ptBR
|
|
4327
|
-
};
|
|
4328
|
-
|
|
4329
|
-
// src/index.ts
|
|
4330
|
-
var DEFAULT_LABELS = {
|
|
4331
|
-
pageTitle: "Marketing",
|
|
4332
|
-
pageSubtitle: "Acquisition & conversion performance",
|
|
4333
|
-
overview: "Overview",
|
|
4334
|
-
channels: "Channels",
|
|
4335
|
-
campaigns: "Campaigns",
|
|
4336
|
-
funnel: "Funnel",
|
|
4337
|
-
landingPages: "Landing pages",
|
|
4338
|
-
content: "Content",
|
|
4339
|
-
blog: "Blog",
|
|
4340
|
-
blogCategories: "Categorias",
|
|
4341
|
-
settings: "Settings"
|
|
4342
|
-
};
|
|
4343
|
-
function resolveConfig(options) {
|
|
4344
|
-
const domain = options?.domain ?? "agency";
|
|
4345
|
-
const preset = MARKETING_PRESETS[domain];
|
|
4346
|
-
const config = {
|
|
4347
|
-
conversion: options?.conversion ?? preset.conversion,
|
|
4348
|
-
channels: options?.channels ?? preset.channels,
|
|
4349
|
-
modules: { ...preset.modules, ...options?.modules },
|
|
4350
|
-
currency: { ...DEFAULT_CURRENCY, ...options?.currency },
|
|
4351
|
-
labels: { ...DEFAULT_LABELS, ...options?.labels }
|
|
4352
|
-
};
|
|
4353
|
-
return { config, domain };
|
|
4354
|
-
}
|
|
4355
|
-
function createSafeProvider(config, domain) {
|
|
4356
|
-
void core.getSupabaseClientOptional();
|
|
4357
|
-
return createMockMarketingProvider({ channels: config.channels, conversion: config.conversion, domain });
|
|
4358
|
-
}
|
|
4359
|
-
function createMarketingPlugin(options) {
|
|
4360
|
-
core.registerTranslations(marketingLocales);
|
|
4361
|
-
const { config, domain } = resolveConfig(options);
|
|
4362
|
-
const provider2 = options?.dataProvider ?? createSafeProvider(config, domain);
|
|
4363
|
-
const store = createMarketingStore(provider2);
|
|
4364
|
-
const dashboardWidgets = createMarketingDashboardWidgets({ config, provider: provider2, store });
|
|
4365
|
-
const contentProvider = options?.contentPlanner?.dataProvider ?? core.createSafeDataProvider(
|
|
4366
|
-
() => createSupabaseContentPlannerProvider(),
|
|
4367
|
-
() => createMockContentPlannerProvider()
|
|
4368
|
-
);
|
|
4369
|
-
const contentStore = createContentPlannerStore(contentProvider);
|
|
4370
|
-
const PageComponent = () => React8__default.default.createElement(
|
|
4371
|
-
MarketingContextProvider,
|
|
4372
|
-
{ config, provider: provider2, store },
|
|
4373
|
-
React8__default.default.createElement(MarketingPage, { config, provider: provider2, store, contentProvider, contentStore })
|
|
4374
|
-
);
|
|
4375
|
-
PageComponent.displayName = "MarketingPage";
|
|
4376
|
-
const marketingRegistries = [
|
|
4377
|
-
...buildMarketingRegistries(config.channels),
|
|
4378
|
-
...options?.settingsRegistries ?? []
|
|
4379
|
-
];
|
|
4380
|
-
const SettingsComponent = () => React8__default.default.createElement(
|
|
4381
|
-
MarketingContextProvider,
|
|
4382
|
-
{ config, provider: provider2, store },
|
|
4383
|
-
React8__default.default.createElement(saas.PluginSettingsPanel, {
|
|
4384
|
-
title: config.labels.settings ?? config.labels.pageTitle,
|
|
4385
|
-
subtitle: config.labels.pageSubtitle,
|
|
4386
|
-
generalSettings: React8__default.default.createElement(SettingsView),
|
|
4387
|
-
registries: marketingRegistries,
|
|
4388
|
-
// Enables the "Integrações" tab (renders manifest connectors for this host).
|
|
4389
|
-
hostPluginId: "marketing",
|
|
4390
|
-
routeBase: "/settings/marketing"
|
|
4391
|
-
})
|
|
4392
|
-
);
|
|
4393
|
-
SettingsComponent.displayName = "MarketingSettings";
|
|
4394
|
-
return {
|
|
4395
|
-
id: "marketing",
|
|
4396
|
-
name: options?.navLabel ?? config.labels.pageTitle,
|
|
4397
|
-
icon: "Megaphone",
|
|
4398
|
-
version: "1.0.0",
|
|
4399
|
-
scope: options?.scope ?? "universal",
|
|
4400
|
-
verticalId: options?.verticalId,
|
|
4401
|
-
// Admin content-planning surface (campaigns, content calendar) — targets the
|
|
4402
|
-
// saas/admin Panel world, not the ecommerce storefront/operator console.
|
|
4403
|
-
scaffolds: ["saas"],
|
|
4404
|
-
defaultEnabled: true,
|
|
4405
|
-
dependencies: [],
|
|
4406
|
-
declaredFeatures: [
|
|
4407
|
-
{ id: "marketing", label: config.labels.pageTitle, group: "Engage" },
|
|
4408
|
-
...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages, group: "Engage" }] : [],
|
|
4409
|
-
...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : [],
|
|
4410
|
-
...config.modules.blog ? [{ id: "marketing.blog", label: config.labels.blog, group: "Engage" }] : []
|
|
4411
|
-
],
|
|
4412
|
-
navigation: [
|
|
4413
|
-
{
|
|
4414
|
-
section: options?.navSection ?? "main",
|
|
4415
|
-
position: options?.navPosition ?? 5,
|
|
4416
|
-
label: options?.navLabel ?? config.labels.pageTitle,
|
|
4417
|
-
route: "/marketing",
|
|
4418
|
-
icon: "Megaphone",
|
|
4419
|
-
permission: { feature: "marketing", action: "read" }
|
|
4420
|
-
}
|
|
4421
|
-
],
|
|
4422
|
-
routes: [
|
|
4423
|
-
{
|
|
4424
|
-
path: "/marketing",
|
|
4425
|
-
component: PageComponent,
|
|
4426
|
-
permission: { feature: "marketing", action: "read" }
|
|
4427
|
-
}
|
|
4428
|
-
],
|
|
4429
|
-
widgets: [],
|
|
4430
|
-
dashboardWidgets,
|
|
4431
|
-
events: [
|
|
4432
|
-
{ name: "marketing.conversion.tracked", description: "A conversion was attributed to a channel/campaign" },
|
|
4433
|
-
{ name: "marketing.campaign.created", description: "A campaign was created" },
|
|
4434
|
-
{ name: "marketing.campaign.updated", description: "A campaign was updated" },
|
|
4435
|
-
{ name: "marketing.channel.synced", description: "A channel pulled fresh attribution data" },
|
|
4436
|
-
{ name: "marketing.content.plan.updated", description: "A content plan (brief/config) was updated" },
|
|
4437
|
-
{ name: "marketing.content.post.created", description: "A content post was created" },
|
|
4438
|
-
{ name: "marketing.content.post.updated", description: "A content post was updated" }
|
|
4439
|
-
],
|
|
4440
|
-
migrations: [
|
|
4441
|
-
{
|
|
4442
|
-
id: "marketing-000-plg-rename",
|
|
4443
|
-
version: "1.0.0",
|
|
4444
|
-
sql: MIGRATION_000_PLG_RENAME,
|
|
4445
|
-
description: "Rename legacy mkt_* tables to plg_marketing_* in-place for pools provisioned before the industry-pool rename (guarded no-op on fresh pools)"
|
|
4446
|
-
},
|
|
4447
|
-
{
|
|
4448
|
-
id: "marketing-001-content-planner",
|
|
4449
|
-
version: "1.0.0",
|
|
4450
|
-
sql: MIGRATION_001_CONTENT_PLANNER,
|
|
4451
|
-
description: "Create plg_marketing_social_accounts, plg_marketing_content_plans and plg_marketing_content_posts"
|
|
4452
|
-
},
|
|
4453
|
-
{
|
|
4454
|
-
id: "marketing-002-multi-platform",
|
|
4455
|
-
version: "1.1.0",
|
|
4456
|
-
sql: MIGRATION_002_MULTI_PLATFORM,
|
|
4457
|
-
description: "Accounts publish to many platforms (platforms[]); posts gain optional platform targets"
|
|
4458
|
-
},
|
|
4459
|
-
{
|
|
4460
|
-
id: "marketing-003-recording-ops",
|
|
4461
|
-
version: "1.2.0",
|
|
4462
|
-
sql: MIGRATION_003_RECORDING_OPS,
|
|
4463
|
-
description: "Recording-day checklist + media asset URL on posts; content-media storage bucket"
|
|
4464
|
-
},
|
|
4465
|
-
{
|
|
4466
|
-
id: "marketing-004-ranklayer",
|
|
4467
|
-
version: "1.3.0",
|
|
4468
|
-
sql: MIGRATION_004_RANKLAYER,
|
|
4469
|
-
description: "RankLayer connector state: plg_marketing_ranklayer_integrations + _sync_log (tenant RLS)"
|
|
4470
|
-
}
|
|
4471
|
-
],
|
|
4472
|
-
// RankLayer SEO connector — renders in the Integrações settings tab. Scaffold
|
|
4473
|
-
// (control plane only); the real sync lands via an external PR (RANKLAYER.md).
|
|
4474
|
-
connectors: [ranklayerConnectorDef],
|
|
4475
|
-
aiTools: [
|
|
4476
|
-
{
|
|
4477
|
-
id: "marketing.channel-performance",
|
|
4478
|
-
name: "channelPerformance",
|
|
4479
|
-
description: "Returns acquisition performance per channel (reach, conversions, CVR, spend, CPA).",
|
|
4480
|
-
icon: "Radio",
|
|
4481
|
-
mode: "read",
|
|
4482
|
-
category: "Marketing",
|
|
4483
|
-
parameters: {
|
|
4484
|
-
type: "object",
|
|
4485
|
-
properties: { range: { type: "string", enum: ["7d", "30d", "90d"] } }
|
|
4486
|
-
},
|
|
4487
|
-
suggestions: [{ label: "Which channel converts best?" }, { label: "What is my cost per acquisition?" }],
|
|
4488
|
-
permission: { feature: "marketing", action: "read" }
|
|
4489
|
-
},
|
|
4490
|
-
{
|
|
4491
|
-
id: "marketing.top-channels",
|
|
4492
|
-
name: "topChannels",
|
|
4493
|
-
description: "Returns the top acquisition channels ranked by conversions.",
|
|
4494
|
-
icon: "Trophy",
|
|
4495
|
-
mode: "read",
|
|
4496
|
-
category: "Marketing",
|
|
4497
|
-
permission: { feature: "marketing", action: "read" }
|
|
4498
|
-
},
|
|
4499
|
-
{
|
|
4500
|
-
id: "marketing.campaign-cvr",
|
|
4501
|
-
name: "campaignCvr",
|
|
4502
|
-
description: "Lists campaigns with their conversion rate for a date range.",
|
|
4503
|
-
icon: "Percent",
|
|
4504
|
-
mode: "read",
|
|
4505
|
-
category: "Marketing",
|
|
4506
|
-
parameters: {
|
|
4507
|
-
type: "object",
|
|
4508
|
-
properties: { range: { type: "string", enum: ["7d", "30d", "90d"] } }
|
|
4509
|
-
},
|
|
4510
|
-
permission: { feature: "marketing", action: "read" }
|
|
4511
|
-
},
|
|
4512
|
-
{
|
|
4513
|
-
id: "marketing.create-campaign",
|
|
4514
|
-
name: "createCampaign",
|
|
4515
|
-
description: "Creates an acquisition campaign on a channel.",
|
|
4516
|
-
icon: "Plus",
|
|
4517
|
-
mode: "persist",
|
|
4518
|
-
category: "Marketing",
|
|
4519
|
-
parameters: {
|
|
4520
|
-
type: "object",
|
|
4521
|
-
properties: {
|
|
4522
|
-
name: { type: "string", description: "Campaign name" },
|
|
4523
|
-
channelId: { type: "string", description: "Acquisition channel id" }
|
|
4524
|
-
},
|
|
4525
|
-
required: ["name", "channelId"]
|
|
4526
|
-
},
|
|
4527
|
-
permission: { feature: "marketing", action: "create" }
|
|
4528
|
-
}
|
|
4529
|
-
],
|
|
4530
|
-
registries: marketingRegistries,
|
|
4531
|
-
settings: [
|
|
4532
|
-
{
|
|
4533
|
-
id: "marketing",
|
|
4534
|
-
label: config.labels.pageTitle,
|
|
4535
|
-
icon: "Megaphone",
|
|
4536
|
-
component: SettingsComponent,
|
|
4537
|
-
order: 20,
|
|
4538
|
-
permission: { feature: "marketing", action: "read" }
|
|
4539
|
-
}
|
|
4540
|
-
],
|
|
4541
|
-
locales: marketingLocales
|
|
4542
|
-
};
|
|
4543
|
-
}
|
|
4544
|
-
|
|
4545
|
-
exports.MARKETING_PRESETS = MARKETING_PRESETS;
|
|
4546
|
-
exports.createMarketingPlugin = createMarketingPlugin;
|
|
4547
|
-
exports.createMockContentPlannerProvider = createMockContentPlannerProvider;
|
|
4548
|
-
exports.createMockMarketingProvider = createMockMarketingProvider;
|
|
4549
|
-
exports.createSupabaseContentPlannerProvider = createSupabaseContentPlannerProvider;
|
|
4550
|
-
//# sourceMappingURL=index.cjs.map
|
|
4551
|
-
//# sourceMappingURL=index.cjs.map
|