@fayz-ai/plugin-marketing 0.2.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/MarketingContext.d.ts +1 -0
- package/dist/MarketingContext.d.ts.map +1 -1
- package/dist/MarketingPage.d.ts +5 -1
- package/dist/MarketingPage.d.ts.map +1 -1
- package/dist/components/icons.d.ts +4 -0
- package/dist/components/icons.d.ts.map +1 -1
- package/dist/data/contentMedia.d.ts +3 -0
- package/dist/data/contentMedia.d.ts.map +1 -0
- package/dist/data/contentMock.d.ts +3 -0
- package/dist/data/contentMock.d.ts.map +1 -0
- package/dist/data/contentSupabase.d.ts +3 -0
- package/dist/data/contentSupabase.d.ts.map +1 -0
- package/dist/data/contentTypes.d.ts +111 -0
- package/dist/data/contentTypes.d.ts.map +1 -0
- package/dist/data/tables.d.ts +6 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/index.cjs +2813 -129
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2815 -133
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +9 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/schema/index.d.ts +717 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema.cjs +65 -0
- package/dist/schema.cjs.map +1 -0
- package/dist/schema.js +61 -0
- package/dist/schema.js.map +1 -0
- package/dist/views/content/ContentPlannerContext.d.ts +13 -0
- package/dist/views/content/ContentPlannerContext.d.ts.map +1 -0
- package/dist/views/content/ContentView.d.ts +5 -0
- package/dist/views/content/ContentView.d.ts.map +1 -0
- package/dist/views/content/PlanBriefView.d.ts +12 -0
- package/dist/views/content/PlanBriefView.d.ts.map +1 -0
- package/dist/views/content/PostPage.d.ts +6 -0
- package/dist/views/content/PostPage.d.ts.map +1 -0
- package/dist/views/content/PostSocialPreview.d.ts +11 -0
- package/dist/views/content/PostSocialPreview.d.ts.map +1 -0
- package/dist/views/content/RecordingChecklist.d.ts +6 -0
- package/dist/views/content/RecordingChecklist.d.ts.map +1 -0
- package/dist/views/content/contentBits.d.ts +28 -0
- package/dist/views/content/contentBits.d.ts.map +1 -0
- package/dist/views/content/contentStore.d.ts +28 -0
- package/dist/views/content/contentStore.d.ts.map +1 -0
- package/dist/views/content/templates.d.ts +3 -0
- package/dist/views/content/templates.d.ts.map +1 -0
- package/package.json +17 -4
- package/src/MarketingContext.tsx +1 -0
- package/src/MarketingPage.tsx +23 -3
- package/src/components/icons.tsx +19 -0
- package/src/data/contentMedia.ts +25 -0
- package/src/data/contentMock.ts +253 -0
- package/src/data/contentSupabase.ts +263 -0
- package/src/data/contentTypes.ts +138 -0
- package/src/data/tables.ts +7 -0
- package/src/index.ts +64 -2
- package/src/locales/en.ts +95 -0
- package/src/locales/pt-BR.ts +95 -0
- package/src/migrations/000_plg_rename.sql +15 -0
- package/src/migrations/001_content_planner.sql +94 -0
- package/src/migrations/002_multi_platform.sql +25 -0
- package/src/migrations/003_recording_ops.sql +25 -0
- package/src/migrations/index.ts +177 -0
- package/src/presets.ts +5 -3
- package/src/schema/index.ts +69 -0
- package/src/views/content/ContentPlannerContext.tsx +13 -0
- package/src/views/content/ContentView.tsx +901 -0
- package/src/views/content/PlanBriefView.tsx +268 -0
- package/src/views/content/PostPage.tsx +370 -0
- package/src/views/content/PostSocialPreview.tsx +212 -0
- package/src/views/content/RecordingChecklist.tsx +145 -0
- package/src/views/content/contentBits.tsx +114 -0
- package/src/views/content/contentStore.ts +160 -0
- package/src/views/content/templates.ts +172 -0
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var React8 = require('react');
|
|
4
4
|
var core = require('@fayz-ai/core');
|
|
5
5
|
var saas = require('@fayz-ai/saas');
|
|
6
6
|
var ui = require('@fayz-ai/ui');
|
|
@@ -10,7 +10,7 @@ var vanilla = require('zustand/vanilla');
|
|
|
10
10
|
|
|
11
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
12
|
|
|
13
|
-
var
|
|
13
|
+
var React8__default = /*#__PURE__*/_interopDefault(React8);
|
|
14
14
|
|
|
15
15
|
// src/index.ts
|
|
16
16
|
var ctx = saas.createPluginContext("MarketingPage");
|
|
@@ -20,7 +20,7 @@ var useMarketingProvider = ctx.useProvider;
|
|
|
20
20
|
var useMarketingStore = ctx.useStore;
|
|
21
21
|
function useChannelLookup() {
|
|
22
22
|
const { channels } = useMarketingConfig();
|
|
23
|
-
return
|
|
23
|
+
return React8__default.default.useMemo(() => new Map(channels.map((c) => [c.id, c])), [channels]);
|
|
24
24
|
}
|
|
25
25
|
function OverviewView() {
|
|
26
26
|
const setRange = useMarketingStore((s) => s.setRange);
|
|
@@ -82,6 +82,20 @@ function ChannelIcon({ name, className }) {
|
|
|
82
82
|
const Icon = ICONS[name] ?? lucideReact.Link2;
|
|
83
83
|
return /* @__PURE__ */ jsxRuntime.jsx(Icon, { className });
|
|
84
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
|
+
}
|
|
85
99
|
function KpiCard({
|
|
86
100
|
icon: Icon,
|
|
87
101
|
label,
|
|
@@ -166,7 +180,7 @@ function ChannelsView({ onOpen }) {
|
|
|
166
180
|
const { conversion, currency } = useMarketingConfig();
|
|
167
181
|
const channels = useMarketingStore((s) => s.channels);
|
|
168
182
|
const sorted = [...channels].sort((a, b) => b.conversions - a.conversions);
|
|
169
|
-
const columns =
|
|
183
|
+
const columns = React8__default.default.useMemo(() => [
|
|
170
184
|
{ accessorKey: "channelId", header: t("marketing.col.channel"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx(ChannelCell, { channelId: getValue() }) },
|
|
171
185
|
{ accessorKey: "reach", header: t("marketing.col.reach"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
172
186
|
{ accessorKey: "leads", header: t("marketing.col.leads"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
@@ -206,13 +220,13 @@ function ChannelDetailView({ channelId, onBack }) {
|
|
|
206
220
|
const channels = useMarketingStore((s) => s.channels);
|
|
207
221
|
const lookup = useChannelLookup();
|
|
208
222
|
const provider = useMarketingProvider();
|
|
209
|
-
const [campaigns, setCampaigns] =
|
|
210
|
-
|
|
223
|
+
const [campaigns, setCampaigns] = React8__default.default.useState([]);
|
|
224
|
+
React8__default.default.useEffect(() => {
|
|
211
225
|
void provider.listCampaigns({ range, channelId }).then(setCampaigns);
|
|
212
226
|
}, [provider, range, channelId]);
|
|
213
227
|
const perf = channels.find((c) => c.channelId === channelId);
|
|
214
228
|
const channel = lookup.get(channelId);
|
|
215
|
-
const columns =
|
|
229
|
+
const columns = React8__default.default.useMemo(() => [
|
|
216
230
|
{ accessorKey: "name", header: t("marketing.col.campaign"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
|
|
217
231
|
{ accessorKey: "conversions", header: conversion.labelPlural, cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
218
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()) }) },
|
|
@@ -249,12 +263,12 @@ function CampaignComposer({ open, onClose }) {
|
|
|
249
263
|
const t = core.useTranslation();
|
|
250
264
|
const { channels } = useMarketingConfig();
|
|
251
265
|
const saveCampaign = useMarketingStore((s) => s.saveCampaign);
|
|
252
|
-
const [name, setName] =
|
|
253
|
-
const [channelId, setChannelId] =
|
|
254
|
-
const [status, setStatus] =
|
|
255
|
-
const [spend, setSpend] =
|
|
256
|
-
const [saving, setSaving] =
|
|
257
|
-
|
|
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(() => {
|
|
258
272
|
if (open) {
|
|
259
273
|
setName("");
|
|
260
274
|
setChannelId(channels[0]?.id ?? "");
|
|
@@ -316,8 +330,8 @@ function CampaignsView() {
|
|
|
316
330
|
const { conversion, currency } = useMarketingConfig();
|
|
317
331
|
const campaigns = useMarketingStore((s) => s.campaigns);
|
|
318
332
|
const deleteCampaign = useMarketingStore((s) => s.deleteCampaign);
|
|
319
|
-
const [composerOpen, setComposerOpen] =
|
|
320
|
-
const columns =
|
|
333
|
+
const [composerOpen, setComposerOpen] = React8__default.default.useState(false);
|
|
334
|
+
const columns = React8__default.default.useMemo(() => [
|
|
321
335
|
{ accessorKey: "name", header: t("marketing.col.campaign"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
|
|
322
336
|
{ accessorKey: "channelId", header: t("marketing.col.channel"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx(ChannelCell, { channelId: getValue() }) },
|
|
323
337
|
{ accessorKey: "reach", header: t("marketing.col.reach"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
@@ -345,102 +359,1824 @@ function CampaignsView() {
|
|
|
345
359
|
}
|
|
346
360
|
)
|
|
347
361
|
}
|
|
348
|
-
], [t, conversion.labelPlural, currency, deleteCampaign]);
|
|
349
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
|
|
350
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
351
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.campaigns.subtitle") }),
|
|
352
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
353
|
-
/* @__PURE__ */ jsxRuntime.jsx(RangeTabs, {}),
|
|
354
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { onClick: () => setComposerOpen(true), children: [
|
|
355
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "mr-1.5 h-4 w-4" }),
|
|
356
|
-
" ",
|
|
357
|
-
t("marketing.campaigns.new")
|
|
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")
|
|
358
1939
|
] })
|
|
359
1940
|
] })
|
|
360
|
-
] })
|
|
361
|
-
|
|
362
|
-
|
|
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
|
+
)
|
|
363
1955
|
] });
|
|
364
1956
|
}
|
|
365
|
-
function
|
|
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 }) {
|
|
366
1970
|
const t = core.useTranslation();
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
1971
|
+
const provider = 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 provider.getPost(postId).then((fetched) => {
|
|
1989
|
+
if (fetched) {
|
|
1990
|
+
setPost(fetched);
|
|
1991
|
+
setDraft(toDraft2(fetched));
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1994
|
+
}, [postId, storePost, post?.id, provider]);
|
|
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" })
|
|
376
2000
|
] }),
|
|
377
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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)) })
|
|
396
2080
|
] })
|
|
397
2081
|
] }),
|
|
398
|
-
/* @__PURE__ */ jsxRuntime.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
+
] })
|
|
405
2129
|
] }),
|
|
406
|
-
/* @__PURE__ */ jsxRuntime.
|
|
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
|
+
)
|
|
407
2176
|
] })
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
var TYPE_STYLE = {
|
|
412
|
-
Funnel: "bg-indigo-100 text-indigo-700",
|
|
413
|
-
"Landing page": "bg-pink-100 text-pink-700",
|
|
414
|
-
Website: "bg-sky-100 text-sky-700"
|
|
415
|
-
};
|
|
416
|
-
function LandingPagesView() {
|
|
417
|
-
const t = core.useTranslation();
|
|
418
|
-
const pages = useMarketingStore((s) => s.landingPages);
|
|
419
|
-
const totalVisits = pages.reduce((s, p) => s + p.visits, 0);
|
|
420
|
-
const totalConv = pages.reduce((s, p) => s + p.conversions, 0);
|
|
421
|
-
const avgCvr = totalVisits > 0 ? totalConv / totalVisits * 100 : 0;
|
|
422
|
-
const columns = React4__default.default.useMemo(() => [
|
|
423
|
-
{ accessorKey: "name", header: t("marketing.col.page"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
|
|
424
|
-
{ accessorKey: "type", header: t("marketing.col.type"), cell: ({ getValue }) => {
|
|
425
|
-
const v = getValue();
|
|
426
|
-
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 });
|
|
427
|
-
} },
|
|
428
|
-
{ accessorKey: "visits", header: t("marketing.col.visits"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
429
|
-
{ accessorKey: "conversions", header: t("marketing.col.conversions"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: formatNumber(getValue()) }) },
|
|
430
|
-
{ accessorKey: "cvr", header: t("marketing.col.cvr"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatPercent(getValue()) }) }
|
|
431
|
-
], [t]);
|
|
432
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
|
|
433
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
434
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.landing.subtitle") }),
|
|
435
|
-
/* @__PURE__ */ jsxRuntime.jsx(RangeTabs, {})
|
|
436
|
-
] }),
|
|
437
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-3 gap-4", children: [
|
|
438
|
-
/* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.MousePointerClick, label: t("marketing.metric.totalVisits"), value: formatNumber(totalVisits) }),
|
|
439
|
-
/* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.Layers, label: t("marketing.metric.pages"), value: String(pages.length) }),
|
|
440
|
-
/* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.Percent, label: t("marketing.metric.avgConversion"), value: formatPercent(avgCvr) })
|
|
441
|
-
] }),
|
|
442
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns, data: pages, emptyMessage: t("marketing.landing.subtitle") })
|
|
443
|
-
] });
|
|
2177
|
+
},
|
|
2178
|
+
post.id
|
|
2179
|
+
);
|
|
444
2180
|
}
|
|
445
2181
|
function buildNav(config, view, navigate) {
|
|
446
2182
|
const items = [
|
|
@@ -464,9 +2200,18 @@ function buildNav(config, view, navigate) {
|
|
|
464
2200
|
if (config.modules.landingPages) {
|
|
465
2201
|
items.push({ id: "landing-pages", label: config.labels.landingPages, icon: "LayoutTemplate", active: view === "landing-pages", onClick: () => navigate("landing-pages") });
|
|
466
2202
|
}
|
|
2203
|
+
if (config.modules.contentPlanner) {
|
|
2204
|
+
items.push({
|
|
2205
|
+
id: "content",
|
|
2206
|
+
label: config.labels.content,
|
|
2207
|
+
icon: "Clapperboard",
|
|
2208
|
+
active: view === "content" || view === "content-brief" || view.startsWith("content-post:"),
|
|
2209
|
+
onClick: () => navigate("content")
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
467
2212
|
return items;
|
|
468
2213
|
}
|
|
469
|
-
function MarketingPage({ config, provider, store }) {
|
|
2214
|
+
function MarketingPage({ config, provider, store, contentProvider, contentStore }) {
|
|
470
2215
|
const t = core.useTranslation();
|
|
471
2216
|
const { view, direction, navigate } = saas.useModuleNavigation("/marketing", {
|
|
472
2217
|
overview: 0,
|
|
@@ -474,14 +2219,17 @@ function MarketingPage({ config, provider, store }) {
|
|
|
474
2219
|
campaigns: 0,
|
|
475
2220
|
funnel: 0,
|
|
476
2221
|
"landing-pages": 0,
|
|
477
|
-
|
|
2222
|
+
content: 0,
|
|
2223
|
+
"channel-detail": 1,
|
|
2224
|
+
"content-post": 1,
|
|
2225
|
+
"content-brief": 1
|
|
478
2226
|
}, "overview");
|
|
479
|
-
|
|
2227
|
+
React8__default.default.useEffect(() => {
|
|
480
2228
|
void store.getState().load();
|
|
481
2229
|
}, [store]);
|
|
482
2230
|
const isOverview = view === "overview";
|
|
483
2231
|
const nav = buildNav(config, view, navigate);
|
|
484
|
-
const quickActions =
|
|
2232
|
+
const quickActions = React8__default.default.useMemo(() => [
|
|
485
2233
|
{ id: "new-campaign", label: t("marketing.campaigns.new"), icon: "Megaphone", description: config.labels.campaigns, action: () => navigate("campaigns") }
|
|
486
2234
|
], [t, config.labels.campaigns]);
|
|
487
2235
|
const renderView = saas.createViewRouter([
|
|
@@ -490,9 +2238,12 @@ function MarketingPage({ config, provider, store }) {
|
|
|
490
2238
|
{ id: "campaigns", render: () => /* @__PURE__ */ jsxRuntime.jsx(CampaignsView, {}) },
|
|
491
2239
|
{ id: "funnel", render: () => /* @__PURE__ */ jsxRuntime.jsx(FunnelView, {}) },
|
|
492
2240
|
{ id: "landing-pages", render: () => /* @__PURE__ */ jsxRuntime.jsx(LandingPagesView, {}) },
|
|
2241
|
+
{ id: "content", render: () => /* @__PURE__ */ jsxRuntime.jsx(ContentView, { onOpenPost: (id) => navigate(`content-post:${id}`) }) },
|
|
2242
|
+
{ id: "content-post", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(PostPage, { postId: id, onBack: () => navigate("content") }) },
|
|
2243
|
+
{ id: "content-brief", render: () => /* @__PURE__ */ jsxRuntime.jsx(PlanBriefView, { onBack: () => navigate("content") }) },
|
|
493
2244
|
{ id: "overview", render: () => /* @__PURE__ */ jsxRuntime.jsx(OverviewView, {}) }
|
|
494
2245
|
], "overview");
|
|
495
|
-
return /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2246
|
+
return /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
496
2247
|
ui.ModulePage,
|
|
497
2248
|
{
|
|
498
2249
|
title: config.labels.pageTitle,
|
|
@@ -511,7 +2262,7 @@ function MarketingPage({ config, provider, store }) {
|
|
|
511
2262
|
),
|
|
512
2263
|
children: renderView(view)
|
|
513
2264
|
}
|
|
514
|
-
) });
|
|
2265
|
+
) }) });
|
|
515
2266
|
}
|
|
516
2267
|
var SOURCE_OPTIONS = [
|
|
517
2268
|
{ value: "crm", label: "CRM" },
|
|
@@ -522,11 +2273,11 @@ var SOURCE_OPTIONS = [
|
|
|
522
2273
|
function SettingsView() {
|
|
523
2274
|
const t = core.useTranslation();
|
|
524
2275
|
const { conversion, channels } = useMarketingConfig();
|
|
525
|
-
const [source, setSource] =
|
|
526
|
-
const [trackValue, setTrackValue] =
|
|
527
|
-
const [autoSync, setAutoSync] =
|
|
528
|
-
const [assisted, setAssisted] =
|
|
529
|
-
const [tracked, setTracked] =
|
|
2276
|
+
const [source, setSource] = React8__default.default.useState(conversion.source);
|
|
2277
|
+
const [trackValue, setTrackValue] = React8__default.default.useState(true);
|
|
2278
|
+
const [autoSync, setAutoSync] = React8__default.default.useState(true);
|
|
2279
|
+
const [assisted, setAssisted] = React8__default.default.useState(false);
|
|
2280
|
+
const [tracked, setTracked] = React8__default.default.useState(
|
|
530
2281
|
() => Object.fromEntries(channels.map((c) => [c.id, true]))
|
|
531
2282
|
);
|
|
532
2283
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
@@ -623,7 +2374,7 @@ function useEnsureMarketingData() {
|
|
|
623
2374
|
const overview = useMarketingStore((s) => s.overview);
|
|
624
2375
|
const loading = useMarketingStore((s) => s.loading);
|
|
625
2376
|
const load = useMarketingStore((s) => s.load);
|
|
626
|
-
|
|
2377
|
+
React8.useEffect(() => {
|
|
627
2378
|
if (!overview && !loading) void load();
|
|
628
2379
|
}, []);
|
|
629
2380
|
}
|
|
@@ -729,9 +2480,9 @@ function CampaignsTable() {
|
|
|
729
2480
|
const recent = campaigns.filter((c) => c.status !== "draft").slice(0, 5);
|
|
730
2481
|
return /* @__PURE__ */ jsxRuntime.jsx(ui.TableWidget, { title: t("marketing.overview.recent"), icon: "Megaphone", columns, data: recent, emptyMessage: t("marketing.campaigns.empty") });
|
|
731
2482
|
}
|
|
732
|
-
function createMarketingDashboardWidgets(
|
|
2483
|
+
function createMarketingDashboardWidgets(ctx3) {
|
|
733
2484
|
const withCtx = (Inner) => {
|
|
734
|
-
const Wrapped = () => /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config:
|
|
2485
|
+
const Wrapped = () => /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config: ctx3.config, provider: ctx3.provider, store: ctx3.store, children: /* @__PURE__ */ jsxRuntime.jsx(Inner, {}) });
|
|
735
2486
|
Wrapped.displayName = `MarketingWidget(${Inner.displayName ?? Inner.name})`;
|
|
736
2487
|
return Wrapped;
|
|
737
2488
|
};
|
|
@@ -767,7 +2518,7 @@ var AGENCY = {
|
|
|
767
2518
|
{ id: "referral", label: "Referral", icon: "Users", kind: "referral" },
|
|
768
2519
|
{ id: "direct", label: "Direct", icon: "MousePointerClick", kind: "direct" }
|
|
769
2520
|
],
|
|
770
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, broadcasts: false, journeys: false }
|
|
2521
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, broadcasts: false, journeys: false }
|
|
771
2522
|
};
|
|
772
2523
|
var BEAUTY = {
|
|
773
2524
|
conversion: {
|
|
@@ -785,7 +2536,7 @@ var BEAUTY = {
|
|
|
785
2536
|
{ id: "whatsapp", label: "WhatsApp", icon: "MessageCircle", kind: "social" },
|
|
786
2537
|
{ id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
|
|
787
2538
|
],
|
|
788
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, broadcasts: false, journeys: false }
|
|
2539
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, broadcasts: false, journeys: false }
|
|
789
2540
|
};
|
|
790
2541
|
var RESTO = {
|
|
791
2542
|
conversion: {
|
|
@@ -803,7 +2554,7 @@ var RESTO = {
|
|
|
803
2554
|
{ id: "instagram", label: "Instagram", icon: "Instagram", kind: "social" },
|
|
804
2555
|
{ id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
|
|
805
2556
|
],
|
|
806
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, broadcasts: false, journeys: false }
|
|
2557
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, broadcasts: false, journeys: false }
|
|
807
2558
|
};
|
|
808
2559
|
var MARKETING_PRESETS = {
|
|
809
2560
|
agency: AGENCY,
|
|
@@ -942,7 +2693,7 @@ function createMockMarketingProvider(cfg) {
|
|
|
942
2693
|
const base = channelBase30(cfg.channels, cfg.conversion);
|
|
943
2694
|
const campaigns = seedCampaigns(cfg);
|
|
944
2695
|
const landing = seedLandingPages();
|
|
945
|
-
let
|
|
2696
|
+
let counter2 = 100;
|
|
946
2697
|
async function channelPerformance(query) {
|
|
947
2698
|
const list = base.map((b) => toPerformance(b, query.range));
|
|
948
2699
|
return query.channelId ? list.filter((c) => c.channelId === query.channelId) : list;
|
|
@@ -1007,7 +2758,7 @@ function createMockMarketingProvider(cfg) {
|
|
|
1007
2758
|
const b = base.find((x) => x.channelId === input.channelId) ?? base[0];
|
|
1008
2759
|
const active = input.status === "active";
|
|
1009
2760
|
const created = {
|
|
1010
|
-
id: `cmp-${++
|
|
2761
|
+
id: `cmp-${++counter2}`,
|
|
1011
2762
|
name: input.name,
|
|
1012
2763
|
channelId: input.channelId,
|
|
1013
2764
|
status: input.status,
|
|
@@ -1036,6 +2787,434 @@ function createMockMarketingProvider(cfg) {
|
|
|
1036
2787
|
}
|
|
1037
2788
|
};
|
|
1038
2789
|
}
|
|
2790
|
+
|
|
2791
|
+
// src/data/contentMock.ts
|
|
2792
|
+
var counter = 0;
|
|
2793
|
+
var nextId = (prefix) => `${prefix}-${++counter}`;
|
|
2794
|
+
var SAMPLE_SCRIPT = `# 5 sinais de que sua queda capilar n\xE3o \xE9 normal
|
|
2795
|
+
|
|
2796
|
+
**TEMA:** 5 SINAIS DE QUE SUA QUEDA CAPILAR N\xC3O \xC9 NORMAL
|
|
2797
|
+
**Formato:** Reel \xB7 **Dura\xE7\xE3o:** 45 a 60 segundos
|
|
2798
|
+
|
|
2799
|
+
---
|
|
2800
|
+
|
|
2801
|
+
## TAKE 1 \u2013 GANCHO (0s a 5s)
|
|
2802
|
+
|
|
2803
|
+
*Dra. andando at\xE9 a c\xE2mera.*
|
|
2804
|
+
|
|
2805
|
+
**Texto na tela:** \u26A0\uFE0F 5 sinais de alerta
|
|
2806
|
+
|
|
2807
|
+
**Fala:** "Seu cabelo est\xE1 caindo e voc\xEA n\xE3o sabe se isso \xE9 normal? Existem sinais que merecem aten\xE7\xE3o."
|
|
2808
|
+
|
|
2809
|
+
---
|
|
2810
|
+
|
|
2811
|
+
## TAKE 2 \u2013 SINAL 1 (5s a 12s)
|
|
2812
|
+
|
|
2813
|
+
*Mostrar a Dra. segurando alguns fios de cabelo ou imagem ilustrativa.*
|
|
2814
|
+
|
|
2815
|
+
**Texto na tela:** Excesso de fios caindo.
|
|
2816
|
+
|
|
2817
|
+
**Fala:** "Primeiro sinal: voc\xEA percebe muitos fios no travesseiro, no banho ou ao pentear os cabelos."
|
|
2818
|
+
|
|
2819
|
+
---
|
|
2820
|
+
|
|
2821
|
+
## TAKE 3 \u2013 CTA (40s a 60s)
|
|
2822
|
+
|
|
2823
|
+
**Fala:** "Se voc\xEA se identificou com algum desses sinais, agende uma avalia\xE7\xE3o capilar."
|
|
2824
|
+
|
|
2825
|
+
**CTA:** Agende sua avalia\xE7\xE3o
|
|
2826
|
+
`;
|
|
2827
|
+
var WEEK_TITLES = [
|
|
2828
|
+
["5 sinais de que sua queda capilar n\xE3o \xE9 normal", "Antes e depois de tratamento capilar"],
|
|
2829
|
+
["O que \xE9 DHT?", "Tour pela cl\xEDnica"],
|
|
2830
|
+
["3 erros que aceleram a calv\xEDcie", "Depoimento de paciente"],
|
|
2831
|
+
["Como funciona uma avalia\xE7\xE3o capilar", "Mitos sobre queda de cabelo"],
|
|
2832
|
+
["Por que seu tratamento anterior n\xE3o funcionou?", "Ozonioterapia para gordura localizada"],
|
|
2833
|
+
["Diferen\xE7a entre dermatologista e tricologista", "Caso real de evolu\xE7\xE3o"],
|
|
2834
|
+
["Alopecia feminina explicada", "Bastidores da cl\xEDnica"],
|
|
2835
|
+
["Quando procurar ajuda para queda capilar", "Convite para avalia\xE7\xE3o gratuita"]
|
|
2836
|
+
];
|
|
2837
|
+
function buildSeed() {
|
|
2838
|
+
const account = {
|
|
2839
|
+
id: nextId("acc"),
|
|
2840
|
+
name: "Conta demo",
|
|
2841
|
+
handle: "@clinica.demo",
|
|
2842
|
+
platforms: ["instagram"],
|
|
2843
|
+
isActive: true
|
|
2844
|
+
};
|
|
2845
|
+
const plan = {
|
|
2846
|
+
id: nextId("plan"),
|
|
2847
|
+
accountId: account.id,
|
|
2848
|
+
name: "Plano de conte\xFAdo \u2014 8 semanas",
|
|
2849
|
+
status: "active",
|
|
2850
|
+
weeksCount: 8,
|
|
2851
|
+
objective: "Atrair pacientes qualificados para avalia\xE7\xE3o capilar",
|
|
2852
|
+
tone: "Autoridade acess\xEDvel \u2014 t\xE9cnica, mas em linguagem simples",
|
|
2853
|
+
pillars: ["Educa\xE7\xE3o", "Autoridade", "Prova social", "Bastidores", "Convers\xE3o"],
|
|
2854
|
+
formats: ["reel", "static"],
|
|
2855
|
+
weeklyFrequency: 3,
|
|
2856
|
+
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"
|
|
2857
|
+
};
|
|
2858
|
+
const posts = [];
|
|
2859
|
+
WEEK_TITLES.forEach(([reel1, reel2], i) => {
|
|
2860
|
+
const week = i + 1;
|
|
2861
|
+
posts.push(
|
|
2862
|
+
{
|
|
2863
|
+
id: nextId("post"),
|
|
2864
|
+
planId: plan.id,
|
|
2865
|
+
weekNumber: week,
|
|
2866
|
+
position: 0,
|
|
2867
|
+
title: reel1,
|
|
2868
|
+
format: "reel",
|
|
2869
|
+
status: week === 1 ? "script" : "idea",
|
|
2870
|
+
platforms: [],
|
|
2871
|
+
checklist: [],
|
|
2872
|
+
contentMd: week === 1 ? SAMPLE_SCRIPT : ""
|
|
2873
|
+
},
|
|
2874
|
+
{
|
|
2875
|
+
id: nextId("post"),
|
|
2876
|
+
planId: plan.id,
|
|
2877
|
+
weekNumber: week,
|
|
2878
|
+
position: 1,
|
|
2879
|
+
title: reel2,
|
|
2880
|
+
format: "reel",
|
|
2881
|
+
status: "idea",
|
|
2882
|
+
platforms: [],
|
|
2883
|
+
checklist: [],
|
|
2884
|
+
contentMd: ""
|
|
2885
|
+
},
|
|
2886
|
+
{
|
|
2887
|
+
id: nextId("post"),
|
|
2888
|
+
planId: plan.id,
|
|
2889
|
+
weekNumber: week,
|
|
2890
|
+
position: 2,
|
|
2891
|
+
title: "Est\xE1tico",
|
|
2892
|
+
format: "static",
|
|
2893
|
+
status: "idea",
|
|
2894
|
+
platforms: [],
|
|
2895
|
+
checklist: [],
|
|
2896
|
+
contentMd: ""
|
|
2897
|
+
}
|
|
2898
|
+
);
|
|
2899
|
+
});
|
|
2900
|
+
return { accounts: [account], plans: [plan], posts };
|
|
2901
|
+
}
|
|
2902
|
+
function createMockContentPlannerProvider() {
|
|
2903
|
+
const db = buildSeed();
|
|
2904
|
+
return {
|
|
2905
|
+
async listAccounts() {
|
|
2906
|
+
return [...db.accounts];
|
|
2907
|
+
},
|
|
2908
|
+
async saveAccount(input) {
|
|
2909
|
+
if (input.id) {
|
|
2910
|
+
const acc2 = db.accounts.find((a) => a.id === input.id);
|
|
2911
|
+
if (!acc2) throw new Error("Account not found");
|
|
2912
|
+
Object.assign(acc2, {
|
|
2913
|
+
name: input.name ?? acc2.name,
|
|
2914
|
+
handle: input.handle ?? acc2.handle,
|
|
2915
|
+
platforms: input.platforms ?? acc2.platforms,
|
|
2916
|
+
isActive: input.isActive ?? acc2.isActive
|
|
2917
|
+
});
|
|
2918
|
+
return { ...acc2 };
|
|
2919
|
+
}
|
|
2920
|
+
const acc = {
|
|
2921
|
+
id: nextId("acc"),
|
|
2922
|
+
name: input.name,
|
|
2923
|
+
handle: input.handle,
|
|
2924
|
+
platforms: input.platforms ?? ["instagram"],
|
|
2925
|
+
isActive: input.isActive ?? true
|
|
2926
|
+
};
|
|
2927
|
+
db.accounts.push(acc);
|
|
2928
|
+
return { ...acc };
|
|
2929
|
+
},
|
|
2930
|
+
async deleteAccount(id) {
|
|
2931
|
+
db.accounts = db.accounts.filter((a) => a.id !== id);
|
|
2932
|
+
const planIds = new Set(db.plans.filter((p) => p.accountId === id).map((p) => p.id));
|
|
2933
|
+
db.plans = db.plans.filter((p) => p.accountId !== id);
|
|
2934
|
+
db.posts = db.posts.filter((p) => !planIds.has(p.planId));
|
|
2935
|
+
},
|
|
2936
|
+
async listPlans(accountId) {
|
|
2937
|
+
return db.plans.filter((p) => p.accountId === accountId).map((p) => ({ ...p }));
|
|
2938
|
+
},
|
|
2939
|
+
async getPlan(id) {
|
|
2940
|
+
const plan = db.plans.find((p) => p.id === id);
|
|
2941
|
+
return plan ? { ...plan } : null;
|
|
2942
|
+
},
|
|
2943
|
+
async savePlan(input) {
|
|
2944
|
+
if (input.id) {
|
|
2945
|
+
const plan2 = db.plans.find((p) => p.id === input.id);
|
|
2946
|
+
if (!plan2) throw new Error("Plan not found");
|
|
2947
|
+
Object.assign(plan2, {
|
|
2948
|
+
...input.name !== void 0 && { name: input.name },
|
|
2949
|
+
...input.status !== void 0 && { status: input.status },
|
|
2950
|
+
...input.weeksCount !== void 0 && { weeksCount: input.weeksCount },
|
|
2951
|
+
...input.startDate !== void 0 && { startDate: input.startDate ?? void 0 },
|
|
2952
|
+
...input.objective !== void 0 && { objective: input.objective },
|
|
2953
|
+
...input.tone !== void 0 && { tone: input.tone },
|
|
2954
|
+
...input.pillars !== void 0 && { pillars: input.pillars },
|
|
2955
|
+
...input.formats !== void 0 && { formats: input.formats },
|
|
2956
|
+
...input.weeklyFrequency !== void 0 && { weeklyFrequency: input.weeklyFrequency },
|
|
2957
|
+
...input.briefMd !== void 0 && { briefMd: input.briefMd }
|
|
2958
|
+
});
|
|
2959
|
+
return { ...plan2 };
|
|
2960
|
+
}
|
|
2961
|
+
if (!input.accountId) throw new Error("accountId is required");
|
|
2962
|
+
const plan = {
|
|
2963
|
+
id: nextId("plan"),
|
|
2964
|
+
accountId: input.accountId,
|
|
2965
|
+
name: input.name ?? "Novo plano",
|
|
2966
|
+
status: input.status ?? "draft",
|
|
2967
|
+
weeksCount: input.weeksCount ?? 4,
|
|
2968
|
+
startDate: input.startDate ?? void 0,
|
|
2969
|
+
objective: input.objective,
|
|
2970
|
+
tone: input.tone,
|
|
2971
|
+
pillars: input.pillars ?? [],
|
|
2972
|
+
formats: input.formats ?? ["reel", "static"],
|
|
2973
|
+
weeklyFrequency: input.weeklyFrequency ?? 3,
|
|
2974
|
+
briefMd: input.briefMd ?? ""
|
|
2975
|
+
};
|
|
2976
|
+
db.plans.push(plan);
|
|
2977
|
+
return { ...plan };
|
|
2978
|
+
},
|
|
2979
|
+
async deletePlan(id) {
|
|
2980
|
+
db.plans = db.plans.filter((p) => p.id !== id);
|
|
2981
|
+
db.posts = db.posts.filter((p) => p.planId !== id);
|
|
2982
|
+
},
|
|
2983
|
+
async listPosts(planId) {
|
|
2984
|
+
return db.posts.filter((p) => p.planId === planId).sort((a, b) => a.weekNumber - b.weekNumber || a.position - b.position).map((p) => ({ ...p }));
|
|
2985
|
+
},
|
|
2986
|
+
async getPost(id) {
|
|
2987
|
+
const post = db.posts.find((p) => p.id === id);
|
|
2988
|
+
return post ? { ...post } : null;
|
|
2989
|
+
},
|
|
2990
|
+
async savePost(input) {
|
|
2991
|
+
if (input.id) {
|
|
2992
|
+
const post2 = db.posts.find((p) => p.id === input.id);
|
|
2993
|
+
if (!post2) throw new Error("Post not found");
|
|
2994
|
+
Object.assign(post2, {
|
|
2995
|
+
...input.weekNumber !== void 0 && { weekNumber: input.weekNumber },
|
|
2996
|
+
...input.position !== void 0 && { position: input.position },
|
|
2997
|
+
...input.title !== void 0 && { title: input.title },
|
|
2998
|
+
...input.format !== void 0 && { format: input.format },
|
|
2999
|
+
...input.status !== void 0 && { status: input.status },
|
|
3000
|
+
...input.scheduledDate !== void 0 && { scheduledDate: input.scheduledDate ?? void 0 },
|
|
3001
|
+
...input.platforms !== void 0 && { platforms: input.platforms },
|
|
3002
|
+
...input.checklist !== void 0 && { checklist: input.checklist },
|
|
3003
|
+
...input.mediaUrl !== void 0 && { mediaUrl: input.mediaUrl ?? void 0 },
|
|
3004
|
+
...input.hook !== void 0 && { hook: input.hook },
|
|
3005
|
+
...input.cta !== void 0 && { cta: input.cta },
|
|
3006
|
+
...input.contentMd !== void 0 && { contentMd: input.contentMd }
|
|
3007
|
+
});
|
|
3008
|
+
return { ...post2 };
|
|
3009
|
+
}
|
|
3010
|
+
if (!input.planId) throw new Error("planId is required");
|
|
3011
|
+
const week = input.weekNumber ?? 1;
|
|
3012
|
+
const siblings = db.posts.filter((p) => p.planId === input.planId && p.weekNumber === week);
|
|
3013
|
+
const post = {
|
|
3014
|
+
id: nextId("post"),
|
|
3015
|
+
planId: input.planId,
|
|
3016
|
+
weekNumber: week,
|
|
3017
|
+
position: input.position ?? siblings.length,
|
|
3018
|
+
title: input.title ?? "",
|
|
3019
|
+
format: input.format ?? "reel",
|
|
3020
|
+
status: input.status ?? "idea",
|
|
3021
|
+
scheduledDate: input.scheduledDate ?? void 0,
|
|
3022
|
+
platforms: input.platforms ?? [],
|
|
3023
|
+
checklist: input.checklist ?? [],
|
|
3024
|
+
mediaUrl: input.mediaUrl ?? void 0,
|
|
3025
|
+
hook: input.hook,
|
|
3026
|
+
cta: input.cta,
|
|
3027
|
+
contentMd: input.contentMd ?? ""
|
|
3028
|
+
};
|
|
3029
|
+
db.posts.push(post);
|
|
3030
|
+
return { ...post };
|
|
3031
|
+
},
|
|
3032
|
+
async deletePost(id) {
|
|
3033
|
+
db.posts = db.posts.filter((p) => p.id !== id);
|
|
3034
|
+
}
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// src/data/tables.ts
|
|
3039
|
+
var T = {
|
|
3040
|
+
socialAccounts: "plg_marketing_social_accounts",
|
|
3041
|
+
contentPlans: "plg_marketing_content_plans",
|
|
3042
|
+
contentPosts: "plg_marketing_content_posts"
|
|
3043
|
+
};
|
|
3044
|
+
|
|
3045
|
+
// src/data/contentSupabase.ts
|
|
3046
|
+
function getClient() {
|
|
3047
|
+
const supabase = core.getSupabaseClientOptional();
|
|
3048
|
+
if (!supabase) throw new Error("Supabase not initialized");
|
|
3049
|
+
return supabase;
|
|
3050
|
+
}
|
|
3051
|
+
function requireTenantId() {
|
|
3052
|
+
const tenantId = core.getActiveTenantId();
|
|
3053
|
+
if (!tenantId) throw new Error("No active tenant");
|
|
3054
|
+
return tenantId;
|
|
3055
|
+
}
|
|
3056
|
+
function snakeToCamel(obj) {
|
|
3057
|
+
const result = {};
|
|
3058
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
3059
|
+
result[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = value ?? void 0;
|
|
3060
|
+
}
|
|
3061
|
+
return result;
|
|
3062
|
+
}
|
|
3063
|
+
function toAccount(row) {
|
|
3064
|
+
const account = snakeToCamel(row);
|
|
3065
|
+
if (!Array.isArray(account.platforms)) account.platforms = [];
|
|
3066
|
+
return account;
|
|
3067
|
+
}
|
|
3068
|
+
function toPlan(row) {
|
|
3069
|
+
const plan = snakeToCamel(row);
|
|
3070
|
+
if (!Array.isArray(plan.pillars)) plan.pillars = [];
|
|
3071
|
+
if (!Array.isArray(plan.formats)) plan.formats = [];
|
|
3072
|
+
plan.briefMd = plan.briefMd ?? "";
|
|
3073
|
+
return plan;
|
|
3074
|
+
}
|
|
3075
|
+
function toPost(row) {
|
|
3076
|
+
const post = snakeToCamel(row);
|
|
3077
|
+
post.contentMd = post.contentMd ?? "";
|
|
3078
|
+
if (!Array.isArray(post.platforms)) post.platforms = [];
|
|
3079
|
+
if (!Array.isArray(post.checklist)) post.checklist = [];
|
|
3080
|
+
return post;
|
|
3081
|
+
}
|
|
3082
|
+
function createSupabaseContentPlannerProvider() {
|
|
3083
|
+
return {
|
|
3084
|
+
async listAccounts() {
|
|
3085
|
+
const tenantId = core.getActiveTenantId();
|
|
3086
|
+
if (!tenantId) return [];
|
|
3087
|
+
const { data } = await getClient().from(T.socialAccounts).select("*").eq("tenant_id", tenantId).eq("is_active", true).order("created_at", { ascending: true });
|
|
3088
|
+
return (data ?? []).map(toAccount);
|
|
3089
|
+
},
|
|
3090
|
+
async saveAccount(input) {
|
|
3091
|
+
if (input.id) {
|
|
3092
|
+
const payload = {};
|
|
3093
|
+
if (input.name !== void 0) payload.name = input.name;
|
|
3094
|
+
if (input.handle !== void 0) payload.handle = input.handle;
|
|
3095
|
+
if (input.platforms !== void 0) payload.platforms = input.platforms;
|
|
3096
|
+
if (input.isActive !== void 0) payload.is_active = input.isActive;
|
|
3097
|
+
const { data: data2, error: error2 } = await getClient().from(T.socialAccounts).update(payload).eq("id", input.id).select("*").single();
|
|
3098
|
+
if (error2) throw error2;
|
|
3099
|
+
return toAccount(data2);
|
|
3100
|
+
}
|
|
3101
|
+
const { data, error } = await getClient().from(T.socialAccounts).insert({
|
|
3102
|
+
tenant_id: requireTenantId(),
|
|
3103
|
+
name: input.name,
|
|
3104
|
+
handle: input.handle ?? null,
|
|
3105
|
+
platforms: input.platforms ?? ["instagram"],
|
|
3106
|
+
is_active: input.isActive ?? true
|
|
3107
|
+
}).select("*").single();
|
|
3108
|
+
if (error) throw error;
|
|
3109
|
+
return toAccount(data);
|
|
3110
|
+
},
|
|
3111
|
+
async deleteAccount(id) {
|
|
3112
|
+
const { error } = await getClient().from(T.socialAccounts).delete().eq("id", id);
|
|
3113
|
+
if (error) throw error;
|
|
3114
|
+
},
|
|
3115
|
+
async listPlans(accountId) {
|
|
3116
|
+
const tenantId = core.getActiveTenantId();
|
|
3117
|
+
if (!tenantId) return [];
|
|
3118
|
+
const { data } = await getClient().from(T.contentPlans).select("*").eq("tenant_id", tenantId).eq("account_id", accountId).order("created_at", { ascending: false });
|
|
3119
|
+
return (data ?? []).map(toPlan);
|
|
3120
|
+
},
|
|
3121
|
+
async getPlan(id) {
|
|
3122
|
+
const { data } = await getClient().from(T.contentPlans).select("*").eq("id", id).maybeSingle();
|
|
3123
|
+
return data ? toPlan(data) : null;
|
|
3124
|
+
},
|
|
3125
|
+
async savePlan(input) {
|
|
3126
|
+
if (input.id) {
|
|
3127
|
+
const payload = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3128
|
+
if (input.name !== void 0) payload.name = input.name;
|
|
3129
|
+
if (input.status !== void 0) payload.status = input.status;
|
|
3130
|
+
if (input.weeksCount !== void 0) payload.weeks_count = input.weeksCount;
|
|
3131
|
+
if (input.startDate !== void 0) payload.start_date = input.startDate;
|
|
3132
|
+
if (input.objective !== void 0) payload.objective = input.objective;
|
|
3133
|
+
if (input.tone !== void 0) payload.tone = input.tone;
|
|
3134
|
+
if (input.pillars !== void 0) payload.pillars = input.pillars;
|
|
3135
|
+
if (input.formats !== void 0) payload.formats = input.formats;
|
|
3136
|
+
if (input.weeklyFrequency !== void 0) payload.weekly_frequency = input.weeklyFrequency;
|
|
3137
|
+
if (input.briefMd !== void 0) payload.brief_md = input.briefMd;
|
|
3138
|
+
const { data: data2, error: error2 } = await getClient().from(T.contentPlans).update(payload).eq("id", input.id).select("*").single();
|
|
3139
|
+
if (error2) throw error2;
|
|
3140
|
+
return toPlan(data2);
|
|
3141
|
+
}
|
|
3142
|
+
if (!input.accountId) throw new Error("accountId is required");
|
|
3143
|
+
const { data, error } = await getClient().from(T.contentPlans).insert({
|
|
3144
|
+
tenant_id: requireTenantId(),
|
|
3145
|
+
account_id: input.accountId,
|
|
3146
|
+
name: input.name ?? "Novo plano",
|
|
3147
|
+
status: input.status ?? "draft",
|
|
3148
|
+
weeks_count: input.weeksCount ?? 4,
|
|
3149
|
+
start_date: input.startDate ?? null,
|
|
3150
|
+
objective: input.objective ?? null,
|
|
3151
|
+
tone: input.tone ?? null,
|
|
3152
|
+
pillars: input.pillars ?? [],
|
|
3153
|
+
formats: input.formats ?? ["reel", "static"],
|
|
3154
|
+
weekly_frequency: input.weeklyFrequency ?? 3,
|
|
3155
|
+
brief_md: input.briefMd ?? ""
|
|
3156
|
+
}).select("*").single();
|
|
3157
|
+
if (error) throw error;
|
|
3158
|
+
return toPlan(data);
|
|
3159
|
+
},
|
|
3160
|
+
async deletePlan(id) {
|
|
3161
|
+
await getClient().from(T.contentPlans).delete().eq("id", id);
|
|
3162
|
+
},
|
|
3163
|
+
async listPosts(planId) {
|
|
3164
|
+
const { data } = await getClient().from(T.contentPosts).select("*").eq("plan_id", planId).order("week_number", { ascending: true }).order("position", { ascending: true });
|
|
3165
|
+
return (data ?? []).map(toPost);
|
|
3166
|
+
},
|
|
3167
|
+
async getPost(id) {
|
|
3168
|
+
const { data } = await getClient().from(T.contentPosts).select("*").eq("id", id).maybeSingle();
|
|
3169
|
+
return data ? toPost(data) : null;
|
|
3170
|
+
},
|
|
3171
|
+
async savePost(input) {
|
|
3172
|
+
if (input.id) {
|
|
3173
|
+
const payload = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3174
|
+
if (input.weekNumber !== void 0) payload.week_number = input.weekNumber;
|
|
3175
|
+
if (input.position !== void 0) payload.position = input.position;
|
|
3176
|
+
if (input.title !== void 0) payload.title = input.title;
|
|
3177
|
+
if (input.format !== void 0) payload.format = input.format;
|
|
3178
|
+
if (input.status !== void 0) payload.status = input.status;
|
|
3179
|
+
if (input.scheduledDate !== void 0) payload.scheduled_date = input.scheduledDate;
|
|
3180
|
+
if (input.platforms !== void 0) payload.platforms = input.platforms;
|
|
3181
|
+
if (input.checklist !== void 0) payload.checklist = input.checklist;
|
|
3182
|
+
if (input.mediaUrl !== void 0) payload.media_url = input.mediaUrl;
|
|
3183
|
+
if (input.hook !== void 0) payload.hook = input.hook;
|
|
3184
|
+
if (input.cta !== void 0) payload.cta = input.cta;
|
|
3185
|
+
if (input.contentMd !== void 0) payload.content_md = input.contentMd;
|
|
3186
|
+
const { data: data2, error: error2 } = await getClient().from(T.contentPosts).update(payload).eq("id", input.id).select("*").single();
|
|
3187
|
+
if (error2) throw error2;
|
|
3188
|
+
return toPost(data2);
|
|
3189
|
+
}
|
|
3190
|
+
if (!input.planId) throw new Error("planId is required");
|
|
3191
|
+
const week = input.weekNumber ?? 1;
|
|
3192
|
+
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);
|
|
3193
|
+
const maxPos = maxData?.[0]?.position ?? -1;
|
|
3194
|
+
const { data, error } = await getClient().from(T.contentPosts).insert({
|
|
3195
|
+
tenant_id: requireTenantId(),
|
|
3196
|
+
plan_id: input.planId,
|
|
3197
|
+
week_number: week,
|
|
3198
|
+
position: input.position ?? maxPos + 1,
|
|
3199
|
+
title: input.title ?? "",
|
|
3200
|
+
format: input.format ?? "reel",
|
|
3201
|
+
status: input.status ?? "idea",
|
|
3202
|
+
scheduled_date: input.scheduledDate ?? null,
|
|
3203
|
+
platforms: input.platforms ?? [],
|
|
3204
|
+
checklist: input.checklist ?? [],
|
|
3205
|
+
media_url: input.mediaUrl ?? null,
|
|
3206
|
+
hook: input.hook ?? null,
|
|
3207
|
+
cta: input.cta ?? null,
|
|
3208
|
+
content_md: input.contentMd ?? ""
|
|
3209
|
+
}).select("*").single();
|
|
3210
|
+
if (error) throw error;
|
|
3211
|
+
return toPost(data);
|
|
3212
|
+
},
|
|
3213
|
+
async deletePost(id) {
|
|
3214
|
+
await getClient().from(T.contentPosts).delete().eq("id", id);
|
|
3215
|
+
}
|
|
3216
|
+
};
|
|
3217
|
+
}
|
|
1039
3218
|
function createMarketingStore(provider) {
|
|
1040
3219
|
return vanilla.createStore((set, get) => ({
|
|
1041
3220
|
range: "30d",
|
|
@@ -1072,6 +3251,280 @@ function createMarketingStore(provider) {
|
|
|
1072
3251
|
}
|
|
1073
3252
|
}));
|
|
1074
3253
|
}
|
|
3254
|
+
function createContentPlannerStore(provider) {
|
|
3255
|
+
async function loadPlansFor(accountId) {
|
|
3256
|
+
const plans = await provider.listPlans(accountId);
|
|
3257
|
+
const activePlan = plans.find((p) => p.status === "active") ?? plans[0] ?? null;
|
|
3258
|
+
const posts = activePlan ? await provider.listPosts(activePlan.id) : [];
|
|
3259
|
+
return { plans, activePlan, posts };
|
|
3260
|
+
}
|
|
3261
|
+
return vanilla.createStore((set, get) => ({
|
|
3262
|
+
loaded: false,
|
|
3263
|
+
loading: false,
|
|
3264
|
+
loadedTenantId: null,
|
|
3265
|
+
viewMode: "board",
|
|
3266
|
+
setViewMode(mode) {
|
|
3267
|
+
set({ viewMode: mode });
|
|
3268
|
+
},
|
|
3269
|
+
accounts: [],
|
|
3270
|
+
activeAccountId: null,
|
|
3271
|
+
plans: [],
|
|
3272
|
+
activePlan: null,
|
|
3273
|
+
posts: [],
|
|
3274
|
+
async load() {
|
|
3275
|
+
if (get().loading) return;
|
|
3276
|
+
set({ loading: true });
|
|
3277
|
+
try {
|
|
3278
|
+
const accounts = await provider.listAccounts();
|
|
3279
|
+
const current = get().activeAccountId;
|
|
3280
|
+
const activeAccountId = (current && accounts.some((a) => a.id === current) ? current : accounts[0]?.id) ?? null;
|
|
3281
|
+
const { plans, activePlan, posts } = activeAccountId ? await loadPlansFor(activeAccountId) : { plans: [], activePlan: null, posts: [] };
|
|
3282
|
+
set({
|
|
3283
|
+
accounts,
|
|
3284
|
+
activeAccountId,
|
|
3285
|
+
plans,
|
|
3286
|
+
activePlan,
|
|
3287
|
+
posts,
|
|
3288
|
+
loaded: true,
|
|
3289
|
+
loading: false,
|
|
3290
|
+
loadedTenantId: core.getActiveTenantId() ?? null
|
|
3291
|
+
});
|
|
3292
|
+
} catch (err) {
|
|
3293
|
+
set({ loading: false, loaded: true });
|
|
3294
|
+
throw err;
|
|
3295
|
+
}
|
|
3296
|
+
},
|
|
3297
|
+
async setAccount(accountId) {
|
|
3298
|
+
set({ activeAccountId: accountId, loading: true });
|
|
3299
|
+
try {
|
|
3300
|
+
const { plans, activePlan, posts } = await loadPlansFor(accountId);
|
|
3301
|
+
set({ plans, activePlan, posts, loading: false });
|
|
3302
|
+
} catch (err) {
|
|
3303
|
+
set({ loading: false });
|
|
3304
|
+
throw err;
|
|
3305
|
+
}
|
|
3306
|
+
},
|
|
3307
|
+
async selectPlan(planId) {
|
|
3308
|
+
const plan = get().plans.find((p) => p.id === planId) ?? null;
|
|
3309
|
+
const posts = plan ? await provider.listPosts(plan.id) : [];
|
|
3310
|
+
set({ activePlan: plan, posts });
|
|
3311
|
+
},
|
|
3312
|
+
async saveAccount(input) {
|
|
3313
|
+
const account = await provider.saveAccount(input);
|
|
3314
|
+
const accounts = await provider.listAccounts();
|
|
3315
|
+
set({ accounts });
|
|
3316
|
+
if (!get().activeAccountId) await get().setAccount(account.id);
|
|
3317
|
+
return account;
|
|
3318
|
+
},
|
|
3319
|
+
async deleteAccount(id) {
|
|
3320
|
+
await provider.deleteAccount(id);
|
|
3321
|
+
const accounts = await provider.listAccounts();
|
|
3322
|
+
const wasActive = get().activeAccountId === id;
|
|
3323
|
+
set({ accounts });
|
|
3324
|
+
if (wasActive) {
|
|
3325
|
+
const next = accounts[0]?.id ?? null;
|
|
3326
|
+
if (next) {
|
|
3327
|
+
await get().setAccount(next);
|
|
3328
|
+
} else {
|
|
3329
|
+
set({ activeAccountId: null, plans: [], activePlan: null, posts: [] });
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
},
|
|
3333
|
+
async savePlan(input) {
|
|
3334
|
+
const plan = await provider.savePlan(input);
|
|
3335
|
+
const accountId = get().activeAccountId;
|
|
3336
|
+
if (accountId) {
|
|
3337
|
+
const plans = await provider.listPlans(accountId);
|
|
3338
|
+
const activePlan = plans.find((p) => p.id === plan.id) ?? get().activePlan;
|
|
3339
|
+
const posts = activePlan ? await provider.listPosts(activePlan.id) : [];
|
|
3340
|
+
set({ plans, activePlan, posts });
|
|
3341
|
+
}
|
|
3342
|
+
return plan;
|
|
3343
|
+
},
|
|
3344
|
+
async savePost(input) {
|
|
3345
|
+
const post = await provider.savePost(input);
|
|
3346
|
+
const planId = get().activePlan?.id;
|
|
3347
|
+
if (planId && post.planId === planId) {
|
|
3348
|
+
const posts = await provider.listPosts(planId);
|
|
3349
|
+
set({ posts });
|
|
3350
|
+
}
|
|
3351
|
+
return post;
|
|
3352
|
+
},
|
|
3353
|
+
async deletePost(id) {
|
|
3354
|
+
await provider.deletePost(id);
|
|
3355
|
+
const planId = get().activePlan?.id;
|
|
3356
|
+
if (planId) {
|
|
3357
|
+
const posts = await provider.listPosts(planId);
|
|
3358
|
+
set({ posts });
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
}));
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
// src/migrations/index.ts
|
|
3365
|
+
var MIGRATION_000_PLG_RENAME = `-- 000_plg_rename.sql \u2014 rename legacy marketing tables to plg_marketing_* for pools
|
|
3366
|
+
-- provisioned before the industry-pool rename. Guarded: fires only when legacy
|
|
3367
|
+
-- name exists and target does not, so fresh pools skip every branch.
|
|
3368
|
+
DO $$
|
|
3369
|
+
BEGIN
|
|
3370
|
+
IF to_regclass('public.mkt_social_accounts') IS NOT NULL AND to_regclass('public.plg_marketing_social_accounts') IS NULL THEN
|
|
3371
|
+
ALTER TABLE public.mkt_social_accounts RENAME TO plg_marketing_social_accounts;
|
|
3372
|
+
END IF;
|
|
3373
|
+
IF to_regclass('public.mkt_content_plans') IS NOT NULL AND to_regclass('public.plg_marketing_content_plans') IS NULL THEN
|
|
3374
|
+
ALTER TABLE public.mkt_content_plans RENAME TO plg_marketing_content_plans;
|
|
3375
|
+
END IF;
|
|
3376
|
+
IF to_regclass('public.mkt_content_posts') IS NOT NULL AND to_regclass('public.plg_marketing_content_posts') IS NULL THEN
|
|
3377
|
+
ALTER TABLE public.mkt_content_posts RENAME TO plg_marketing_content_posts;
|
|
3378
|
+
END IF;
|
|
3379
|
+
END $$;
|
|
3380
|
+
`;
|
|
3381
|
+
var MIGRATION_001_CONTENT_PLANNER = `-- Marketing Plugin: Content Planner
|
|
3382
|
+
-- Prefix: plg_marketing_
|
|
3383
|
+
-- Social accounts + content plans + posts (Notion-style markdown pages).
|
|
3384
|
+
|
|
3385
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_social_accounts (
|
|
3386
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3387
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3388
|
+
name text NOT NULL,
|
|
3389
|
+
handle text,
|
|
3390
|
+
platform text NOT NULL DEFAULT 'instagram'
|
|
3391
|
+
CHECK (platform IN ('instagram', 'tiktok', 'youtube', 'facebook', 'linkedin')),
|
|
3392
|
+
is_active boolean NOT NULL DEFAULT true,
|
|
3393
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
3394
|
+
);
|
|
3395
|
+
ALTER TABLE public.plg_marketing_social_accounts ENABLE ROW LEVEL SECURITY;
|
|
3396
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_social_accounts_tenant ON public.plg_marketing_social_accounts(tenant_id);
|
|
3397
|
+
|
|
3398
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_content_plans (
|
|
3399
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3400
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3401
|
+
account_id uuid NOT NULL REFERENCES public.plg_marketing_social_accounts(id) ON DELETE CASCADE,
|
|
3402
|
+
name text NOT NULL,
|
|
3403
|
+
status text NOT NULL DEFAULT 'active'
|
|
3404
|
+
CHECK (status IN ('draft', 'active', 'archived')),
|
|
3405
|
+
weeks_count integer NOT NULL DEFAULT 8,
|
|
3406
|
+
start_date date,
|
|
3407
|
+
objective text,
|
|
3408
|
+
tone text,
|
|
3409
|
+
pillars jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
3410
|
+
formats jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
3411
|
+
weekly_frequency integer NOT NULL DEFAULT 3,
|
|
3412
|
+
brief_md text NOT NULL DEFAULT '',
|
|
3413
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3414
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
3415
|
+
);
|
|
3416
|
+
ALTER TABLE public.plg_marketing_content_plans ENABLE ROW LEVEL SECURITY;
|
|
3417
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_tenant ON public.plg_marketing_content_plans(tenant_id);
|
|
3418
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_account ON public.plg_marketing_content_plans(account_id);
|
|
3419
|
+
|
|
3420
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_content_posts (
|
|
3421
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3422
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3423
|
+
plan_id uuid NOT NULL REFERENCES public.plg_marketing_content_plans(id) ON DELETE CASCADE,
|
|
3424
|
+
week_number integer NOT NULL DEFAULT 1,
|
|
3425
|
+
position integer NOT NULL DEFAULT 0,
|
|
3426
|
+
title text NOT NULL,
|
|
3427
|
+
format text NOT NULL DEFAULT 'reel'
|
|
3428
|
+
CHECK (format IN ('reel', 'static', 'carousel', 'story')),
|
|
3429
|
+
status text NOT NULL DEFAULT 'idea'
|
|
3430
|
+
CHECK (status IN ('idea', 'script', 'recording', 'editing', 'scheduled', 'published')),
|
|
3431
|
+
scheduled_date date,
|
|
3432
|
+
hook text,
|
|
3433
|
+
cta text,
|
|
3434
|
+
content_md text NOT NULL DEFAULT '',
|
|
3435
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3436
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
3437
|
+
);
|
|
3438
|
+
ALTER TABLE public.plg_marketing_content_posts ENABLE ROW LEVEL SECURITY;
|
|
3439
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_tenant ON public.plg_marketing_content_posts(tenant_id);
|
|
3440
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_plan ON public.plg_marketing_content_posts(plan_id, week_number, position);
|
|
3441
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_status ON public.plg_marketing_content_posts(tenant_id, status);
|
|
3442
|
+
|
|
3443
|
+
-- RLS policies for plg_marketing_social_accounts
|
|
3444
|
+
DROP POLICY IF EXISTS plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts;
|
|
3445
|
+
DROP POLICY IF EXISTS plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts;
|
|
3446
|
+
DROP POLICY IF EXISTS plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts;
|
|
3447
|
+
DROP POLICY IF EXISTS plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts;
|
|
3448
|
+
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()));
|
|
3449
|
+
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()));
|
|
3450
|
+
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()));
|
|
3451
|
+
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()));
|
|
3452
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_social_accounts TO authenticated;
|
|
3453
|
+
|
|
3454
|
+
-- RLS policies for plg_marketing_content_plans
|
|
3455
|
+
DROP POLICY IF EXISTS plg_marketing_content_plans_select ON public.plg_marketing_content_plans;
|
|
3456
|
+
DROP POLICY IF EXISTS plg_marketing_content_plans_insert ON public.plg_marketing_content_plans;
|
|
3457
|
+
DROP POLICY IF EXISTS plg_marketing_content_plans_update ON public.plg_marketing_content_plans;
|
|
3458
|
+
DROP POLICY IF EXISTS plg_marketing_content_plans_delete ON public.plg_marketing_content_plans;
|
|
3459
|
+
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()));
|
|
3460
|
+
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()));
|
|
3461
|
+
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()));
|
|
3462
|
+
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()));
|
|
3463
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_plans TO authenticated;
|
|
3464
|
+
|
|
3465
|
+
-- RLS policies for plg_marketing_content_posts
|
|
3466
|
+
DROP POLICY IF EXISTS plg_marketing_content_posts_select ON public.plg_marketing_content_posts;
|
|
3467
|
+
DROP POLICY IF EXISTS plg_marketing_content_posts_insert ON public.plg_marketing_content_posts;
|
|
3468
|
+
DROP POLICY IF EXISTS plg_marketing_content_posts_update ON public.plg_marketing_content_posts;
|
|
3469
|
+
DROP POLICY IF EXISTS plg_marketing_content_posts_delete ON public.plg_marketing_content_posts;
|
|
3470
|
+
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()));
|
|
3471
|
+
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()));
|
|
3472
|
+
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()));
|
|
3473
|
+
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()));
|
|
3474
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_posts TO authenticated;
|
|
3475
|
+
`;
|
|
3476
|
+
var MIGRATION_002_MULTI_PLATFORM = `-- Marketing Plugin: multi-platform accounts + per-post targets.
|
|
3477
|
+
-- An account is a brand/profile that publishes to MANY platforms (Instagram,
|
|
3478
|
+
-- YouTube, TikTok, ...). Posts optionally narrow the target platforms; an
|
|
3479
|
+
-- empty array means "inherit the account's platforms".
|
|
3480
|
+
|
|
3481
|
+
ALTER TABLE public.plg_marketing_social_accounts
|
|
3482
|
+
ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{instagram}';
|
|
3483
|
+
|
|
3484
|
+
-- Backfill from the legacy single-platform column, then drop it (also drops
|
|
3485
|
+
-- its CHECK constraint \u2014 the platform list is UI-driven from here on).
|
|
3486
|
+
DO $$
|
|
3487
|
+
BEGIN
|
|
3488
|
+
IF EXISTS (
|
|
3489
|
+
SELECT 1 FROM information_schema.columns
|
|
3490
|
+
WHERE table_schema = 'public' AND table_name = 'plg_marketing_social_accounts' AND column_name = 'platform'
|
|
3491
|
+
) THEN
|
|
3492
|
+
UPDATE public.plg_marketing_social_accounts
|
|
3493
|
+
SET platforms = ARRAY[platform]
|
|
3494
|
+
WHERE platform IS NOT NULL AND platforms = '{instagram}';
|
|
3495
|
+
ALTER TABLE public.plg_marketing_social_accounts DROP COLUMN platform;
|
|
3496
|
+
END IF;
|
|
3497
|
+
END $$;
|
|
3498
|
+
|
|
3499
|
+
ALTER TABLE public.plg_marketing_content_posts
|
|
3500
|
+
ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{}';
|
|
3501
|
+
`;
|
|
3502
|
+
var MIGRATION_003_RECORDING_OPS = `-- Marketing Plugin: recording-day ops on posts.
|
|
3503
|
+
-- checklist: shooting checklist items [{ id, text, done }] \u2014 the on-set
|
|
3504
|
+
-- companion for recording day. media_url: uploaded asset (static posts get a
|
|
3505
|
+
-- caption + final art instead of a script).
|
|
3506
|
+
|
|
3507
|
+
ALTER TABLE public.plg_marketing_content_posts
|
|
3508
|
+
ADD COLUMN IF NOT EXISTS checklist jsonb NOT NULL DEFAULT '[]';
|
|
3509
|
+
|
|
3510
|
+
ALTER TABLE public.plg_marketing_content_posts
|
|
3511
|
+
ADD COLUMN IF NOT EXISTS media_url text;
|
|
3512
|
+
|
|
3513
|
+
-- Public media bucket for content assets (mirrors the avatars-bucket pattern:
|
|
3514
|
+
-- public read, authenticated write). Path convention: content/{tenantId}/{postId}-{ts}.{ext}
|
|
3515
|
+
INSERT INTO storage.buckets (id, name, public)
|
|
3516
|
+
VALUES ('mkt-media', 'mkt-media', true)
|
|
3517
|
+
ON CONFLICT (id) DO NOTHING;
|
|
3518
|
+
|
|
3519
|
+
DROP POLICY IF EXISTS plg_marketing_media_read ON storage.objects;
|
|
3520
|
+
DROP POLICY IF EXISTS plg_marketing_media_insert ON storage.objects;
|
|
3521
|
+
DROP POLICY IF EXISTS plg_marketing_media_update ON storage.objects;
|
|
3522
|
+
DROP POLICY IF EXISTS plg_marketing_media_delete ON storage.objects;
|
|
3523
|
+
CREATE POLICY plg_marketing_media_read ON storage.objects FOR SELECT TO public USING (bucket_id = 'mkt-media');
|
|
3524
|
+
CREATE POLICY plg_marketing_media_insert ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'mkt-media');
|
|
3525
|
+
CREATE POLICY plg_marketing_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
|
|
3526
|
+
CREATE POLICY plg_marketing_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');
|
|
3527
|
+
`;
|
|
1075
3528
|
|
|
1076
3529
|
// src/locales/en.ts
|
|
1077
3530
|
var en = {
|
|
@@ -1146,7 +3599,102 @@ var en = {
|
|
|
1146
3599
|
"marketing.settings.autoSyncDesc": "Pull conversions automatically from the source.",
|
|
1147
3600
|
"marketing.settings.assisted": "Count assisted conversions",
|
|
1148
3601
|
"marketing.settings.assistedDesc": "Credit channels that contributed to a conversion.",
|
|
1149
|
-
"marketing.settings.channelTrackDesc": "Track this acquisition source."
|
|
3602
|
+
"marketing.settings.channelTrackDesc": "Track this acquisition source.",
|
|
3603
|
+
// content planner
|
|
3604
|
+
"marketing.content.title": "Content",
|
|
3605
|
+
"marketing.content.week": "Week",
|
|
3606
|
+
"marketing.content.month": "Month",
|
|
3607
|
+
"marketing.content.newPost": "New post",
|
|
3608
|
+
"marketing.content.untitled": "Untitled",
|
|
3609
|
+
"marketing.content.account": "Account",
|
|
3610
|
+
"marketing.content.addAccount": "Add account",
|
|
3611
|
+
"marketing.content.previewNow": "Just now",
|
|
3612
|
+
"marketing.content.previewLike": "Like",
|
|
3613
|
+
"marketing.content.previewComment": "Comment",
|
|
3614
|
+
"marketing.content.previewShare": "Share",
|
|
3615
|
+
"marketing.content.showLess": "show less",
|
|
3616
|
+
"marketing.content.checklistTitle": "Recording checklist",
|
|
3617
|
+
"marketing.content.checklistDescription": "Shooting-day operations",
|
|
3618
|
+
"marketing.content.checklistOpen": "Checklist",
|
|
3619
|
+
"marketing.content.checklistEmpty": "No items yet \u2014 generate them from the script or add your own below.",
|
|
3620
|
+
"marketing.content.checklistAddPlaceholder": "New item\u2026",
|
|
3621
|
+
"marketing.content.checklistAdd": "Add item",
|
|
3622
|
+
"marketing.content.checklistGenerate": "Generate from script",
|
|
3623
|
+
"marketing.content.scriptLabel": "Script",
|
|
3624
|
+
"marketing.content.captionLabel": "Caption",
|
|
3625
|
+
"marketing.content.captionPlaceholder": "Write the post caption\u2026",
|
|
3626
|
+
"marketing.content.mediaLabel": "Media asset",
|
|
3627
|
+
"marketing.content.mediaUpload": "Upload media",
|
|
3628
|
+
"marketing.content.mediaHint": "Image or video for this post",
|
|
3629
|
+
"marketing.content.mediaReplace": "Replace",
|
|
3630
|
+
"marketing.content.mediaRemove": "Remove",
|
|
3631
|
+
"marketing.content.mediaUnavailable": "Media upload requires a connected Supabase project.",
|
|
3632
|
+
"marketing.content.editAccountTitle": "Account settings",
|
|
3633
|
+
"marketing.content.accountSettings": "Account settings",
|
|
3634
|
+
"marketing.content.deleteAccount": "Delete account",
|
|
3635
|
+
"marketing.content.deleteAccountConfirm": "The account and all of its content plans and posts will be permanently deleted.",
|
|
3636
|
+
"marketing.content.noAccountsAvailable": "No accounts available",
|
|
3637
|
+
"marketing.content.addAccountTitle": "New account",
|
|
3638
|
+
"marketing.content.nameLabel": "Name",
|
|
3639
|
+
"marketing.content.handleLabel": "Handle",
|
|
3640
|
+
"marketing.content.handlePlaceholder": "@handle (optional)",
|
|
3641
|
+
"marketing.content.viewBoard": "Weekly",
|
|
3642
|
+
"marketing.content.viewCalendar": "Monthly",
|
|
3643
|
+
"marketing.content.viewList": "List",
|
|
3644
|
+
"marketing.content.unscheduled": "Unscheduled",
|
|
3645
|
+
"marketing.content.noPosts": "No posts yet \u2014 add your first one from any week.",
|
|
3646
|
+
"marketing.content.listTitle": "Title",
|
|
3647
|
+
"marketing.content.listWeek": "Week",
|
|
3648
|
+
"marketing.content.listDate": "Date",
|
|
3649
|
+
"marketing.content.listFormat": "Format",
|
|
3650
|
+
"marketing.content.listStatus": "Status",
|
|
3651
|
+
"marketing.content.noAccountsHint": "Create the account (e.g. your Instagram profile) to start planning content.",
|
|
3652
|
+
"marketing.content.accountNamePlaceholder": "e.g. @myclinic",
|
|
3653
|
+
"marketing.content.createAccount": "Create account",
|
|
3654
|
+
"marketing.content.defaultPlanName": "Content plan \u2014 8 weeks",
|
|
3655
|
+
"marketing.content.brief": "Brief",
|
|
3656
|
+
"marketing.content.planName": "Plan name",
|
|
3657
|
+
"marketing.content.objective": "Objective",
|
|
3658
|
+
"marketing.content.objectivePlaceholder": "e.g. generate evaluation bookings",
|
|
3659
|
+
"marketing.content.tone": "Tone of voice",
|
|
3660
|
+
"marketing.content.tonePlaceholder": "e.g. approachable authority, plain language",
|
|
3661
|
+
"marketing.content.pillars": "Content pillars",
|
|
3662
|
+
"marketing.content.pillarPlaceholder": "Add pillar\u2026",
|
|
3663
|
+
"marketing.content.formatsLabel": "Formats",
|
|
3664
|
+
"marketing.content.weeklyFrequency": "Posts per week",
|
|
3665
|
+
"marketing.content.weeksCount": "Weeks",
|
|
3666
|
+
"marketing.content.briefBody": "Brief (markdown)",
|
|
3667
|
+
"marketing.content.briefPlaceholder": "Describe the strategy, references and what the content should achieve\u2026",
|
|
3668
|
+
"marketing.content.formatLabel": "Format",
|
|
3669
|
+
"marketing.content.statusLabel": "Status",
|
|
3670
|
+
"marketing.content.scheduledDate": "Planned date",
|
|
3671
|
+
"marketing.content.hook": "Hook",
|
|
3672
|
+
"marketing.content.hookPlaceholder": "The first line that grabs attention\u2026",
|
|
3673
|
+
"marketing.content.cta": "CTA",
|
|
3674
|
+
"marketing.content.ctaPlaceholder": "e.g. Book your evaluation",
|
|
3675
|
+
"marketing.content.titlePlaceholder": "Post title\u2026",
|
|
3676
|
+
"marketing.content.bodyPlaceholder": "Post script / plan in markdown\u2026",
|
|
3677
|
+
"marketing.content.insertTemplate": "Insert script template",
|
|
3678
|
+
"marketing.content.save": "Save",
|
|
3679
|
+
"marketing.content.cancel": "Cancel",
|
|
3680
|
+
"marketing.content.edit": "Edit",
|
|
3681
|
+
"marketing.content.preview": "Preview",
|
|
3682
|
+
"marketing.content.deletePost": "Delete post",
|
|
3683
|
+
"marketing.content.deletePostConfirm": "This post and its script will be permanently removed.",
|
|
3684
|
+
"marketing.content.format.reel": "Reel",
|
|
3685
|
+
"marketing.content.format.static": "Static",
|
|
3686
|
+
"marketing.content.format.carousel": "Carousel",
|
|
3687
|
+
"marketing.content.format.story": "Story",
|
|
3688
|
+
"marketing.content.format.video": "Video",
|
|
3689
|
+
"marketing.content.format.live": "Live",
|
|
3690
|
+
"marketing.content.format.article": "Article",
|
|
3691
|
+
"marketing.content.platformsLabel": "Platforms",
|
|
3692
|
+
"marketing.content.status.idea": "idea",
|
|
3693
|
+
"marketing.content.status.script": "script",
|
|
3694
|
+
"marketing.content.status.recording": "recording",
|
|
3695
|
+
"marketing.content.status.editing": "editing",
|
|
3696
|
+
"marketing.content.status.scheduled": "scheduled",
|
|
3697
|
+
"marketing.content.status.published": "published"
|
|
1150
3698
|
};
|
|
1151
3699
|
|
|
1152
3700
|
// src/locales/pt-BR.ts
|
|
@@ -1222,7 +3770,102 @@ var ptBR = {
|
|
|
1222
3770
|
"marketing.settings.autoSyncDesc": "Importar convers\xF5es automaticamente da fonte.",
|
|
1223
3771
|
"marketing.settings.assisted": "Contar convers\xF5es assistidas",
|
|
1224
3772
|
"marketing.settings.assistedDesc": "Creditar canais que contribu\xEDram para uma convers\xE3o.",
|
|
1225
|
-
"marketing.settings.channelTrackDesc": "Monitorar esta fonte de aquisi\xE7\xE3o."
|
|
3773
|
+
"marketing.settings.channelTrackDesc": "Monitorar esta fonte de aquisi\xE7\xE3o.",
|
|
3774
|
+
// content planner
|
|
3775
|
+
"marketing.content.title": "Conte\xFAdo",
|
|
3776
|
+
"marketing.content.week": "Semana",
|
|
3777
|
+
"marketing.content.month": "M\xEAs",
|
|
3778
|
+
"marketing.content.newPost": "Novo post",
|
|
3779
|
+
"marketing.content.untitled": "Sem t\xEDtulo",
|
|
3780
|
+
"marketing.content.account": "Conta",
|
|
3781
|
+
"marketing.content.addAccount": "Adicionar conta",
|
|
3782
|
+
"marketing.content.previewNow": "Agora",
|
|
3783
|
+
"marketing.content.previewLike": "Curtir",
|
|
3784
|
+
"marketing.content.previewComment": "Comentar",
|
|
3785
|
+
"marketing.content.previewShare": "Compartilhar",
|
|
3786
|
+
"marketing.content.showLess": "mostrar menos",
|
|
3787
|
+
"marketing.content.checklistTitle": "Checklist de grava\xE7\xE3o",
|
|
3788
|
+
"marketing.content.checklistDescription": "Opera\xE7\xE3o do dia de grava\xE7\xE3o",
|
|
3789
|
+
"marketing.content.checklistOpen": "Checklist",
|
|
3790
|
+
"marketing.content.checklistEmpty": "Nenhum item ainda \u2014 gere a partir do roteiro ou adicione o seu abaixo.",
|
|
3791
|
+
"marketing.content.checklistAddPlaceholder": "Novo item\u2026",
|
|
3792
|
+
"marketing.content.checklistAdd": "Adicionar item",
|
|
3793
|
+
"marketing.content.checklistGenerate": "Gerar do roteiro",
|
|
3794
|
+
"marketing.content.scriptLabel": "Roteiro",
|
|
3795
|
+
"marketing.content.captionLabel": "Legenda",
|
|
3796
|
+
"marketing.content.captionPlaceholder": "Escreva a legenda do post\u2026",
|
|
3797
|
+
"marketing.content.mediaLabel": "Arte / m\xEDdia",
|
|
3798
|
+
"marketing.content.mediaUpload": "Enviar m\xEDdia",
|
|
3799
|
+
"marketing.content.mediaHint": "Imagem ou v\xEDdeo deste post",
|
|
3800
|
+
"marketing.content.mediaReplace": "Substituir",
|
|
3801
|
+
"marketing.content.mediaRemove": "Remover",
|
|
3802
|
+
"marketing.content.mediaUnavailable": "O upload de m\xEDdia requer um projeto Supabase conectado.",
|
|
3803
|
+
"marketing.content.editAccountTitle": "Configura\xE7\xF5es da conta",
|
|
3804
|
+
"marketing.content.accountSettings": "Configura\xE7\xF5es da conta",
|
|
3805
|
+
"marketing.content.deleteAccount": "Excluir conta",
|
|
3806
|
+
"marketing.content.deleteAccountConfirm": "A conta e todos os planos e posts de conte\xFAdo dela ser\xE3o exclu\xEDdos permanentemente.",
|
|
3807
|
+
"marketing.content.noAccountsAvailable": "Nenhuma conta dispon\xEDvel",
|
|
3808
|
+
"marketing.content.addAccountTitle": "Nova conta",
|
|
3809
|
+
"marketing.content.nameLabel": "Nome",
|
|
3810
|
+
"marketing.content.handleLabel": "Usu\xE1rio",
|
|
3811
|
+
"marketing.content.handlePlaceholder": "@usuario (opcional)",
|
|
3812
|
+
"marketing.content.viewBoard": "Semanal",
|
|
3813
|
+
"marketing.content.viewCalendar": "Mensal",
|
|
3814
|
+
"marketing.content.viewList": "Lista",
|
|
3815
|
+
"marketing.content.unscheduled": "Sem data",
|
|
3816
|
+
"marketing.content.noPosts": "Nenhum post ainda \u2014 adicione o primeiro em qualquer semana.",
|
|
3817
|
+
"marketing.content.listTitle": "T\xEDtulo",
|
|
3818
|
+
"marketing.content.listWeek": "Semana",
|
|
3819
|
+
"marketing.content.listDate": "Data",
|
|
3820
|
+
"marketing.content.listFormat": "Formato",
|
|
3821
|
+
"marketing.content.listStatus": "Status",
|
|
3822
|
+
"marketing.content.noAccountsHint": "Crie a conta (ex.: o perfil do Instagram) para come\xE7ar a planejar o conte\xFAdo.",
|
|
3823
|
+
"marketing.content.accountNamePlaceholder": "ex.: @minhaclinica",
|
|
3824
|
+
"marketing.content.createAccount": "Criar conta",
|
|
3825
|
+
"marketing.content.defaultPlanName": "Plano de conte\xFAdo \u2014 8 semanas",
|
|
3826
|
+
"marketing.content.brief": "Briefing",
|
|
3827
|
+
"marketing.content.planName": "Nome do plano",
|
|
3828
|
+
"marketing.content.objective": "Objetivo",
|
|
3829
|
+
"marketing.content.objectivePlaceholder": "ex.: gerar agendamentos de avalia\xE7\xE3o",
|
|
3830
|
+
"marketing.content.tone": "Tom de voz",
|
|
3831
|
+
"marketing.content.tonePlaceholder": "ex.: autoridade acess\xEDvel, linguagem simples",
|
|
3832
|
+
"marketing.content.pillars": "Pilares de conte\xFAdo",
|
|
3833
|
+
"marketing.content.pillarPlaceholder": "Adicionar pilar\u2026",
|
|
3834
|
+
"marketing.content.formatsLabel": "Formatos",
|
|
3835
|
+
"marketing.content.weeklyFrequency": "Posts por semana",
|
|
3836
|
+
"marketing.content.weeksCount": "Semanas",
|
|
3837
|
+
"marketing.content.briefBody": "Briefing (markdown)",
|
|
3838
|
+
"marketing.content.briefPlaceholder": "Descreva a estrat\xE9gia, refer\xEAncias e o que se quer com o conte\xFAdo\u2026",
|
|
3839
|
+
"marketing.content.formatLabel": "Formato",
|
|
3840
|
+
"marketing.content.statusLabel": "Status",
|
|
3841
|
+
"marketing.content.scheduledDate": "Data prevista",
|
|
3842
|
+
"marketing.content.hook": "Gancho",
|
|
3843
|
+
"marketing.content.hookPlaceholder": "A primeira frase que segura a aten\xE7\xE3o\u2026",
|
|
3844
|
+
"marketing.content.cta": "CTA",
|
|
3845
|
+
"marketing.content.ctaPlaceholder": "ex.: Agende sua avalia\xE7\xE3o",
|
|
3846
|
+
"marketing.content.titlePlaceholder": "T\xEDtulo do post\u2026",
|
|
3847
|
+
"marketing.content.bodyPlaceholder": "Roteiro / plano do post em markdown\u2026",
|
|
3848
|
+
"marketing.content.insertTemplate": "Inserir modelo de roteiro",
|
|
3849
|
+
"marketing.content.save": "Salvar",
|
|
3850
|
+
"marketing.content.cancel": "Cancelar",
|
|
3851
|
+
"marketing.content.edit": "Editar",
|
|
3852
|
+
"marketing.content.preview": "Visualizar",
|
|
3853
|
+
"marketing.content.deletePost": "Excluir post",
|
|
3854
|
+
"marketing.content.deletePostConfirm": "Este post e seu roteiro ser\xE3o removidos permanentemente.",
|
|
3855
|
+
"marketing.content.format.reel": "Reel",
|
|
3856
|
+
"marketing.content.format.static": "Est\xE1tico",
|
|
3857
|
+
"marketing.content.format.carousel": "Carrossel",
|
|
3858
|
+
"marketing.content.format.story": "Story",
|
|
3859
|
+
"marketing.content.format.video": "V\xEDdeo",
|
|
3860
|
+
"marketing.content.format.live": "Live",
|
|
3861
|
+
"marketing.content.format.article": "Artigo",
|
|
3862
|
+
"marketing.content.platformsLabel": "Plataformas",
|
|
3863
|
+
"marketing.content.status.idea": "ideia",
|
|
3864
|
+
"marketing.content.status.script": "roteiro",
|
|
3865
|
+
"marketing.content.status.recording": "grava\xE7\xE3o",
|
|
3866
|
+
"marketing.content.status.editing": "edi\xE7\xE3o",
|
|
3867
|
+
"marketing.content.status.scheduled": "agendado",
|
|
3868
|
+
"marketing.content.status.published": "publicado"
|
|
1226
3869
|
};
|
|
1227
3870
|
|
|
1228
3871
|
// src/locales/index.ts
|
|
@@ -1240,6 +3883,7 @@ var DEFAULT_LABELS = {
|
|
|
1240
3883
|
campaigns: "Campaigns",
|
|
1241
3884
|
funnel: "Funnel",
|
|
1242
3885
|
landingPages: "Landing pages",
|
|
3886
|
+
content: "Content",
|
|
1243
3887
|
settings: "Settings"
|
|
1244
3888
|
};
|
|
1245
3889
|
function resolveConfig(options) {
|
|
@@ -1264,23 +3908,28 @@ function createMarketingPlugin(options) {
|
|
|
1264
3908
|
const provider = options?.dataProvider ?? createSafeProvider(config, domain);
|
|
1265
3909
|
const store = createMarketingStore(provider);
|
|
1266
3910
|
const dashboardWidgets = createMarketingDashboardWidgets({ config, provider, store });
|
|
1267
|
-
const
|
|
3911
|
+
const contentProvider = options?.contentPlanner?.dataProvider ?? core.createSafeDataProvider(
|
|
3912
|
+
() => createSupabaseContentPlannerProvider(),
|
|
3913
|
+
() => createMockContentPlannerProvider()
|
|
3914
|
+
);
|
|
3915
|
+
const contentStore = createContentPlannerStore(contentProvider);
|
|
3916
|
+
const PageComponent = () => React8__default.default.createElement(
|
|
1268
3917
|
MarketingContextProvider,
|
|
1269
3918
|
{ config, provider, store },
|
|
1270
|
-
|
|
3919
|
+
React8__default.default.createElement(MarketingPage, { config, provider, store, contentProvider, contentStore })
|
|
1271
3920
|
);
|
|
1272
3921
|
PageComponent.displayName = "MarketingPage";
|
|
1273
3922
|
const marketingRegistries = [
|
|
1274
3923
|
...buildMarketingRegistries(config.channels),
|
|
1275
3924
|
...options?.settingsRegistries ?? []
|
|
1276
3925
|
];
|
|
1277
|
-
const SettingsComponent = () =>
|
|
3926
|
+
const SettingsComponent = () => React8__default.default.createElement(
|
|
1278
3927
|
MarketingContextProvider,
|
|
1279
3928
|
{ config, provider, store },
|
|
1280
|
-
|
|
3929
|
+
React8__default.default.createElement(saas.PluginSettingsPanel, {
|
|
1281
3930
|
title: config.labels.settings ?? config.labels.pageTitle,
|
|
1282
3931
|
subtitle: config.labels.pageSubtitle,
|
|
1283
|
-
generalSettings:
|
|
3932
|
+
generalSettings: React8__default.default.createElement(SettingsView),
|
|
1284
3933
|
registries: marketingRegistries,
|
|
1285
3934
|
routeBase: "/settings/marketing"
|
|
1286
3935
|
})
|
|
@@ -1293,11 +3942,15 @@ function createMarketingPlugin(options) {
|
|
|
1293
3942
|
version: "1.0.0",
|
|
1294
3943
|
scope: options?.scope ?? "universal",
|
|
1295
3944
|
verticalId: options?.verticalId,
|
|
3945
|
+
// Admin content-planning surface (campaigns, content calendar) — targets the
|
|
3946
|
+
// saas/admin Panel world, not the ecommerce storefront/operator console.
|
|
3947
|
+
scaffolds: ["saas"],
|
|
1296
3948
|
defaultEnabled: true,
|
|
1297
3949
|
dependencies: [],
|
|
1298
3950
|
declaredFeatures: [
|
|
1299
3951
|
{ id: "marketing", label: config.labels.pageTitle, group: "Engage" },
|
|
1300
|
-
...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages, group: "Engage" }] : []
|
|
3952
|
+
...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages, group: "Engage" }] : [],
|
|
3953
|
+
...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : []
|
|
1301
3954
|
],
|
|
1302
3955
|
navigation: [
|
|
1303
3956
|
{
|
|
@@ -1322,7 +3975,36 @@ function createMarketingPlugin(options) {
|
|
|
1322
3975
|
{ name: "marketing.conversion.tracked", description: "A conversion was attributed to a channel/campaign" },
|
|
1323
3976
|
{ name: "marketing.campaign.created", description: "A campaign was created" },
|
|
1324
3977
|
{ name: "marketing.campaign.updated", description: "A campaign was updated" },
|
|
1325
|
-
{ name: "marketing.channel.synced", description: "A channel pulled fresh attribution data" }
|
|
3978
|
+
{ name: "marketing.channel.synced", description: "A channel pulled fresh attribution data" },
|
|
3979
|
+
{ name: "marketing.content.plan.updated", description: "A content plan (brief/config) was updated" },
|
|
3980
|
+
{ name: "marketing.content.post.created", description: "A content post was created" },
|
|
3981
|
+
{ name: "marketing.content.post.updated", description: "A content post was updated" }
|
|
3982
|
+
],
|
|
3983
|
+
migrations: [
|
|
3984
|
+
{
|
|
3985
|
+
id: "marketing-000-plg-rename",
|
|
3986
|
+
version: "1.0.0",
|
|
3987
|
+
sql: MIGRATION_000_PLG_RENAME,
|
|
3988
|
+
description: "Rename legacy mkt_* tables to plg_marketing_* in-place for pools provisioned before the industry-pool rename (guarded no-op on fresh pools)"
|
|
3989
|
+
},
|
|
3990
|
+
{
|
|
3991
|
+
id: "marketing-001-content-planner",
|
|
3992
|
+
version: "1.0.0",
|
|
3993
|
+
sql: MIGRATION_001_CONTENT_PLANNER,
|
|
3994
|
+
description: "Create plg_marketing_social_accounts, plg_marketing_content_plans and plg_marketing_content_posts"
|
|
3995
|
+
},
|
|
3996
|
+
{
|
|
3997
|
+
id: "marketing-002-multi-platform",
|
|
3998
|
+
version: "1.1.0",
|
|
3999
|
+
sql: MIGRATION_002_MULTI_PLATFORM,
|
|
4000
|
+
description: "Accounts publish to many platforms (platforms[]); posts gain optional platform targets"
|
|
4001
|
+
},
|
|
4002
|
+
{
|
|
4003
|
+
id: "marketing-003-recording-ops",
|
|
4004
|
+
version: "1.2.0",
|
|
4005
|
+
sql: MIGRATION_003_RECORDING_OPS,
|
|
4006
|
+
description: "Recording-day checklist + media asset URL on posts; content-media storage bucket"
|
|
4007
|
+
}
|
|
1326
4008
|
],
|
|
1327
4009
|
aiTools: [
|
|
1328
4010
|
{
|
|
@@ -1396,6 +4078,8 @@ function createMarketingPlugin(options) {
|
|
|
1396
4078
|
|
|
1397
4079
|
exports.MARKETING_PRESETS = MARKETING_PRESETS;
|
|
1398
4080
|
exports.createMarketingPlugin = createMarketingPlugin;
|
|
4081
|
+
exports.createMockContentPlannerProvider = createMockContentPlannerProvider;
|
|
1399
4082
|
exports.createMockMarketingProvider = createMockMarketingProvider;
|
|
4083
|
+
exports.createSupabaseContentPlannerProvider = createSupabaseContentPlannerProvider;
|
|
1400
4084
|
//# sourceMappingURL=index.cjs.map
|
|
1401
4085
|
//# sourceMappingURL=index.cjs.map
|