@fayz-ai/plugin-marketing 0.1.6 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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/index.cjs +2780 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2782 -134
- 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 +4 -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 +11 -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 +262 -0
- package/src/data/contentTypes.ts +138 -0
- package/src/index.ts +62 -4
- package/src/locales/en.ts +95 -0
- package/src/locales/pt-BR.ts +95 -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 +152 -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.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { registerTranslations, getSupabaseClientOptional, useTranslation } from '@fayz-ai/core';
|
|
3
|
-
import { createPluginContext, PluginSettingsPanel, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, SelectRow, ToggleRow } from '@fayz-ai/saas';
|
|
4
|
-
import { defineKpiWidget, defineCustomWidget, defineTableWidget, ModulePage, DataTable, SubpageHeader, Button, DashboardCanvas, SegmentedControl, cn, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody,
|
|
5
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
6
|
-
import { ChevronRight, Users, Target, Percent, DollarSign, Trash2, Plus, MousePointerClick, Layers, ArrowUpRight, ArrowDownRight, Bike, Utensils, DoorOpen, MessageCircle, Instagram, Mail, Globe, Megaphone, Search, Link2, Trophy } from 'lucide-react';
|
|
1
|
+
import React8, { useEffect } from 'react';
|
|
2
|
+
import { getActiveTenantId, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation } from '@fayz-ai/core';
|
|
3
|
+
import { createPluginContext, PluginSettingsPanel, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, SelectRow, ToggleRow, useOrganizationStore } from '@fayz-ai/saas';
|
|
4
|
+
import { defineKpiWidget, defineCustomWidget, defineTableWidget, ModulePage, DataTable, SubpageHeader, Button, Skeleton, ConfirmDialog, ContentSplit, ContentSplitTrigger, Input, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MarkdownEditor, DashboardCanvas, SegmentedControl, cn, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, SelectSeparator, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, Checkbox, KpiCard as KpiCard$1, Card, CardHeader, CardTitle, CardContent, TableWidget } from '@fayz-ai/ui';
|
|
5
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
|
+
import { ChevronRight, Users, Target, Percent, DollarSign, Trash2, Plus, MousePointerClick, Layers, NotebookPen, ArrowUpRight, ArrowDownRight, Settings2, Columns3, CalendarDays, List, ChevronLeft, Loader2, ImagePlus, X, Sparkles, Bike, Utensils, DoorOpen, MessageCircle, Instagram, Mail, Globe, Megaphone, Search, Link2, AtSign, Pin, Twitter, Linkedin, Facebook, Youtube, Music2, Repeat2, Heart, BarChart2, Share2, ThumbsUp, Send, MoreHorizontal, Bookmark, GripVertical, Image, Trophy } from 'lucide-react';
|
|
7
7
|
import { createStore } from 'zustand/vanilla';
|
|
8
8
|
|
|
9
9
|
// src/index.ts
|
|
@@ -14,7 +14,7 @@ var useMarketingProvider = ctx.useProvider;
|
|
|
14
14
|
var useMarketingStore = ctx.useStore;
|
|
15
15
|
function useChannelLookup() {
|
|
16
16
|
const { channels } = useMarketingConfig();
|
|
17
|
-
return
|
|
17
|
+
return React8.useMemo(() => new Map(channels.map((c) => [c.id, c])), [channels]);
|
|
18
18
|
}
|
|
19
19
|
function OverviewView() {
|
|
20
20
|
const setRange = useMarketingStore((s) => s.setRange);
|
|
@@ -76,6 +76,20 @@ function ChannelIcon({ name, className }) {
|
|
|
76
76
|
const Icon = ICONS[name] ?? Link2;
|
|
77
77
|
return /* @__PURE__ */ jsx(Icon, { className });
|
|
78
78
|
}
|
|
79
|
+
var PLATFORM_ICONS = {
|
|
80
|
+
instagram: Instagram,
|
|
81
|
+
tiktok: Music2,
|
|
82
|
+
youtube: Youtube,
|
|
83
|
+
facebook: Facebook,
|
|
84
|
+
linkedin: Linkedin,
|
|
85
|
+
x: Twitter,
|
|
86
|
+
pinterest: Pin,
|
|
87
|
+
threads: AtSign
|
|
88
|
+
};
|
|
89
|
+
function PlatformIcon({ platform, className }) {
|
|
90
|
+
const Icon = PLATFORM_ICONS[platform] ?? Link2;
|
|
91
|
+
return /* @__PURE__ */ jsx(Icon, { className });
|
|
92
|
+
}
|
|
79
93
|
function KpiCard({
|
|
80
94
|
icon: Icon,
|
|
81
95
|
label,
|
|
@@ -160,7 +174,7 @@ function ChannelsView({ onOpen }) {
|
|
|
160
174
|
const { conversion, currency } = useMarketingConfig();
|
|
161
175
|
const channels = useMarketingStore((s) => s.channels);
|
|
162
176
|
const sorted = [...channels].sort((a, b) => b.conversions - a.conversions);
|
|
163
|
-
const columns =
|
|
177
|
+
const columns = React8.useMemo(() => [
|
|
164
178
|
{ accessorKey: "channelId", header: t("marketing.col.channel"), cell: ({ getValue }) => /* @__PURE__ */ jsx(ChannelCell, { channelId: getValue() }) },
|
|
165
179
|
{ accessorKey: "reach", header: t("marketing.col.reach"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
166
180
|
{ accessorKey: "leads", header: t("marketing.col.leads"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
@@ -200,13 +214,13 @@ function ChannelDetailView({ channelId, onBack }) {
|
|
|
200
214
|
const channels = useMarketingStore((s) => s.channels);
|
|
201
215
|
const lookup = useChannelLookup();
|
|
202
216
|
const provider = useMarketingProvider();
|
|
203
|
-
const [campaigns, setCampaigns] =
|
|
204
|
-
|
|
217
|
+
const [campaigns, setCampaigns] = React8.useState([]);
|
|
218
|
+
React8.useEffect(() => {
|
|
205
219
|
void provider.listCampaigns({ range, channelId }).then(setCampaigns);
|
|
206
220
|
}, [provider, range, channelId]);
|
|
207
221
|
const perf = channels.find((c) => c.channelId === channelId);
|
|
208
222
|
const channel = lookup.get(channelId);
|
|
209
|
-
const columns =
|
|
223
|
+
const columns = React8.useMemo(() => [
|
|
210
224
|
{ accessorKey: "name", header: t("marketing.col.campaign"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
|
|
211
225
|
{ accessorKey: "conversions", header: conversion.labelPlural, cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
212
226
|
{ id: "cvr", accessorFn: (c) => c.reach > 0 ? c.conversions / c.reach * 100 : 0, header: t("marketing.col.cvr"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatPercent(getValue()) }) },
|
|
@@ -243,12 +257,12 @@ function CampaignComposer({ open, onClose }) {
|
|
|
243
257
|
const t = useTranslation();
|
|
244
258
|
const { channels } = useMarketingConfig();
|
|
245
259
|
const saveCampaign = useMarketingStore((s) => s.saveCampaign);
|
|
246
|
-
const [name, setName] =
|
|
247
|
-
const [channelId, setChannelId] =
|
|
248
|
-
const [status, setStatus] =
|
|
249
|
-
const [spend, setSpend] =
|
|
250
|
-
const [saving, setSaving] =
|
|
251
|
-
|
|
260
|
+
const [name, setName] = React8.useState("");
|
|
261
|
+
const [channelId, setChannelId] = React8.useState(channels[0]?.id ?? "");
|
|
262
|
+
const [status, setStatus] = React8.useState("active");
|
|
263
|
+
const [spend, setSpend] = React8.useState("");
|
|
264
|
+
const [saving, setSaving] = React8.useState(false);
|
|
265
|
+
React8.useEffect(() => {
|
|
252
266
|
if (open) {
|
|
253
267
|
setName("");
|
|
254
268
|
setChannelId(channels[0]?.id ?? "");
|
|
@@ -310,8 +324,8 @@ function CampaignsView() {
|
|
|
310
324
|
const { conversion, currency } = useMarketingConfig();
|
|
311
325
|
const campaigns = useMarketingStore((s) => s.campaigns);
|
|
312
326
|
const deleteCampaign = useMarketingStore((s) => s.deleteCampaign);
|
|
313
|
-
const [composerOpen, setComposerOpen] =
|
|
314
|
-
const columns =
|
|
327
|
+
const [composerOpen, setComposerOpen] = React8.useState(false);
|
|
328
|
+
const columns = React8.useMemo(() => [
|
|
315
329
|
{ accessorKey: "name", header: t("marketing.col.campaign"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
|
|
316
330
|
{ accessorKey: "channelId", header: t("marketing.col.channel"), cell: ({ getValue }) => /* @__PURE__ */ jsx(ChannelCell, { channelId: getValue() }) },
|
|
317
331
|
{ accessorKey: "reach", header: t("marketing.col.reach"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
@@ -339,102 +353,1824 @@ function CampaignsView() {
|
|
|
339
353
|
}
|
|
340
354
|
)
|
|
341
355
|
}
|
|
342
|
-
], [t, conversion.labelPlural, currency, deleteCampaign]);
|
|
343
|
-
return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
|
|
344
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
345
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.campaigns.subtitle") }),
|
|
346
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
347
|
-
/* @__PURE__ */ jsx(RangeTabs, {}),
|
|
348
|
-
/* @__PURE__ */ jsxs(Button, { onClick: () => setComposerOpen(true), children: [
|
|
349
|
-
/* @__PURE__ */ jsx(Plus, { className: "mr-1.5 h-4 w-4" }),
|
|
350
|
-
" ",
|
|
351
|
-
t("marketing.campaigns.new")
|
|
356
|
+
], [t, conversion.labelPlural, currency, deleteCampaign]);
|
|
357
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
|
|
358
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
359
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.campaigns.subtitle") }),
|
|
360
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
361
|
+
/* @__PURE__ */ jsx(RangeTabs, {}),
|
|
362
|
+
/* @__PURE__ */ jsxs(Button, { onClick: () => setComposerOpen(true), children: [
|
|
363
|
+
/* @__PURE__ */ jsx(Plus, { className: "mr-1.5 h-4 w-4" }),
|
|
364
|
+
" ",
|
|
365
|
+
t("marketing.campaigns.new")
|
|
366
|
+
] })
|
|
367
|
+
] })
|
|
368
|
+
] }),
|
|
369
|
+
/* @__PURE__ */ jsx(DataTable, { columns, data: campaigns, emptyMessage: t("marketing.campaigns.empty") }),
|
|
370
|
+
/* @__PURE__ */ jsx(CampaignComposer, { open: composerOpen, onClose: () => setComposerOpen(false) })
|
|
371
|
+
] });
|
|
372
|
+
}
|
|
373
|
+
function FunnelView() {
|
|
374
|
+
const t = useTranslation();
|
|
375
|
+
const { conversion } = useMarketingConfig();
|
|
376
|
+
const funnel = useMarketingStore((s) => s.funnel);
|
|
377
|
+
const max = Math.max(...funnel.map((s) => s.count), 1);
|
|
378
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-4xl space-y-4", children: [
|
|
379
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
380
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
381
|
+
t("marketing.funnel.subtitlePrefix"),
|
|
382
|
+
" ",
|
|
383
|
+
conversion.label.toLowerCase()
|
|
384
|
+
] }),
|
|
385
|
+
/* @__PURE__ */ jsx(RangeTabs, {})
|
|
386
|
+
] }),
|
|
387
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-card border border-border bg-card p-5", children: [
|
|
388
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-3", children: funnel.map((stage, i) => {
|
|
389
|
+
const pct = max > 0 ? Math.max(stage.count / max * 100, 4) : 0;
|
|
390
|
+
const prev = funnel[i - 1];
|
|
391
|
+
const stepRate = prev && prev.count > 0 ? stage.count / prev.count * 100 : null;
|
|
392
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
393
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center justify-between text-sm", children: [
|
|
394
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: stage.label }),
|
|
395
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
396
|
+
formatNumber(stage.count),
|
|
397
|
+
stepRate != null && /* @__PURE__ */ jsxs("span", { className: "ml-2 text-xs", children: [
|
|
398
|
+
"(",
|
|
399
|
+
formatPercent(stepRate, 0),
|
|
400
|
+
" ",
|
|
401
|
+
t("marketing.funnel.ofPrev"),
|
|
402
|
+
")"
|
|
403
|
+
] })
|
|
404
|
+
] })
|
|
405
|
+
] }),
|
|
406
|
+
/* @__PURE__ */ jsx("div", { className: "h-9 overflow-hidden rounded-md bg-muted/40", children: /* @__PURE__ */ jsx("div", { className: "h-full rounded-md", style: { width: `${pct}%`, backgroundColor: stage.color + "33", borderLeft: `3px solid ${stage.color}` } }) })
|
|
407
|
+
] }, stage.id);
|
|
408
|
+
}) }),
|
|
409
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-5 border-t border-border pt-4 text-sm", children: [
|
|
410
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
411
|
+
t("marketing.funnel.overall"),
|
|
412
|
+
": "
|
|
413
|
+
] }),
|
|
414
|
+
/* @__PURE__ */ 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" })
|
|
415
|
+
] })
|
|
416
|
+
] })
|
|
417
|
+
] });
|
|
418
|
+
}
|
|
419
|
+
var TYPE_STYLE = {
|
|
420
|
+
Funnel: "bg-indigo-100 text-indigo-700",
|
|
421
|
+
"Landing page": "bg-pink-100 text-pink-700",
|
|
422
|
+
Website: "bg-sky-100 text-sky-700"
|
|
423
|
+
};
|
|
424
|
+
function LandingPagesView() {
|
|
425
|
+
const t = useTranslation();
|
|
426
|
+
const pages = useMarketingStore((s) => s.landingPages);
|
|
427
|
+
const totalVisits = pages.reduce((s, p) => s + p.visits, 0);
|
|
428
|
+
const totalConv = pages.reduce((s, p) => s + p.conversions, 0);
|
|
429
|
+
const avgCvr = totalVisits > 0 ? totalConv / totalVisits * 100 : 0;
|
|
430
|
+
const columns = React8.useMemo(() => [
|
|
431
|
+
{ accessorKey: "name", header: t("marketing.col.page"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
|
|
432
|
+
{ accessorKey: "type", header: t("marketing.col.type"), cell: ({ getValue }) => {
|
|
433
|
+
const v = getValue();
|
|
434
|
+
return /* @__PURE__ */ jsx("span", { className: `rounded-full px-2 py-0.5 text-xs font-medium ${TYPE_STYLE[v] ?? "bg-muted text-muted-foreground"}`, children: v });
|
|
435
|
+
} },
|
|
436
|
+
{ accessorKey: "visits", header: t("marketing.col.visits"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
437
|
+
{ accessorKey: "conversions", header: t("marketing.col.conversions"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatNumber(getValue()) }) },
|
|
438
|
+
{ accessorKey: "cvr", header: t("marketing.col.cvr"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatPercent(getValue()) }) }
|
|
439
|
+
], [t]);
|
|
440
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
|
|
441
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
442
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.landing.subtitle") }),
|
|
443
|
+
/* @__PURE__ */ jsx(RangeTabs, {})
|
|
444
|
+
] }),
|
|
445
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-4", children: [
|
|
446
|
+
/* @__PURE__ */ jsx(KpiCard, { icon: MousePointerClick, label: t("marketing.metric.totalVisits"), value: formatNumber(totalVisits) }),
|
|
447
|
+
/* @__PURE__ */ jsx(KpiCard, { icon: Layers, label: t("marketing.metric.pages"), value: String(pages.length) }),
|
|
448
|
+
/* @__PURE__ */ jsx(KpiCard, { icon: Percent, label: t("marketing.metric.avgConversion"), value: formatPercent(avgCvr) })
|
|
449
|
+
] }),
|
|
450
|
+
/* @__PURE__ */ jsx(DataTable, { columns, data: pages, emptyMessage: t("marketing.landing.subtitle") })
|
|
451
|
+
] });
|
|
452
|
+
}
|
|
453
|
+
var ctx2 = createPluginContext("ContentPlanner");
|
|
454
|
+
var ContentPlannerContextProvider = ctx2.ContextProvider;
|
|
455
|
+
ctx2.useConfig;
|
|
456
|
+
var useContentPlannerProvider = ctx2.useProvider;
|
|
457
|
+
var useContentPlannerStore = ctx2.useStore;
|
|
458
|
+
|
|
459
|
+
// src/data/contentTypes.ts
|
|
460
|
+
var PLATFORMS = [
|
|
461
|
+
"instagram",
|
|
462
|
+
"tiktok",
|
|
463
|
+
"youtube",
|
|
464
|
+
"facebook",
|
|
465
|
+
"linkedin",
|
|
466
|
+
"x",
|
|
467
|
+
"pinterest",
|
|
468
|
+
"threads"
|
|
469
|
+
];
|
|
470
|
+
var POST_STATUS_ORDER = ["idea", "script", "recording", "editing", "scheduled", "published"];
|
|
471
|
+
var POST_FORMATS = ["reel", "static", "carousel", "story", "video", "live", "article"];
|
|
472
|
+
var FORMAT_STYLE = {
|
|
473
|
+
reel: "bg-violet-100 text-violet-700",
|
|
474
|
+
static: "bg-sky-100 text-sky-700",
|
|
475
|
+
carousel: "bg-amber-100 text-amber-700",
|
|
476
|
+
story: "bg-pink-100 text-pink-700",
|
|
477
|
+
video: "bg-red-100 text-red-700",
|
|
478
|
+
live: "bg-rose-100 text-rose-700",
|
|
479
|
+
article: "bg-teal-100 text-teal-700"
|
|
480
|
+
};
|
|
481
|
+
var POST_STATUS_COLOR = {
|
|
482
|
+
idea: "#6b7280",
|
|
483
|
+
script: "#6366f1",
|
|
484
|
+
recording: "#f59e0b",
|
|
485
|
+
editing: "#f97316",
|
|
486
|
+
scheduled: "#3b82f6",
|
|
487
|
+
published: "#10b981"
|
|
488
|
+
};
|
|
489
|
+
function StatusDot({ status, className }) {
|
|
490
|
+
return /* @__PURE__ */ jsx(
|
|
491
|
+
"span",
|
|
492
|
+
{
|
|
493
|
+
className: cn("h-2 w-2 shrink-0 rounded-full", className),
|
|
494
|
+
style: { backgroundColor: POST_STATUS_COLOR[status] }
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
var STATUS_STYLE2 = {
|
|
499
|
+
idea: "bg-slate-100 text-slate-600",
|
|
500
|
+
script: "bg-indigo-100 text-indigo-700",
|
|
501
|
+
recording: "bg-amber-100 text-amber-700",
|
|
502
|
+
editing: "bg-orange-100 text-orange-700",
|
|
503
|
+
scheduled: "bg-sky-100 text-sky-700",
|
|
504
|
+
published: "bg-emerald-100 text-emerald-700"
|
|
505
|
+
};
|
|
506
|
+
function FormatBadge({ format }) {
|
|
507
|
+
const t = useTranslation();
|
|
508
|
+
return /* @__PURE__ */ jsx("span", { className: cn("rounded-full px-2 py-0.5 text-xs font-medium", FORMAT_STYLE[format]), children: t(`marketing.content.format.${format}`) });
|
|
509
|
+
}
|
|
510
|
+
function PostStatusBadge({ status }) {
|
|
511
|
+
const t = useTranslation();
|
|
512
|
+
return /* @__PURE__ */ jsx("span", { className: cn("rounded-full px-2 py-0.5 text-xs font-medium", STATUS_STYLE2[status]), children: t(`marketing.content.status.${status}`) });
|
|
513
|
+
}
|
|
514
|
+
function PlatformBadges({ platforms, className }) {
|
|
515
|
+
if (platforms.length === 0) return null;
|
|
516
|
+
return /* @__PURE__ */ jsx("span", { className: cn("inline-flex items-center gap-1 text-muted-foreground", className), children: platforms.map((p) => /* @__PURE__ */ jsx(PlatformIcon, { platform: p, className: "h-3.5 w-3.5" }, p)) });
|
|
517
|
+
}
|
|
518
|
+
function PlatformPicker({ value, onChange }) {
|
|
519
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-1.5", children: PLATFORMS.map((platform) => {
|
|
520
|
+
const selected = value.includes(platform);
|
|
521
|
+
return /* @__PURE__ */ jsxs(
|
|
522
|
+
"button",
|
|
523
|
+
{
|
|
524
|
+
type: "button",
|
|
525
|
+
"aria-pressed": selected,
|
|
526
|
+
onClick: () => onChange(selected ? value.filter((p) => p !== platform) : [...value, platform]),
|
|
527
|
+
className: cn(
|
|
528
|
+
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium capitalize transition-colors",
|
|
529
|
+
selected ? "border-primary bg-primary/10 text-foreground" : "border-border text-muted-foreground hover:bg-muted/40"
|
|
530
|
+
),
|
|
531
|
+
children: [
|
|
532
|
+
/* @__PURE__ */ jsx(PlatformIcon, { platform, className: "h-3.5 w-3.5" }),
|
|
533
|
+
platform
|
|
534
|
+
]
|
|
535
|
+
},
|
|
536
|
+
platform
|
|
537
|
+
);
|
|
538
|
+
}) });
|
|
539
|
+
}
|
|
540
|
+
function toDraft(plan) {
|
|
541
|
+
return {
|
|
542
|
+
name: plan.name,
|
|
543
|
+
objective: plan.objective ?? "",
|
|
544
|
+
tone: plan.tone ?? "",
|
|
545
|
+
pillars: plan.pillars,
|
|
546
|
+
formats: plan.formats,
|
|
547
|
+
weeklyFrequency: plan.weeklyFrequency,
|
|
548
|
+
weeksCount: plan.weeksCount,
|
|
549
|
+
briefMd: plan.briefMd
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function Field({ label, children }) {
|
|
553
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
554
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: label }),
|
|
555
|
+
children
|
|
556
|
+
] });
|
|
557
|
+
}
|
|
558
|
+
function useBriefDraft() {
|
|
559
|
+
const plan = useContentPlannerStore((s) => s.activePlan);
|
|
560
|
+
const savePlan = useContentPlannerStore((s) => s.savePlan);
|
|
561
|
+
const [draft, setDraft] = React8.useState(plan ? toDraft(plan) : null);
|
|
562
|
+
const [saving, setSaving] = React8.useState(false);
|
|
563
|
+
React8.useEffect(() => {
|
|
564
|
+
if (plan) setDraft(toDraft(plan));
|
|
565
|
+
}, [plan?.id]);
|
|
566
|
+
const patch = (partial) => setDraft((d) => d ? { ...d, ...partial } : d);
|
|
567
|
+
const dirty = !!plan && !!draft && JSON.stringify(draft) !== JSON.stringify(toDraft(plan));
|
|
568
|
+
const save = async () => {
|
|
569
|
+
if (!plan || !draft) return;
|
|
570
|
+
setSaving(true);
|
|
571
|
+
try {
|
|
572
|
+
await savePlan({
|
|
573
|
+
id: plan.id,
|
|
574
|
+
name: draft.name,
|
|
575
|
+
objective: draft.objective,
|
|
576
|
+
tone: draft.tone,
|
|
577
|
+
pillars: draft.pillars,
|
|
578
|
+
formats: draft.formats,
|
|
579
|
+
weeklyFrequency: draft.weeklyFrequency,
|
|
580
|
+
weeksCount: draft.weeksCount,
|
|
581
|
+
briefMd: draft.briefMd
|
|
582
|
+
});
|
|
583
|
+
} finally {
|
|
584
|
+
setSaving(false);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
return { plan, draft, patch, dirty, saving, save };
|
|
588
|
+
}
|
|
589
|
+
function BriefFields({ draft, patch }) {
|
|
590
|
+
const t = useTranslation();
|
|
591
|
+
const [pillarInput, setPillarInput] = React8.useState("");
|
|
592
|
+
const addPillar = () => {
|
|
593
|
+
const value = pillarInput.trim();
|
|
594
|
+
if (!value || draft.pillars.includes(value)) return;
|
|
595
|
+
patch({ pillars: [...draft.pillars, value] });
|
|
596
|
+
setPillarInput("");
|
|
597
|
+
};
|
|
598
|
+
const toggleFormat = (format) => {
|
|
599
|
+
patch({
|
|
600
|
+
formats: draft.formats.includes(format) ? draft.formats.filter((f) => f !== format) : [...draft.formats, format]
|
|
601
|
+
});
|
|
602
|
+
};
|
|
603
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
|
|
604
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: [
|
|
605
|
+
/* @__PURE__ */ jsx(Field, { label: t("marketing.content.planName"), children: /* @__PURE__ */ jsx(Input, { value: draft.name, onChange: (e) => patch({ name: e.target.value }) }) }),
|
|
606
|
+
/* @__PURE__ */ jsx(Field, { label: t("marketing.content.objective"), children: /* @__PURE__ */ jsx(
|
|
607
|
+
Input,
|
|
608
|
+
{
|
|
609
|
+
value: draft.objective,
|
|
610
|
+
onChange: (e) => patch({ objective: e.target.value }),
|
|
611
|
+
placeholder: t("marketing.content.objectivePlaceholder")
|
|
612
|
+
}
|
|
613
|
+
) }),
|
|
614
|
+
/* @__PURE__ */ jsx(Field, { label: t("marketing.content.tone"), children: /* @__PURE__ */ jsx(
|
|
615
|
+
Input,
|
|
616
|
+
{
|
|
617
|
+
value: draft.tone,
|
|
618
|
+
onChange: (e) => patch({ tone: e.target.value }),
|
|
619
|
+
placeholder: t("marketing.content.tonePlaceholder")
|
|
620
|
+
}
|
|
621
|
+
) }),
|
|
622
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
623
|
+
/* @__PURE__ */ jsx(Field, { label: t("marketing.content.weeklyFrequency"), children: /* @__PURE__ */ jsx(
|
|
624
|
+
Input,
|
|
625
|
+
{
|
|
626
|
+
type: "number",
|
|
627
|
+
min: 1,
|
|
628
|
+
max: 14,
|
|
629
|
+
value: draft.weeklyFrequency,
|
|
630
|
+
onChange: (e) => patch({ weeklyFrequency: Math.max(1, Number(e.target.value) || 1) })
|
|
631
|
+
}
|
|
632
|
+
) }),
|
|
633
|
+
/* @__PURE__ */ jsx(Field, { label: t("marketing.content.weeksCount"), children: /* @__PURE__ */ jsx(
|
|
634
|
+
Input,
|
|
635
|
+
{
|
|
636
|
+
type: "number",
|
|
637
|
+
min: 1,
|
|
638
|
+
max: 52,
|
|
639
|
+
value: draft.weeksCount,
|
|
640
|
+
onChange: (e) => patch({ weeksCount: Math.max(1, Number(e.target.value) || 1) })
|
|
641
|
+
}
|
|
642
|
+
) })
|
|
643
|
+
] })
|
|
644
|
+
] }),
|
|
645
|
+
/* @__PURE__ */ jsx(Field, { label: t("marketing.content.pillars"), children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
|
|
646
|
+
draft.pillars.map((pillar) => /* @__PURE__ */ jsxs(
|
|
647
|
+
"span",
|
|
648
|
+
{
|
|
649
|
+
className: "inline-flex items-center gap-1 rounded-full bg-muted px-2.5 py-0.5 text-xs font-medium text-foreground",
|
|
650
|
+
children: [
|
|
651
|
+
pillar,
|
|
652
|
+
/* @__PURE__ */ jsx(
|
|
653
|
+
"button",
|
|
654
|
+
{
|
|
655
|
+
type: "button",
|
|
656
|
+
onClick: () => patch({ pillars: draft.pillars.filter((p) => p !== pillar) }),
|
|
657
|
+
className: "text-muted-foreground hover:text-foreground",
|
|
658
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
|
|
659
|
+
}
|
|
660
|
+
)
|
|
661
|
+
]
|
|
662
|
+
},
|
|
663
|
+
pillar
|
|
664
|
+
)),
|
|
665
|
+
/* @__PURE__ */ jsx(
|
|
666
|
+
Input,
|
|
667
|
+
{
|
|
668
|
+
value: pillarInput,
|
|
669
|
+
onChange: (e) => setPillarInput(e.target.value),
|
|
670
|
+
onKeyDown: (e) => {
|
|
671
|
+
if (e.key === "Enter") {
|
|
672
|
+
e.preventDefault();
|
|
673
|
+
addPillar();
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
onBlur: addPillar,
|
|
677
|
+
placeholder: t("marketing.content.pillarPlaceholder"),
|
|
678
|
+
className: "h-7 w-44 text-xs"
|
|
679
|
+
}
|
|
680
|
+
)
|
|
681
|
+
] }) }),
|
|
682
|
+
/* @__PURE__ */ jsx(Field, { label: t("marketing.content.formatsLabel"), children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-4", children: POST_FORMATS.map((format) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm text-foreground", children: [
|
|
683
|
+
/* @__PURE__ */ jsx(
|
|
684
|
+
Checkbox,
|
|
685
|
+
{
|
|
686
|
+
checked: draft.formats.includes(format),
|
|
687
|
+
onChange: () => toggleFormat(format)
|
|
688
|
+
}
|
|
689
|
+
),
|
|
690
|
+
t(`marketing.content.format.${format}`)
|
|
691
|
+
] }, format)) }) }),
|
|
692
|
+
/* @__PURE__ */ jsx(Field, { label: t("marketing.content.briefBody"), children: /* @__PURE__ */ jsx(
|
|
693
|
+
MarkdownEditor,
|
|
694
|
+
{
|
|
695
|
+
value: draft.briefMd,
|
|
696
|
+
onChange: (briefMd) => patch({ briefMd }),
|
|
697
|
+
placeholder: t("marketing.content.briefPlaceholder"),
|
|
698
|
+
editLabel: t("marketing.content.edit"),
|
|
699
|
+
previewLabel: t("marketing.content.preview"),
|
|
700
|
+
minRows: 10
|
|
701
|
+
}
|
|
702
|
+
) })
|
|
703
|
+
] });
|
|
704
|
+
}
|
|
705
|
+
function PlanBriefView({ onBack }) {
|
|
706
|
+
const t = useTranslation();
|
|
707
|
+
const { plan, draft, patch, dirty, saving, save } = useBriefDraft();
|
|
708
|
+
if (!plan || !draft) {
|
|
709
|
+
return /* @__PURE__ */ jsx("div", { className: "mx-auto max-w-3xl text-sm text-muted-foreground", children: "\u2026" });
|
|
710
|
+
}
|
|
711
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-3xl space-y-5", children: [
|
|
712
|
+
/* @__PURE__ */ jsx(
|
|
713
|
+
SubpageHeader,
|
|
714
|
+
{
|
|
715
|
+
title: t("marketing.content.brief"),
|
|
716
|
+
subtitle: plan.name,
|
|
717
|
+
parentLabel: t("marketing.content.title"),
|
|
718
|
+
onBack,
|
|
719
|
+
actions: /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => void save(), disabled: !dirty || saving, children: t("marketing.content.save") })
|
|
720
|
+
}
|
|
721
|
+
),
|
|
722
|
+
/* @__PURE__ */ jsx(BriefFields, { draft, patch })
|
|
723
|
+
] });
|
|
724
|
+
}
|
|
725
|
+
function PlanBriefSheet({ open, onClose }) {
|
|
726
|
+
const t = useTranslation();
|
|
727
|
+
const { plan, draft, patch, dirty, saving, save } = useBriefDraft();
|
|
728
|
+
return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange: (v) => {
|
|
729
|
+
if (!v) onClose();
|
|
730
|
+
}, children: /* @__PURE__ */ jsxs(SheetContent, { width: "max-w-2xl", children: [
|
|
731
|
+
/* @__PURE__ */ jsxs(SheetHeader, { children: [
|
|
732
|
+
/* @__PURE__ */ jsx(SheetTitle, { children: t("marketing.content.brief") }),
|
|
733
|
+
plan && /* @__PURE__ */ jsx(SheetDescription, { children: plan.name })
|
|
734
|
+
] }),
|
|
735
|
+
/* @__PURE__ */ jsx(SheetBody, { children: draft ? /* @__PURE__ */ jsx(BriefFields, { draft, patch }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" }) }),
|
|
736
|
+
/* @__PURE__ */ jsxs(SheetFooter, { children: [
|
|
737
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, disabled: saving, children: t("marketing.content.cancel") }),
|
|
738
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => void save(), disabled: !dirty || saving, children: t("marketing.content.save") })
|
|
739
|
+
] })
|
|
740
|
+
] }) });
|
|
741
|
+
}
|
|
742
|
+
var DEFAULT_WEEKS = 8;
|
|
743
|
+
var ADD_ACCOUNT_VALUE = "__add-account__";
|
|
744
|
+
var POST_DRAG_MIME = "application/x-mkt-post";
|
|
745
|
+
function PostCard({ post, onOpen, onDelete }) {
|
|
746
|
+
const t = useTranslation();
|
|
747
|
+
const [dragging, setDragging] = React8.useState(false);
|
|
748
|
+
return /* @__PURE__ */ jsxs(
|
|
749
|
+
"div",
|
|
750
|
+
{
|
|
751
|
+
role: "button",
|
|
752
|
+
tabIndex: 0,
|
|
753
|
+
onClick: onOpen,
|
|
754
|
+
onKeyDown: (e) => {
|
|
755
|
+
if (e.key === "Enter") onOpen();
|
|
756
|
+
},
|
|
757
|
+
draggable: true,
|
|
758
|
+
onDragStart: (e) => {
|
|
759
|
+
e.dataTransfer.setData(POST_DRAG_MIME, post.id);
|
|
760
|
+
e.dataTransfer.effectAllowed = "move";
|
|
761
|
+
setDragging(true);
|
|
762
|
+
},
|
|
763
|
+
onDragEnd: () => setDragging(false),
|
|
764
|
+
className: cn(
|
|
765
|
+
"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",
|
|
766
|
+
dragging && "opacity-50"
|
|
767
|
+
),
|
|
768
|
+
children: [
|
|
769
|
+
/* @__PURE__ */ jsx(
|
|
770
|
+
GripVertical,
|
|
771
|
+
{
|
|
772
|
+
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",
|
|
773
|
+
"aria-hidden": true
|
|
774
|
+
}
|
|
775
|
+
),
|
|
776
|
+
onDelete && /* @__PURE__ */ jsx(
|
|
777
|
+
"button",
|
|
778
|
+
{
|
|
779
|
+
type: "button",
|
|
780
|
+
"aria-label": t("marketing.content.deletePost"),
|
|
781
|
+
onClick: (e) => {
|
|
782
|
+
e.stopPropagation();
|
|
783
|
+
onDelete();
|
|
784
|
+
},
|
|
785
|
+
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",
|
|
786
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
|
|
787
|
+
}
|
|
788
|
+
),
|
|
789
|
+
/* @__PURE__ */ jsx("p", { className: "pr-5 text-sm font-medium text-foreground", children: post.title || t("marketing.content.untitled") }),
|
|
790
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-1.5", children: [
|
|
791
|
+
/* @__PURE__ */ jsx(FormatBadge, { format: post.format }),
|
|
792
|
+
/* @__PURE__ */ jsx(PostStatusBadge, { status: post.status }),
|
|
793
|
+
post.scheduledDate && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: post.scheduledDate }),
|
|
794
|
+
/* @__PURE__ */ jsx(PlatformBadges, { platforms: post.platforms })
|
|
795
|
+
] })
|
|
796
|
+
]
|
|
797
|
+
}
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
function WeekColumn({ week, rangeLabel, posts, onOpenPost, onNewPost, onDeletePost, onDropPost, disabled }) {
|
|
801
|
+
const t = useTranslation();
|
|
802
|
+
const [isDropTarget, setIsDropTarget] = React8.useState(false);
|
|
803
|
+
return /* @__PURE__ */ jsxs(
|
|
804
|
+
"div",
|
|
805
|
+
{
|
|
806
|
+
className: cn(
|
|
807
|
+
"flex flex-col gap-2 rounded-card border border-border bg-muted/20 p-2.5 transition-colors",
|
|
808
|
+
isDropTarget && "border-primary bg-primary/5 ring-1 ring-primary/40"
|
|
809
|
+
),
|
|
810
|
+
onDragOver: (e) => {
|
|
811
|
+
if (!onDropPost || !e.dataTransfer.types.includes(POST_DRAG_MIME)) return;
|
|
812
|
+
e.preventDefault();
|
|
813
|
+
e.dataTransfer.dropEffect = "move";
|
|
814
|
+
setIsDropTarget(true);
|
|
815
|
+
},
|
|
816
|
+
onDragLeave: () => setIsDropTarget(false),
|
|
817
|
+
onDrop: (e) => {
|
|
818
|
+
if (!onDropPost) return;
|
|
819
|
+
e.preventDefault();
|
|
820
|
+
setIsDropTarget(false);
|
|
821
|
+
const postId = e.dataTransfer.getData(POST_DRAG_MIME);
|
|
822
|
+
if (postId) onDropPost(postId, week);
|
|
823
|
+
},
|
|
824
|
+
children: [
|
|
825
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between px-1", children: [
|
|
826
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: [
|
|
827
|
+
t("marketing.content.week"),
|
|
828
|
+
" ",
|
|
829
|
+
week
|
|
830
|
+
] }),
|
|
831
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground/80", children: rangeLabel })
|
|
832
|
+
] }),
|
|
833
|
+
posts.map((post) => /* @__PURE__ */ jsx(
|
|
834
|
+
PostCard,
|
|
835
|
+
{
|
|
836
|
+
post,
|
|
837
|
+
onOpen: () => onOpenPost(post.id),
|
|
838
|
+
onDelete: onDeletePost ? () => onDeletePost(post.id) : void 0
|
|
839
|
+
},
|
|
840
|
+
post.id
|
|
841
|
+
)),
|
|
842
|
+
/* @__PURE__ */ jsxs(
|
|
843
|
+
"button",
|
|
844
|
+
{
|
|
845
|
+
type: "button",
|
|
846
|
+
onClick: () => onNewPost(week),
|
|
847
|
+
disabled,
|
|
848
|
+
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",
|
|
849
|
+
children: [
|
|
850
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
|
|
851
|
+
t("marketing.content.newPost")
|
|
852
|
+
]
|
|
853
|
+
}
|
|
854
|
+
)
|
|
855
|
+
]
|
|
856
|
+
}
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
function AccountModal({ open, account, onClose }) {
|
|
860
|
+
const t = useTranslation();
|
|
861
|
+
const saveAccount = useContentPlannerStore((s) => s.saveAccount);
|
|
862
|
+
const deleteAccount = useContentPlannerStore((s) => s.deleteAccount);
|
|
863
|
+
const isEdit = !!account;
|
|
864
|
+
const [name, setName] = React8.useState("");
|
|
865
|
+
const [handle, setHandle] = React8.useState("");
|
|
866
|
+
const [platforms, setPlatforms] = React8.useState(["instagram"]);
|
|
867
|
+
const [saving, setSaving] = React8.useState(false);
|
|
868
|
+
const [confirmDelete, setConfirmDelete] = React8.useState(false);
|
|
869
|
+
React8.useEffect(() => {
|
|
870
|
+
if (open) {
|
|
871
|
+
setName(account?.name ?? "");
|
|
872
|
+
setHandle(account?.handle ?? "");
|
|
873
|
+
setPlatforms(account?.platforms?.length ? account.platforms : ["instagram"]);
|
|
874
|
+
setSaving(false);
|
|
875
|
+
setConfirmDelete(false);
|
|
876
|
+
}
|
|
877
|
+
}, [open, account]);
|
|
878
|
+
const create = async () => {
|
|
879
|
+
if (!name.trim() || platforms.length === 0 || saving) return;
|
|
880
|
+
setSaving(true);
|
|
881
|
+
try {
|
|
882
|
+
await saveAccount({
|
|
883
|
+
...isEdit ? { id: account.id } : {},
|
|
884
|
+
name: name.trim(),
|
|
885
|
+
handle: handle.trim() || void 0,
|
|
886
|
+
platforms
|
|
887
|
+
});
|
|
888
|
+
onClose();
|
|
889
|
+
} finally {
|
|
890
|
+
setSaving(false);
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
const remove = async () => {
|
|
894
|
+
if (!account || saving) return;
|
|
895
|
+
setSaving(true);
|
|
896
|
+
try {
|
|
897
|
+
await deleteAccount(account.id);
|
|
898
|
+
onClose();
|
|
899
|
+
} finally {
|
|
900
|
+
setSaving(false);
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (o) => {
|
|
904
|
+
if (!o) onClose();
|
|
905
|
+
}, children: /* @__PURE__ */ jsxs(ModalContent, { size: "sm", children: [
|
|
906
|
+
/* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: isEdit ? t("marketing.content.editAccountTitle") : t("marketing.content.addAccountTitle") }) }),
|
|
907
|
+
/* @__PURE__ */ jsxs(ModalBody, { className: "space-y-4", children: [
|
|
908
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
909
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.nameLabel") }),
|
|
910
|
+
/* @__PURE__ */ jsx(
|
|
911
|
+
Input,
|
|
912
|
+
{
|
|
913
|
+
autoFocus: true,
|
|
914
|
+
value: name,
|
|
915
|
+
onChange: (e) => setName(e.target.value),
|
|
916
|
+
placeholder: t("marketing.content.accountNamePlaceholder"),
|
|
917
|
+
onKeyDown: (e) => {
|
|
918
|
+
if (e.key === "Enter") void create();
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
)
|
|
922
|
+
] }),
|
|
923
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
924
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.handleLabel") }),
|
|
925
|
+
/* @__PURE__ */ jsx(
|
|
926
|
+
Input,
|
|
927
|
+
{
|
|
928
|
+
value: handle,
|
|
929
|
+
onChange: (e) => setHandle(e.target.value),
|
|
930
|
+
placeholder: t("marketing.content.handlePlaceholder")
|
|
931
|
+
}
|
|
932
|
+
)
|
|
933
|
+
] }),
|
|
934
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
935
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.platformsLabel") }),
|
|
936
|
+
/* @__PURE__ */ jsx(PlatformPicker, { value: platforms, onChange: setPlatforms })
|
|
937
|
+
] })
|
|
938
|
+
] }),
|
|
939
|
+
/* @__PURE__ */ jsx(ModalFooter, { children: confirmDelete && isEdit ? /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-3 rounded-card border border-destructive/30 bg-destructive/5 p-3", children: [
|
|
940
|
+
/* @__PURE__ */ jsx("p", { className: "flex-1 text-xs text-destructive", children: t("marketing.content.deleteAccountConfirm") }),
|
|
941
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => setConfirmDelete(false), disabled: saving, children: t("marketing.content.cancel") }),
|
|
942
|
+
/* @__PURE__ */ jsx(Button, { variant: "destructive", size: "sm", onClick: () => void remove(), disabled: saving, children: t("marketing.content.deleteAccount") })
|
|
943
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
944
|
+
isEdit && /* @__PURE__ */ jsxs(
|
|
945
|
+
Button,
|
|
946
|
+
{
|
|
947
|
+
variant: "outline",
|
|
948
|
+
onClick: () => setConfirmDelete(true),
|
|
949
|
+
disabled: saving,
|
|
950
|
+
className: "mr-auto text-destructive hover:text-destructive",
|
|
951
|
+
children: [
|
|
952
|
+
/* @__PURE__ */ jsx(Trash2, { className: "mr-1.5 h-4 w-4" }),
|
|
953
|
+
t("marketing.content.deleteAccount")
|
|
954
|
+
]
|
|
955
|
+
}
|
|
956
|
+
),
|
|
957
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, disabled: saving, children: t("marketing.content.cancel") }),
|
|
958
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => void create(), disabled: saving || !name.trim() || platforms.length === 0, children: isEdit ? t("marketing.content.save") : t("marketing.content.createAccount") })
|
|
959
|
+
] }) })
|
|
960
|
+
] }) });
|
|
961
|
+
}
|
|
962
|
+
function AccountSelector() {
|
|
963
|
+
const t = useTranslation();
|
|
964
|
+
const loaded = useContentPlannerStore((s) => s.loaded);
|
|
965
|
+
const accounts = useContentPlannerStore((s) => s.accounts);
|
|
966
|
+
const activeAccountId = useContentPlannerStore((s) => s.activeAccountId);
|
|
967
|
+
const setAccount = useContentPlannerStore((s) => s.setAccount);
|
|
968
|
+
const [open, setOpen] = React8.useState(false);
|
|
969
|
+
const [modal, setModal] = React8.useState(null);
|
|
970
|
+
const activeAccount = accounts.find((a) => a.id === activeAccountId) ?? null;
|
|
971
|
+
if (!loaded) {
|
|
972
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
973
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-64" }),
|
|
974
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-9" })
|
|
975
|
+
] });
|
|
976
|
+
}
|
|
977
|
+
const openSettings = (account) => {
|
|
978
|
+
setOpen(false);
|
|
979
|
+
setModal({ mode: "edit", account });
|
|
980
|
+
};
|
|
981
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
982
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
983
|
+
/* @__PURE__ */ jsxs(
|
|
984
|
+
Select,
|
|
985
|
+
{
|
|
986
|
+
open,
|
|
987
|
+
onOpenChange: setOpen,
|
|
988
|
+
value: activeAccountId ?? void 0,
|
|
989
|
+
onValueChange: (id) => {
|
|
990
|
+
if (id === ADD_ACCOUNT_VALUE) {
|
|
991
|
+
setModal({ mode: "create" });
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
void setAccount(id);
|
|
995
|
+
},
|
|
996
|
+
children: [
|
|
997
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "w-64", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("marketing.content.account") }) }),
|
|
998
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
999
|
+
accounts.length === 0 ? /* @__PURE__ */ jsx(SelectItem, { value: "__none__", disabled: true, children: t("marketing.content.noAccountsAvailable") }) : accounts.map((acc) => /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
1000
|
+
/* @__PURE__ */ jsx(SelectItem, { value: acc.id, className: "pr-9", children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
1001
|
+
acc.name,
|
|
1002
|
+
/* @__PURE__ */ jsx(PlatformBadges, { platforms: acc.platforms })
|
|
1003
|
+
] }) }),
|
|
1004
|
+
/* @__PURE__ */ jsx(
|
|
1005
|
+
"button",
|
|
1006
|
+
{
|
|
1007
|
+
type: "button",
|
|
1008
|
+
"aria-label": t("marketing.content.accountSettings"),
|
|
1009
|
+
onPointerDown: (e) => {
|
|
1010
|
+
e.stopPropagation();
|
|
1011
|
+
e.preventDefault();
|
|
1012
|
+
},
|
|
1013
|
+
onPointerUp: (e) => {
|
|
1014
|
+
e.stopPropagation();
|
|
1015
|
+
e.preventDefault();
|
|
1016
|
+
},
|
|
1017
|
+
onClick: (e) => {
|
|
1018
|
+
e.stopPropagation();
|
|
1019
|
+
openSettings(acc);
|
|
1020
|
+
},
|
|
1021
|
+
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",
|
|
1022
|
+
children: /* @__PURE__ */ jsx(Settings2, { className: "h-3.5 w-3.5" })
|
|
1023
|
+
}
|
|
1024
|
+
)
|
|
1025
|
+
] }, acc.id)),
|
|
1026
|
+
/* @__PURE__ */ jsx(SelectSeparator, {}),
|
|
1027
|
+
/* @__PURE__ */ jsx(SelectItem, { value: ADD_ACCOUNT_VALUE, children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1028
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
|
|
1029
|
+
t("marketing.content.addAccount")
|
|
1030
|
+
] }) })
|
|
1031
|
+
] })
|
|
1032
|
+
]
|
|
1033
|
+
}
|
|
1034
|
+
),
|
|
1035
|
+
activeAccount && /* @__PURE__ */ jsx(
|
|
1036
|
+
Button,
|
|
1037
|
+
{
|
|
1038
|
+
variant: "ghost",
|
|
1039
|
+
size: "sm",
|
|
1040
|
+
className: "h-9 w-9 p-0 text-muted-foreground",
|
|
1041
|
+
"aria-label": t("marketing.content.accountSettings"),
|
|
1042
|
+
onClick: () => setModal({ mode: "edit", account: activeAccount }),
|
|
1043
|
+
children: /* @__PURE__ */ jsx(Settings2, { className: "h-4 w-4" })
|
|
1044
|
+
}
|
|
1045
|
+
)
|
|
1046
|
+
] }),
|
|
1047
|
+
/* @__PURE__ */ jsx(
|
|
1048
|
+
AccountModal,
|
|
1049
|
+
{
|
|
1050
|
+
open: !!modal,
|
|
1051
|
+
account: modal?.mode === "edit" ? modal.account : null,
|
|
1052
|
+
onClose: () => setModal(null)
|
|
1053
|
+
}
|
|
1054
|
+
)
|
|
1055
|
+
] });
|
|
1056
|
+
}
|
|
1057
|
+
function ViewSwitcher({ mode, onChange }) {
|
|
1058
|
+
const t = useTranslation();
|
|
1059
|
+
return /* @__PURE__ */ jsx(
|
|
1060
|
+
SegmentedControl,
|
|
1061
|
+
{
|
|
1062
|
+
size: "md",
|
|
1063
|
+
"aria-label": t("marketing.content.viewBoard"),
|
|
1064
|
+
value: mode,
|
|
1065
|
+
onChange,
|
|
1066
|
+
options: [
|
|
1067
|
+
{ value: "board", label: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1068
|
+
/* @__PURE__ */ jsx(Columns3, { className: "h-3.5 w-3.5" }),
|
|
1069
|
+
t("marketing.content.viewBoard")
|
|
1070
|
+
] }) },
|
|
1071
|
+
{ value: "calendar", label: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1072
|
+
/* @__PURE__ */ jsx(CalendarDays, { className: "h-3.5 w-3.5" }),
|
|
1073
|
+
t("marketing.content.viewCalendar")
|
|
1074
|
+
] }) },
|
|
1075
|
+
{ value: "list", label: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1076
|
+
/* @__PURE__ */ jsx(List, { className: "h-3.5 w-3.5" }),
|
|
1077
|
+
t("marketing.content.viewList")
|
|
1078
|
+
] }) }
|
|
1079
|
+
]
|
|
1080
|
+
}
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
function shortDay(d) {
|
|
1084
|
+
return d.toLocaleDateString(void 0, { day: "numeric", month: "short" });
|
|
1085
|
+
}
|
|
1086
|
+
function buildBoardWeeks(startDate, weeksCount) {
|
|
1087
|
+
return Array.from({ length: weeksCount }, (_, i) => {
|
|
1088
|
+
const start = new Date(startDate);
|
|
1089
|
+
start.setDate(start.getDate() + i * 7);
|
|
1090
|
+
const end = new Date(start);
|
|
1091
|
+
end.setDate(start.getDate() + 6);
|
|
1092
|
+
return {
|
|
1093
|
+
week: i + 1,
|
|
1094
|
+
rangeLabel: `${shortDay(start)} \u2013 ${shortDay(end)}`,
|
|
1095
|
+
monthLabel: start.toLocaleDateString(void 0, { month: "long", year: "numeric" })
|
|
1096
|
+
};
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
function BoardView({ startDate, weeksCount, postsByWeek, onOpenPost, onNewPost, onDeletePost, onDropPost, disabled }) {
|
|
1100
|
+
const months = [];
|
|
1101
|
+
for (const bw of buildBoardWeeks(startDate, weeksCount)) {
|
|
1102
|
+
const current = months[months.length - 1];
|
|
1103
|
+
if (current && current.label === bw.monthLabel) current.weeks.push(bw);
|
|
1104
|
+
else months.push({ label: bw.monthLabel, weeks: [bw] });
|
|
1105
|
+
}
|
|
1106
|
+
return /* @__PURE__ */ jsx("div", { className: "space-y-8", children: months.map(({ label, weeks }) => /* @__PURE__ */ jsxs("section", { className: "space-y-3", children: [
|
|
1107
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: label }),
|
|
1108
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4", children: weeks.map(({ week, rangeLabel }) => /* @__PURE__ */ jsx(
|
|
1109
|
+
WeekColumn,
|
|
1110
|
+
{
|
|
1111
|
+
week,
|
|
1112
|
+
rangeLabel,
|
|
1113
|
+
posts: postsByWeek.get(week) ?? [],
|
|
1114
|
+
onOpenPost,
|
|
1115
|
+
onNewPost,
|
|
1116
|
+
onDeletePost,
|
|
1117
|
+
onDropPost,
|
|
1118
|
+
disabled
|
|
1119
|
+
},
|
|
1120
|
+
week
|
|
1121
|
+
)) })
|
|
1122
|
+
] }, label)) });
|
|
1123
|
+
}
|
|
1124
|
+
function toISODate(d) {
|
|
1125
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1126
|
+
}
|
|
1127
|
+
function mondayOfCurrentWeek() {
|
|
1128
|
+
const now = /* @__PURE__ */ new Date();
|
|
1129
|
+
const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1130
|
+
monday.setDate(monday.getDate() - (monday.getDay() + 6) % 7);
|
|
1131
|
+
return monday;
|
|
1132
|
+
}
|
|
1133
|
+
function CalendarView({ posts, onOpenPost, onNewPostAt, disabled }) {
|
|
1134
|
+
const t = useTranslation();
|
|
1135
|
+
const [cursor, setCursor] = React8.useState(() => {
|
|
1136
|
+
const first = posts.find((p) => p.scheduledDate)?.scheduledDate;
|
|
1137
|
+
const base = first ? /* @__PURE__ */ new Date(`${first}T00:00:00`) : /* @__PURE__ */ new Date();
|
|
1138
|
+
return new Date(base.getFullYear(), base.getMonth(), 1);
|
|
1139
|
+
});
|
|
1140
|
+
const [expandedDay, setExpandedDay] = React8.useState(null);
|
|
1141
|
+
const MAX_VISIBLE = 3;
|
|
1142
|
+
const postsByDate = React8.useMemo(() => {
|
|
1143
|
+
const map = /* @__PURE__ */ new Map();
|
|
1144
|
+
for (const post of posts) {
|
|
1145
|
+
if (!post.scheduledDate) continue;
|
|
1146
|
+
const list = map.get(post.scheduledDate) ?? [];
|
|
1147
|
+
list.push(post);
|
|
1148
|
+
map.set(post.scheduledDate, list);
|
|
1149
|
+
}
|
|
1150
|
+
return map;
|
|
1151
|
+
}, [posts]);
|
|
1152
|
+
const unscheduled = posts.filter((p) => !p.scheduledDate);
|
|
1153
|
+
const days = React8.useMemo(() => {
|
|
1154
|
+
const firstOfMonth = new Date(cursor.getFullYear(), cursor.getMonth(), 1);
|
|
1155
|
+
const start = new Date(firstOfMonth);
|
|
1156
|
+
start.setDate(start.getDate() - (firstOfMonth.getDay() + 6) % 7);
|
|
1157
|
+
const cells = [];
|
|
1158
|
+
for (let i = 0; i < 42; i++) {
|
|
1159
|
+
const d = new Date(start);
|
|
1160
|
+
d.setDate(start.getDate() + i);
|
|
1161
|
+
cells.push(d);
|
|
1162
|
+
}
|
|
1163
|
+
while (cells.length > 7 && cells[cells.length - 7].getMonth() !== cursor.getMonth()) {
|
|
1164
|
+
cells.splice(cells.length - 7, 7);
|
|
1165
|
+
}
|
|
1166
|
+
return cells;
|
|
1167
|
+
}, [cursor]);
|
|
1168
|
+
const monthLabel = cursor.toLocaleDateString(void 0, { month: "long", year: "numeric" });
|
|
1169
|
+
const weekdayLabels = days.slice(0, 7).map(
|
|
1170
|
+
(d) => d.toLocaleDateString(void 0, { weekday: "short" })
|
|
1171
|
+
);
|
|
1172
|
+
const todayISO = toISODate(/* @__PURE__ */ new Date());
|
|
1173
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
1174
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1175
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold capitalize text-foreground", children: monthLabel }),
|
|
1176
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
1177
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => setCursor(new Date(cursor.getFullYear(), cursor.getMonth() - 1, 1)), children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" }) }),
|
|
1178
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => setCursor(new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1)), children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" }) })
|
|
1179
|
+
] })
|
|
1180
|
+
] }),
|
|
1181
|
+
unscheduled.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 rounded-card border border-dashed border-border bg-muted/20 p-2.5", children: [
|
|
1182
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: t("marketing.content.unscheduled") }),
|
|
1183
|
+
unscheduled.map((post) => /* @__PURE__ */ jsxs(
|
|
1184
|
+
"button",
|
|
1185
|
+
{
|
|
1186
|
+
type: "button",
|
|
1187
|
+
onClick: () => onOpenPost(post.id),
|
|
1188
|
+
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",
|
|
1189
|
+
children: [
|
|
1190
|
+
post.title || t("marketing.content.untitled"),
|
|
1191
|
+
/* @__PURE__ */ jsx(FormatBadge, { format: post.format })
|
|
1192
|
+
]
|
|
1193
|
+
},
|
|
1194
|
+
post.id
|
|
1195
|
+
))
|
|
1196
|
+
] }),
|
|
1197
|
+
/* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-card border border-border", children: [
|
|
1198
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 border-b border-border bg-muted/30", children: weekdayLabels.map((label, i) => /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-center text-xs font-medium capitalize text-muted-foreground", children: label }, i)) }),
|
|
1199
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-7", children: days.map((day, i) => {
|
|
1200
|
+
const iso = toISODate(day);
|
|
1201
|
+
const inMonth = day.getMonth() === cursor.getMonth();
|
|
1202
|
+
const dayPosts = postsByDate.get(iso) ?? [];
|
|
1203
|
+
return /* @__PURE__ */ jsxs(
|
|
1204
|
+
"div",
|
|
1205
|
+
{
|
|
1206
|
+
className: cn(
|
|
1207
|
+
"group min-h-24 border-b border-r border-border p-1.5 [&:nth-child(7n)]:border-r-0",
|
|
1208
|
+
!inMonth && "bg-muted/20"
|
|
1209
|
+
),
|
|
1210
|
+
children: [
|
|
1211
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1212
|
+
/* @__PURE__ */ jsx(
|
|
1213
|
+
"span",
|
|
1214
|
+
{
|
|
1215
|
+
className: cn(
|
|
1216
|
+
"text-xs",
|
|
1217
|
+
inMonth ? "text-foreground" : "text-muted-foreground/60",
|
|
1218
|
+
iso === todayISO && "flex h-5 w-5 items-center justify-center rounded-full bg-primary font-semibold text-primary-foreground"
|
|
1219
|
+
),
|
|
1220
|
+
children: day.getDate()
|
|
1221
|
+
}
|
|
1222
|
+
),
|
|
1223
|
+
!disabled && /* @__PURE__ */ jsx(
|
|
1224
|
+
"button",
|
|
1225
|
+
{
|
|
1226
|
+
type: "button",
|
|
1227
|
+
onClick: () => onNewPostAt(iso),
|
|
1228
|
+
className: "rounded p-0.5 text-muted-foreground opacity-0 transition-opacity hover:bg-muted/60 hover:text-foreground group-hover:opacity-100",
|
|
1229
|
+
"aria-label": t("marketing.content.newPost"),
|
|
1230
|
+
children: /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" })
|
|
1231
|
+
}
|
|
1232
|
+
)
|
|
1233
|
+
] }),
|
|
1234
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-1 space-y-1", children: [
|
|
1235
|
+
(expandedDay === iso ? dayPosts : dayPosts.slice(0, MAX_VISIBLE)).map((post) => /* @__PURE__ */ jsx(
|
|
1236
|
+
"button",
|
|
1237
|
+
{
|
|
1238
|
+
type: "button",
|
|
1239
|
+
onClick: () => onOpenPost(post.id),
|
|
1240
|
+
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",
|
|
1241
|
+
children: post.title || t("marketing.content.untitled")
|
|
1242
|
+
},
|
|
1243
|
+
post.id
|
|
1244
|
+
)),
|
|
1245
|
+
dayPosts.length > MAX_VISIBLE && /* @__PURE__ */ jsx(
|
|
1246
|
+
"button",
|
|
1247
|
+
{
|
|
1248
|
+
type: "button",
|
|
1249
|
+
onClick: () => setExpandedDay(expandedDay === iso ? null : iso),
|
|
1250
|
+
className: "block w-full px-1.5 py-0.5 text-left text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
|
|
1251
|
+
children: expandedDay === iso ? t("marketing.content.showLess") : `+${dayPosts.length - MAX_VISIBLE}`
|
|
1252
|
+
}
|
|
1253
|
+
)
|
|
1254
|
+
] })
|
|
1255
|
+
]
|
|
1256
|
+
},
|
|
1257
|
+
i
|
|
1258
|
+
);
|
|
1259
|
+
}) })
|
|
1260
|
+
] })
|
|
1261
|
+
] });
|
|
1262
|
+
}
|
|
1263
|
+
function ListView({ posts, onOpenPost }) {
|
|
1264
|
+
const t = useTranslation();
|
|
1265
|
+
if (posts.length === 0) {
|
|
1266
|
+
return /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.content.noPosts") });
|
|
1267
|
+
}
|
|
1268
|
+
return /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-card border border-border", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-sm", children: [
|
|
1269
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "border-b border-border bg-muted/30 text-left text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [
|
|
1270
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2", children: t("marketing.content.listWeek") }),
|
|
1271
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2", children: t("marketing.content.listTitle") }),
|
|
1272
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2", children: t("marketing.content.listFormat") }),
|
|
1273
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2", children: t("marketing.content.listStatus") }),
|
|
1274
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2", children: t("marketing.content.listDate") })
|
|
1275
|
+
] }) }),
|
|
1276
|
+
/* @__PURE__ */ jsx("tbody", { children: posts.map((post) => /* @__PURE__ */ jsxs(
|
|
1277
|
+
"tr",
|
|
1278
|
+
{
|
|
1279
|
+
onClick: () => onOpenPost(post.id),
|
|
1280
|
+
className: "cursor-pointer border-b border-border last:border-b-0 transition-colors hover:bg-muted/30",
|
|
1281
|
+
children: [
|
|
1282
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-muted-foreground", children: post.weekNumber }),
|
|
1283
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-2 font-medium text-foreground", children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
1284
|
+
post.title || t("marketing.content.untitled"),
|
|
1285
|
+
/* @__PURE__ */ jsx(PlatformBadges, { platforms: post.platforms })
|
|
1286
|
+
] }) }),
|
|
1287
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsx(FormatBadge, { format: post.format }) }),
|
|
1288
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsx(PostStatusBadge, { status: post.status }) }),
|
|
1289
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-muted-foreground", children: post.scheduledDate ?? "\u2014" })
|
|
1290
|
+
]
|
|
1291
|
+
},
|
|
1292
|
+
post.id
|
|
1293
|
+
)) })
|
|
1294
|
+
] }) });
|
|
1295
|
+
}
|
|
1296
|
+
function ContentView({ onOpenPost }) {
|
|
1297
|
+
const t = useTranslation();
|
|
1298
|
+
const loaded = useContentPlannerStore((s) => s.loaded);
|
|
1299
|
+
const loading = useContentPlannerStore((s) => s.loading);
|
|
1300
|
+
const accounts = useContentPlannerStore((s) => s.accounts);
|
|
1301
|
+
const activeAccountId = useContentPlannerStore((s) => s.activeAccountId);
|
|
1302
|
+
const plan = useContentPlannerStore((s) => s.activePlan);
|
|
1303
|
+
const posts = useContentPlannerStore((s) => s.posts);
|
|
1304
|
+
const load = useContentPlannerStore((s) => s.load);
|
|
1305
|
+
const savePlan = useContentPlannerStore((s) => s.savePlan);
|
|
1306
|
+
const savePost = useContentPlannerStore((s) => s.savePost);
|
|
1307
|
+
const deletePost = useContentPlannerStore((s) => s.deletePost);
|
|
1308
|
+
const viewMode = useContentPlannerStore((s) => s.viewMode);
|
|
1309
|
+
const setViewMode = useContentPlannerStore((s) => s.setViewMode);
|
|
1310
|
+
const loadedTenantId = useContentPlannerStore((s) => s.loadedTenantId);
|
|
1311
|
+
const [briefOpen, setBriefOpen] = React8.useState(false);
|
|
1312
|
+
const [pendingDeleteId, setPendingDeleteId] = React8.useState(null);
|
|
1313
|
+
const tenantId = useOrganizationStore((state) => state.currentOrg?.id);
|
|
1314
|
+
React8.useEffect(() => {
|
|
1315
|
+
if (!loading && (!loaded || tenantId && tenantId !== loadedTenantId)) void load();
|
|
1316
|
+
}, [tenantId, loadedTenantId, loaded, loading, load]);
|
|
1317
|
+
const postsByWeek = React8.useMemo(() => {
|
|
1318
|
+
const map = /* @__PURE__ */ new Map();
|
|
1319
|
+
for (const post of posts) {
|
|
1320
|
+
const list = map.get(post.weekNumber) ?? [];
|
|
1321
|
+
list.push(post);
|
|
1322
|
+
map.set(post.weekNumber, list);
|
|
1323
|
+
}
|
|
1324
|
+
return map;
|
|
1325
|
+
}, [posts]);
|
|
1326
|
+
const ensurePlan = async () => {
|
|
1327
|
+
if (plan) return plan;
|
|
1328
|
+
if (!activeAccountId) return null;
|
|
1329
|
+
return savePlan({
|
|
1330
|
+
accountId: activeAccountId,
|
|
1331
|
+
name: t("marketing.content.defaultPlanName"),
|
|
1332
|
+
status: "active",
|
|
1333
|
+
weeksCount: DEFAULT_WEEKS,
|
|
1334
|
+
startDate: toISODate(mondayOfCurrentWeek())
|
|
1335
|
+
});
|
|
1336
|
+
};
|
|
1337
|
+
const createPost = async (week) => {
|
|
1338
|
+
const targetPlan = await ensurePlan();
|
|
1339
|
+
if (!targetPlan) return;
|
|
1340
|
+
const start = targetPlan.startDate ? /* @__PURE__ */ new Date(`${targetPlan.startDate}T00:00:00`) : mondayOfCurrentWeek();
|
|
1341
|
+
start.setDate(start.getDate() + (week - 1) * 7);
|
|
1342
|
+
const post = await savePost({
|
|
1343
|
+
planId: targetPlan.id,
|
|
1344
|
+
weekNumber: week,
|
|
1345
|
+
title: "",
|
|
1346
|
+
format: "reel",
|
|
1347
|
+
status: "idea",
|
|
1348
|
+
scheduledDate: toISODate(start)
|
|
1349
|
+
});
|
|
1350
|
+
onOpenPost(post.id);
|
|
1351
|
+
};
|
|
1352
|
+
const movePost = async (postId, week) => {
|
|
1353
|
+
const post = posts.find((p) => p.id === postId);
|
|
1354
|
+
if (!post || post.weekNumber === week) return;
|
|
1355
|
+
await savePost({ id: postId, weekNumber: week });
|
|
1356
|
+
};
|
|
1357
|
+
const createPostAt = async (isoDate) => {
|
|
1358
|
+
const targetPlan = await ensurePlan();
|
|
1359
|
+
if (!targetPlan) return;
|
|
1360
|
+
let week = 1;
|
|
1361
|
+
if (targetPlan.startDate) {
|
|
1362
|
+
const start = /* @__PURE__ */ new Date(`${targetPlan.startDate}T00:00:00`);
|
|
1363
|
+
const day = /* @__PURE__ */ new Date(`${isoDate}T00:00:00`);
|
|
1364
|
+
const diff = Math.floor((day.getTime() - start.getTime()) / (7 * 24 * 3600 * 1e3)) + 1;
|
|
1365
|
+
week = Math.min(Math.max(diff, 1), targetPlan.weeksCount);
|
|
1366
|
+
}
|
|
1367
|
+
const post = await savePost({
|
|
1368
|
+
planId: targetPlan.id,
|
|
1369
|
+
weekNumber: week,
|
|
1370
|
+
title: "",
|
|
1371
|
+
format: "reel",
|
|
1372
|
+
status: "idea",
|
|
1373
|
+
scheduledDate: isoDate
|
|
1374
|
+
});
|
|
1375
|
+
onOpenPost(post.id);
|
|
1376
|
+
};
|
|
1377
|
+
const busy = !loaded || loading;
|
|
1378
|
+
const hasAccount = accounts.length > 0;
|
|
1379
|
+
const weeksCount = plan?.weeksCount ?? DEFAULT_WEEKS;
|
|
1380
|
+
const boardStart = plan?.startDate ? /* @__PURE__ */ new Date(`${plan.startDate}T00:00:00`) : mondayOfCurrentWeek();
|
|
1381
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-6xl space-y-6", children: [
|
|
1382
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
|
|
1383
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
|
|
1384
|
+
/* @__PURE__ */ jsx(AccountSelector, {}),
|
|
1385
|
+
plan && /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: plan.name })
|
|
1386
|
+
] }),
|
|
1387
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1388
|
+
/* @__PURE__ */ jsx(ViewSwitcher, { mode: viewMode, onChange: setViewMode }),
|
|
1389
|
+
plan && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => setBriefOpen(true), children: [
|
|
1390
|
+
/* @__PURE__ */ jsx(NotebookPen, { className: "mr-1.5 h-4 w-4" }),
|
|
1391
|
+
t("marketing.content.brief")
|
|
1392
|
+
] })
|
|
1393
|
+
] })
|
|
1394
|
+
] }),
|
|
1395
|
+
!busy && !hasAccount && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.content.noAccountsHint") }),
|
|
1396
|
+
busy && viewMode === "board" && /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4", children: Array.from({ length: 8 }, (_, i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-44" }, i)) }),
|
|
1397
|
+
busy && viewMode === "calendar" && /* @__PURE__ */ jsx(Skeleton, { className: "h-96 w-full" }),
|
|
1398
|
+
busy && viewMode === "list" && /* @__PURE__ */ jsx("div", { className: "space-y-2", children: Array.from({ length: 6 }, (_, i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }, i)) }),
|
|
1399
|
+
!busy && viewMode === "board" && /* @__PURE__ */ jsx(
|
|
1400
|
+
BoardView,
|
|
1401
|
+
{
|
|
1402
|
+
startDate: boardStart,
|
|
1403
|
+
weeksCount,
|
|
1404
|
+
postsByWeek,
|
|
1405
|
+
onOpenPost,
|
|
1406
|
+
onNewPost: (w) => void createPost(w),
|
|
1407
|
+
onDeletePost: setPendingDeleteId,
|
|
1408
|
+
onDropPost: (id, w) => void movePost(id, w),
|
|
1409
|
+
disabled: !hasAccount
|
|
1410
|
+
}
|
|
1411
|
+
),
|
|
1412
|
+
!busy && viewMode === "calendar" && /* @__PURE__ */ jsx(
|
|
1413
|
+
CalendarView,
|
|
1414
|
+
{
|
|
1415
|
+
posts,
|
|
1416
|
+
onOpenPost,
|
|
1417
|
+
onNewPostAt: (iso) => void createPostAt(iso),
|
|
1418
|
+
disabled: !hasAccount
|
|
1419
|
+
}
|
|
1420
|
+
),
|
|
1421
|
+
!busy && viewMode === "list" && /* @__PURE__ */ jsx(ListView, { posts, onOpenPost }),
|
|
1422
|
+
/* @__PURE__ */ jsx(PlanBriefSheet, { open: briefOpen, onClose: () => setBriefOpen(false) }),
|
|
1423
|
+
/* @__PURE__ */ jsx(
|
|
1424
|
+
ConfirmDialog,
|
|
1425
|
+
{
|
|
1426
|
+
open: !!pendingDeleteId,
|
|
1427
|
+
variant: "destructive",
|
|
1428
|
+
title: t("marketing.content.deletePost"),
|
|
1429
|
+
description: t("marketing.content.deletePostConfirm"),
|
|
1430
|
+
confirmLabel: t("marketing.content.deletePost"),
|
|
1431
|
+
cancelLabel: t("marketing.content.cancel"),
|
|
1432
|
+
onCancel: () => setPendingDeleteId(null),
|
|
1433
|
+
onConfirm: () => {
|
|
1434
|
+
const id = pendingDeleteId;
|
|
1435
|
+
setPendingDeleteId(null);
|
|
1436
|
+
if (id) void deletePost(id);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
)
|
|
1440
|
+
] });
|
|
1441
|
+
}
|
|
1442
|
+
function canUploadMedia() {
|
|
1443
|
+
return !!getSupabaseClientOptional();
|
|
1444
|
+
}
|
|
1445
|
+
async function uploadPostMedia(postId, file) {
|
|
1446
|
+
const supabase = getSupabaseClientOptional();
|
|
1447
|
+
if (!supabase) throw new Error("Supabase not initialized");
|
|
1448
|
+
const tenantId = getActiveTenantId();
|
|
1449
|
+
if (!tenantId) throw new Error("No active tenant");
|
|
1450
|
+
const ext = file.name.split(".").pop()?.toLowerCase() ?? "bin";
|
|
1451
|
+
const path = `content/${tenantId}/${postId}-${Date.now()}.${ext}`;
|
|
1452
|
+
const { error } = await supabase.storage.from("mkt-media").upload(path, file, { upsert: true });
|
|
1453
|
+
if (error) throw error;
|
|
1454
|
+
const { data } = supabase.storage.from("mkt-media").getPublicUrl(path);
|
|
1455
|
+
return data.publicUrl;
|
|
1456
|
+
}
|
|
1457
|
+
function newId() {
|
|
1458
|
+
return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
1459
|
+
}
|
|
1460
|
+
function RecordingChecklist({ post }) {
|
|
1461
|
+
const t = useTranslation();
|
|
1462
|
+
const savePost = useContentPlannerStore((s) => s.savePost);
|
|
1463
|
+
const [items, setItems] = React8.useState(post.checklist);
|
|
1464
|
+
const [text, setText] = React8.useState("");
|
|
1465
|
+
React8.useEffect(() => {
|
|
1466
|
+
setItems(post.checklist);
|
|
1467
|
+
}, [post.id]);
|
|
1468
|
+
const persist = (next) => {
|
|
1469
|
+
setItems(next);
|
|
1470
|
+
void savePost({ id: post.id, checklist: next });
|
|
1471
|
+
};
|
|
1472
|
+
const toggle = (id) => persist(items.map((i) => i.id === id ? { ...i, done: !i.done } : i));
|
|
1473
|
+
const remove = (id) => persist(items.filter((i) => i.id !== id));
|
|
1474
|
+
const add = () => {
|
|
1475
|
+
const value = text.trim();
|
|
1476
|
+
if (!value) return;
|
|
1477
|
+
persist([...items, { id: newId(), text: value, done: false }]);
|
|
1478
|
+
setText("");
|
|
1479
|
+
};
|
|
1480
|
+
const generateFromScript = () => {
|
|
1481
|
+
const takes = [...post.contentMd.matchAll(/^##\s+(TAKE\s+.+)$/gim)].map(
|
|
1482
|
+
(m) => m[1].trim()
|
|
1483
|
+
);
|
|
1484
|
+
const existing = new Set(items.map((i) => i.text));
|
|
1485
|
+
const fresh = takes.filter((take) => !existing.has(take)).map((take) => ({ id: newId(), text: take, done: false }));
|
|
1486
|
+
if (fresh.length > 0) persist([...items, ...fresh]);
|
|
1487
|
+
};
|
|
1488
|
+
const hasTakes = /^##\s+TAKE\s+/im.test(post.contentMd);
|
|
1489
|
+
const done = items.filter((i) => i.done).length;
|
|
1490
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
1491
|
+
items.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
1492
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [
|
|
1493
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
1494
|
+
done,
|
|
1495
|
+
"/",
|
|
1496
|
+
items.length
|
|
1497
|
+
] }),
|
|
1498
|
+
hasTakes && /* @__PURE__ */ jsxs(
|
|
1499
|
+
"button",
|
|
1500
|
+
{
|
|
1501
|
+
type: "button",
|
|
1502
|
+
onClick: generateFromScript,
|
|
1503
|
+
className: "flex items-center gap-1 font-medium text-muted-foreground transition-colors hover:text-foreground",
|
|
1504
|
+
children: [
|
|
1505
|
+
/* @__PURE__ */ jsx(Sparkles, { className: "h-3 w-3" }),
|
|
1506
|
+
t("marketing.content.checklistGenerate")
|
|
1507
|
+
]
|
|
1508
|
+
}
|
|
1509
|
+
)
|
|
1510
|
+
] }),
|
|
1511
|
+
/* @__PURE__ */ jsx("div", { className: "h-1 overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ jsx(
|
|
1512
|
+
"div",
|
|
1513
|
+
{
|
|
1514
|
+
className: "h-full rounded-full bg-primary transition-[width]",
|
|
1515
|
+
style: { width: items.length ? `${done / items.length * 100}%` : 0 }
|
|
1516
|
+
}
|
|
1517
|
+
) })
|
|
1518
|
+
] }),
|
|
1519
|
+
items.length === 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-3 rounded-card border border-dashed border-border p-3 text-center", children: [
|
|
1520
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.content.checklistEmpty") }),
|
|
1521
|
+
hasTakes && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: generateFromScript, children: [
|
|
1522
|
+
/* @__PURE__ */ jsx(Sparkles, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
1523
|
+
t("marketing.content.checklistGenerate")
|
|
1524
|
+
] })
|
|
1525
|
+
] }),
|
|
1526
|
+
/* @__PURE__ */ jsx("ul", { className: "space-y-1", children: items.map((item) => /* @__PURE__ */ jsxs("li", { className: "group/item flex items-start gap-2.5 rounded-button px-1 py-2 transition-colors hover:bg-muted/40", children: [
|
|
1527
|
+
/* @__PURE__ */ jsxs("label", { className: "flex min-w-0 flex-1 cursor-pointer items-start gap-2.5", children: [
|
|
1528
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: item.done, onChange: () => toggle(item.id), className: "mt-0.5" }),
|
|
1529
|
+
/* @__PURE__ */ jsx(
|
|
1530
|
+
"span",
|
|
1531
|
+
{
|
|
1532
|
+
className: cn(
|
|
1533
|
+
"min-w-0 flex-1 text-sm leading-snug",
|
|
1534
|
+
item.done ? "text-muted-foreground line-through" : "text-foreground"
|
|
1535
|
+
),
|
|
1536
|
+
children: item.text
|
|
1537
|
+
}
|
|
1538
|
+
)
|
|
1539
|
+
] }),
|
|
1540
|
+
/* @__PURE__ */ jsx(
|
|
1541
|
+
"button",
|
|
1542
|
+
{
|
|
1543
|
+
type: "button",
|
|
1544
|
+
"aria-label": t("marketing.content.cancel"),
|
|
1545
|
+
onClick: () => remove(item.id),
|
|
1546
|
+
className: "rounded p-0.5 text-muted-foreground/50 opacity-0 transition-opacity hover:text-destructive group-hover/item:opacity-100 focus:opacity-100",
|
|
1547
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
|
|
1548
|
+
}
|
|
1549
|
+
)
|
|
1550
|
+
] }, item.id)) }),
|
|
1551
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1552
|
+
/* @__PURE__ */ jsx(
|
|
1553
|
+
Input,
|
|
1554
|
+
{
|
|
1555
|
+
value: text,
|
|
1556
|
+
onChange: (e) => setText(e.target.value),
|
|
1557
|
+
placeholder: t("marketing.content.checklistAddPlaceholder"),
|
|
1558
|
+
onKeyDown: (e) => {
|
|
1559
|
+
if (e.key === "Enter") add();
|
|
1560
|
+
},
|
|
1561
|
+
className: "h-8 text-sm"
|
|
1562
|
+
}
|
|
1563
|
+
),
|
|
1564
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: add, disabled: !text.trim(), "aria-label": t("marketing.content.checklistAdd"), children: /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }) })
|
|
1565
|
+
] })
|
|
1566
|
+
] });
|
|
1567
|
+
}
|
|
1568
|
+
function toCaption(md) {
|
|
1569
|
+
return md.replace(/^#+\s*/gm, "").replace(/\*\*/g, "").replace(/^[-*]\s+/gm, "").trim();
|
|
1570
|
+
}
|
|
1571
|
+
function isVideo(url) {
|
|
1572
|
+
return !!url && /\.(mp4|webm|mov)($|\?)/i.test(url);
|
|
1573
|
+
}
|
|
1574
|
+
function Media({ mediaUrl, className }) {
|
|
1575
|
+
if (!mediaUrl) {
|
|
1576
|
+
return /* @__PURE__ */ jsx("div", { className: cn("flex aspect-square w-full items-center justify-center bg-muted/40", className), children: /* @__PURE__ */ jsx(Image, { className: "h-8 w-8 text-muted-foreground/40" }) });
|
|
1577
|
+
}
|
|
1578
|
+
return isVideo(mediaUrl) ? /* @__PURE__ */ jsx("video", { src: mediaUrl, controls: true, className: cn("w-full bg-black object-contain", className) }) : /* @__PURE__ */ jsx("img", { src: mediaUrl, alt: "", className: cn("w-full object-cover", className) });
|
|
1579
|
+
}
|
|
1580
|
+
function Avatar({ name, ring }) {
|
|
1581
|
+
return /* @__PURE__ */ jsx(
|
|
1582
|
+
"span",
|
|
1583
|
+
{
|
|
1584
|
+
className: cn(
|
|
1585
|
+
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold text-foreground",
|
|
1586
|
+
ring && "ring-2 ring-offset-2 ring-pink-500/60"
|
|
1587
|
+
),
|
|
1588
|
+
children: (name.replace(/^@/, "")[0] ?? "?").toUpperCase()
|
|
1589
|
+
}
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
function InstagramFrame({ caption, mediaUrl, accountName, accountHandle }) {
|
|
1593
|
+
const t = useTranslation();
|
|
1594
|
+
const handle = (accountHandle || accountName).replace(/^@/, "");
|
|
1595
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-sm overflow-hidden rounded-card border border-border bg-card", children: [
|
|
1596
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2.5", children: [
|
|
1597
|
+
/* @__PURE__ */ jsx(Avatar, { name: handle, ring: true }),
|
|
1598
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 truncate text-sm font-semibold text-foreground", children: handle }),
|
|
1599
|
+
/* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4 text-muted-foreground" })
|
|
1600
|
+
] }),
|
|
1601
|
+
/* @__PURE__ */ jsx(Media, { mediaUrl }),
|
|
1602
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2 px-3 py-2.5", children: [
|
|
1603
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4 text-foreground", children: [
|
|
1604
|
+
/* @__PURE__ */ jsx(Heart, { className: "h-6 w-6" }),
|
|
1605
|
+
/* @__PURE__ */ jsx(MessageCircle, { className: "h-6 w-6" }),
|
|
1606
|
+
/* @__PURE__ */ jsx(Send, { className: "h-6 w-6" }),
|
|
1607
|
+
/* @__PURE__ */ jsx(Bookmark, { className: "ml-auto h-6 w-6" })
|
|
1608
|
+
] }),
|
|
1609
|
+
/* @__PURE__ */ jsxs("p", { className: "whitespace-pre-wrap text-sm leading-snug text-foreground", children: [
|
|
1610
|
+
/* @__PURE__ */ jsx("span", { className: "mr-1.5 font-semibold", children: handle }),
|
|
1611
|
+
toCaption(caption)
|
|
1612
|
+
] }),
|
|
1613
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: t("marketing.content.previewNow") })
|
|
1614
|
+
] })
|
|
1615
|
+
] });
|
|
1616
|
+
}
|
|
1617
|
+
function FacebookFrame({ caption, mediaUrl, accountName }) {
|
|
1618
|
+
const t = useTranslation();
|
|
1619
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-sm overflow-hidden rounded-card border border-border bg-card", children: [
|
|
1620
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2.5", children: [
|
|
1621
|
+
/* @__PURE__ */ jsx(Avatar, { name: accountName }),
|
|
1622
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
1623
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: accountName.replace(/^@/, "") }),
|
|
1624
|
+
/* @__PURE__ */ jsxs("p", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
|
|
1625
|
+
t("marketing.content.previewNow"),
|
|
1626
|
+
" \xB7 ",
|
|
1627
|
+
/* @__PURE__ */ jsx(Globe, { className: "h-3 w-3" })
|
|
1628
|
+
] })
|
|
1629
|
+
] }),
|
|
1630
|
+
/* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4 text-muted-foreground" })
|
|
1631
|
+
] }),
|
|
1632
|
+
/* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap px-3 pb-2.5 text-sm leading-snug text-foreground", children: toCaption(caption) }),
|
|
1633
|
+
/* @__PURE__ */ jsx(Media, { mediaUrl, className: "aspect-auto" }),
|
|
1634
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-around border-t border-border px-3 py-2 text-muted-foreground", children: [
|
|
1635
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium", children: [
|
|
1636
|
+
/* @__PURE__ */ jsx(ThumbsUp, { className: "h-4 w-4" }),
|
|
1637
|
+
" ",
|
|
1638
|
+
t("marketing.content.previewLike")
|
|
1639
|
+
] }),
|
|
1640
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium", children: [
|
|
1641
|
+
/* @__PURE__ */ jsx(MessageCircle, { className: "h-4 w-4" }),
|
|
1642
|
+
" ",
|
|
1643
|
+
t("marketing.content.previewComment")
|
|
1644
|
+
] }),
|
|
1645
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium", children: [
|
|
1646
|
+
/* @__PURE__ */ jsx(Share2, { className: "h-4 w-4" }),
|
|
1647
|
+
" ",
|
|
1648
|
+
t("marketing.content.previewShare")
|
|
1649
|
+
] })
|
|
1650
|
+
] })
|
|
1651
|
+
] });
|
|
1652
|
+
}
|
|
1653
|
+
function LinkedinFrame({ caption, mediaUrl, accountName }) {
|
|
1654
|
+
const t = useTranslation();
|
|
1655
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-sm overflow-hidden rounded-card border border-border bg-card", children: [
|
|
1656
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2.5", children: [
|
|
1657
|
+
/* @__PURE__ */ jsx(Avatar, { name: accountName }),
|
|
1658
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
1659
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: accountName.replace(/^@/, "") }),
|
|
1660
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.content.previewNow") })
|
|
1661
|
+
] })
|
|
1662
|
+
] }),
|
|
1663
|
+
/* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap px-3 pb-2.5 text-sm leading-snug text-foreground", children: toCaption(caption) }),
|
|
1664
|
+
/* @__PURE__ */ jsx(Media, { mediaUrl, className: "aspect-auto" }),
|
|
1665
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-around border-t border-border px-3 py-2 text-muted-foreground", children: [
|
|
1666
|
+
/* @__PURE__ */ jsx(ThumbsUp, { className: "h-4 w-4" }),
|
|
1667
|
+
/* @__PURE__ */ jsx(MessageCircle, { className: "h-4 w-4" }),
|
|
1668
|
+
/* @__PURE__ */ jsx(Repeat2, { className: "h-4 w-4" }),
|
|
1669
|
+
/* @__PURE__ */ jsx(Send, { className: "h-4 w-4" })
|
|
1670
|
+
] })
|
|
1671
|
+
] });
|
|
1672
|
+
}
|
|
1673
|
+
function XFrame({ caption, mediaUrl, accountName, accountHandle }) {
|
|
1674
|
+
const t = useTranslation();
|
|
1675
|
+
const handle = (accountHandle || accountName).replace(/^@/, "");
|
|
1676
|
+
return /* @__PURE__ */ jsx("div", { className: "mx-auto w-full max-w-sm rounded-card border border-border bg-card p-3", children: /* @__PURE__ */ jsxs("div", { className: "flex gap-2.5", children: [
|
|
1677
|
+
/* @__PURE__ */ jsx(Avatar, { name: handle }),
|
|
1678
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
1679
|
+
/* @__PURE__ */ jsxs("p", { className: "truncate text-sm text-foreground", children: [
|
|
1680
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold", children: accountName.replace(/^@/, "") }),
|
|
1681
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
1682
|
+
" @",
|
|
1683
|
+
handle,
|
|
1684
|
+
" \xB7 ",
|
|
1685
|
+
t("marketing.content.previewNow")
|
|
1686
|
+
] })
|
|
1687
|
+
] }),
|
|
1688
|
+
/* @__PURE__ */ jsx("p", { className: "mt-0.5 whitespace-pre-wrap text-sm leading-snug text-foreground", children: toCaption(caption) }),
|
|
1689
|
+
/* @__PURE__ */ jsx("div", { className: "mt-2 overflow-hidden rounded-xl border border-border", children: /* @__PURE__ */ jsx(Media, { mediaUrl, className: "aspect-auto" }) }),
|
|
1690
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center justify-between pr-6 text-muted-foreground", children: [
|
|
1691
|
+
/* @__PURE__ */ jsx(MessageCircle, { className: "h-4 w-4" }),
|
|
1692
|
+
/* @__PURE__ */ jsx(Repeat2, { className: "h-4 w-4" }),
|
|
1693
|
+
/* @__PURE__ */ jsx(Heart, { className: "h-4 w-4" }),
|
|
1694
|
+
/* @__PURE__ */ jsx(BarChart2, { className: "h-4 w-4" }),
|
|
1695
|
+
/* @__PURE__ */ jsx(Share2, { className: "h-4 w-4" })
|
|
1696
|
+
] })
|
|
1697
|
+
] })
|
|
1698
|
+
] }) });
|
|
1699
|
+
}
|
|
1700
|
+
var FRAMES = {
|
|
1701
|
+
instagram: InstagramFrame,
|
|
1702
|
+
facebook: FacebookFrame,
|
|
1703
|
+
linkedin: LinkedinFrame,
|
|
1704
|
+
x: XFrame
|
|
1705
|
+
};
|
|
1706
|
+
function PostSocialPreview(props) {
|
|
1707
|
+
const platforms = props.platforms.length > 0 ? props.platforms : ["instagram"];
|
|
1708
|
+
const [selected, setSelected] = React8.useState(platforms[0]);
|
|
1709
|
+
const active = platforms.includes(selected) ? selected : platforms[0];
|
|
1710
|
+
const Frame = FRAMES[active] ?? InstagramFrame;
|
|
1711
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-3 rounded-md border border-border bg-muted/20 p-4", children: [
|
|
1712
|
+
platforms.length > 1 && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center gap-1", children: platforms.map((platform) => /* @__PURE__ */ jsxs(
|
|
1713
|
+
"button",
|
|
1714
|
+
{
|
|
1715
|
+
type: "button",
|
|
1716
|
+
onClick: () => setSelected(platform),
|
|
1717
|
+
"aria-pressed": active === platform,
|
|
1718
|
+
className: cn(
|
|
1719
|
+
"flex items-center gap-1.5 rounded-button px-2.5 py-1.5 text-xs font-medium capitalize transition-colors",
|
|
1720
|
+
active === platform ? "bg-card text-foreground shadow-button" : "text-muted-foreground hover:text-foreground"
|
|
1721
|
+
),
|
|
1722
|
+
children: [
|
|
1723
|
+
/* @__PURE__ */ jsx(PlatformIcon, { platform, className: "h-3.5 w-3.5" }),
|
|
1724
|
+
platform
|
|
1725
|
+
]
|
|
1726
|
+
},
|
|
1727
|
+
platform
|
|
1728
|
+
)) }),
|
|
1729
|
+
/* @__PURE__ */ jsx(Frame, { ...props })
|
|
1730
|
+
] });
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// src/views/content/templates.ts
|
|
1734
|
+
var reel = (title) => `# ${title}
|
|
1735
|
+
|
|
1736
|
+
**TEMA:** ${title.toUpperCase()}
|
|
1737
|
+
**Formato:** Reel \xB7 **Dura\xE7\xE3o:** 45 a 60 segundos
|
|
1738
|
+
|
|
1739
|
+
---
|
|
1740
|
+
|
|
1741
|
+
## TAKE 1 \u2013 GANCHO (0s a 5s)
|
|
1742
|
+
|
|
1743
|
+
*Dire\xE7\xE3o de cena: olhando para a c\xE2mera, energia alta.*
|
|
1744
|
+
|
|
1745
|
+
**Texto na tela:** \u2026
|
|
1746
|
+
|
|
1747
|
+
**Fala:** "\u2026"
|
|
1748
|
+
|
|
1749
|
+
---
|
|
1750
|
+
|
|
1751
|
+
## TAKE 2 \u2013 DESENVOLVIMENTO (5s a 40s)
|
|
1752
|
+
|
|
1753
|
+
*Dire\xE7\xE3o de cena: \u2026*
|
|
1754
|
+
|
|
1755
|
+
**Texto na tela:** \u2026
|
|
1756
|
+
|
|
1757
|
+
**Fala:** "\u2026"
|
|
1758
|
+
|
|
1759
|
+
---
|
|
1760
|
+
|
|
1761
|
+
## TAKE 3 \u2013 CTA (40s a 60s)
|
|
1762
|
+
|
|
1763
|
+
**Fala:** "\u2026"
|
|
1764
|
+
|
|
1765
|
+
**CTA:** \u2026
|
|
1766
|
+
`;
|
|
1767
|
+
var staticPost = (title) => `# ${title}
|
|
1768
|
+
|
|
1769
|
+
**TEMA:** ${title.toUpperCase()}
|
|
1770
|
+
**Formato:** Est\xE1tico
|
|
1771
|
+
|
|
1772
|
+
## Texto da arte
|
|
1773
|
+
|
|
1774
|
+
\u2026
|
|
1775
|
+
|
|
1776
|
+
## Legenda
|
|
1777
|
+
|
|
1778
|
+
\u2026
|
|
1779
|
+
|
|
1780
|
+
**CTA:** \u2026
|
|
1781
|
+
`;
|
|
1782
|
+
var carousel = (title) => `# ${title}
|
|
1783
|
+
|
|
1784
|
+
**TEMA:** ${title.toUpperCase()}
|
|
1785
|
+
**Formato:** Carrossel
|
|
1786
|
+
|
|
1787
|
+
## Slide 1 \u2014 Capa (gancho)
|
|
1788
|
+
|
|
1789
|
+
\u2026
|
|
1790
|
+
|
|
1791
|
+
## Slide 2
|
|
1792
|
+
|
|
1793
|
+
\u2026
|
|
1794
|
+
|
|
1795
|
+
## Slide 3
|
|
1796
|
+
|
|
1797
|
+
\u2026
|
|
1798
|
+
|
|
1799
|
+
## \xDAltimo slide \u2014 CTA
|
|
1800
|
+
|
|
1801
|
+
\u2026
|
|
1802
|
+
`;
|
|
1803
|
+
var story = (title) => `# ${title}
|
|
1804
|
+
|
|
1805
|
+
**TEMA:** ${title.toUpperCase()}
|
|
1806
|
+
**Formato:** Story
|
|
1807
|
+
|
|
1808
|
+
## Frame 1
|
|
1809
|
+
|
|
1810
|
+
\u2026
|
|
1811
|
+
|
|
1812
|
+
## Frame 2
|
|
1813
|
+
|
|
1814
|
+
\u2026
|
|
1815
|
+
|
|
1816
|
+
## Frame final \u2014 CTA
|
|
1817
|
+
|
|
1818
|
+
*Sticker de link / caixa de pergunta / enquete.*
|
|
1819
|
+
|
|
1820
|
+
\u2026
|
|
1821
|
+
`;
|
|
1822
|
+
var video = (title) => `# ${title}
|
|
1823
|
+
|
|
1824
|
+
**TEMA:** ${title.toUpperCase()}
|
|
1825
|
+
**Formato:** V\xEDdeo longo (YouTube) \xB7 **Dura\xE7\xE3o:** 8 a 12 minutos
|
|
1826
|
+
|
|
1827
|
+
## Gancho (0s a 30s)
|
|
1828
|
+
|
|
1829
|
+
\u2026
|
|
1830
|
+
|
|
1831
|
+
## Roteiro por blocos
|
|
1832
|
+
|
|
1833
|
+
### Bloco 1 \u2014 Contexto
|
|
1834
|
+
|
|
1835
|
+
\u2026
|
|
1836
|
+
|
|
1837
|
+
### Bloco 2 \u2014 Desenvolvimento
|
|
1838
|
+
|
|
1839
|
+
\u2026
|
|
1840
|
+
|
|
1841
|
+
### Bloco 3 \u2014 Fechamento
|
|
1842
|
+
|
|
1843
|
+
\u2026
|
|
1844
|
+
|
|
1845
|
+
**CTA:** inscreva-se / comente / link na descri\xE7\xE3o
|
|
1846
|
+
`;
|
|
1847
|
+
var live = (title) => `# ${title}
|
|
1848
|
+
|
|
1849
|
+
**TEMA:** ${title.toUpperCase()}
|
|
1850
|
+
**Formato:** Live
|
|
1851
|
+
|
|
1852
|
+
## Pauta
|
|
1853
|
+
|
|
1854
|
+
1. Abertura e aquecimento (5 min)
|
|
1855
|
+
2. \u2026
|
|
1856
|
+
3. Perguntas e respostas (15 min)
|
|
1857
|
+
|
|
1858
|
+
## Avisos / CTAs durante a live
|
|
1859
|
+
|
|
1860
|
+
\u2026
|
|
1861
|
+
`;
|
|
1862
|
+
var article = (title) => `# ${title}
|
|
1863
|
+
|
|
1864
|
+
**TEMA:** ${title.toUpperCase()}
|
|
1865
|
+
**Formato:** Artigo / Newsletter
|
|
1866
|
+
|
|
1867
|
+
## T\xEDtulo de trabalho
|
|
1868
|
+
|
|
1869
|
+
\u2026
|
|
1870
|
+
|
|
1871
|
+
## Estrutura
|
|
1872
|
+
|
|
1873
|
+
- Abertura (dor / promessa)
|
|
1874
|
+
- Desenvolvimento em 3 pontos
|
|
1875
|
+
- Conclus\xE3o + CTA
|
|
1876
|
+
|
|
1877
|
+
## Rascunho
|
|
1878
|
+
|
|
1879
|
+
\u2026
|
|
1880
|
+
`;
|
|
1881
|
+
var POST_TEMPLATES = {
|
|
1882
|
+
reel,
|
|
1883
|
+
static: staticPost,
|
|
1884
|
+
carousel,
|
|
1885
|
+
story,
|
|
1886
|
+
video,
|
|
1887
|
+
live,
|
|
1888
|
+
article
|
|
1889
|
+
};
|
|
1890
|
+
function MediaAsset({ post, onChanged }) {
|
|
1891
|
+
const t = useTranslation();
|
|
1892
|
+
const inputRef = React8.useRef(null);
|
|
1893
|
+
const [busy, setBusy] = React8.useState(false);
|
|
1894
|
+
const isVideo2 = post.mediaUrl ? /\.(mp4|webm|mov)($|\?)/i.test(post.mediaUrl) : false;
|
|
1895
|
+
const pick = () => inputRef.current?.click();
|
|
1896
|
+
const upload = async (file) => {
|
|
1897
|
+
setBusy(true);
|
|
1898
|
+
try {
|
|
1899
|
+
const url = await uploadPostMedia(post.id, file);
|
|
1900
|
+
onChanged(url);
|
|
1901
|
+
} finally {
|
|
1902
|
+
setBusy(false);
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
if (!canUploadMedia()) {
|
|
1906
|
+
return /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.content.mediaUnavailable") });
|
|
1907
|
+
}
|
|
1908
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1909
|
+
/* @__PURE__ */ jsx(
|
|
1910
|
+
"input",
|
|
1911
|
+
{
|
|
1912
|
+
ref: inputRef,
|
|
1913
|
+
type: "file",
|
|
1914
|
+
accept: "image/*,video/*",
|
|
1915
|
+
className: "hidden",
|
|
1916
|
+
onChange: (e) => {
|
|
1917
|
+
const file = e.target.files?.[0];
|
|
1918
|
+
if (file) void upload(file);
|
|
1919
|
+
e.target.value = "";
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
),
|
|
1923
|
+
post.mediaUrl ? /* @__PURE__ */ jsxs("div", { className: "relative overflow-hidden rounded-card border border-border bg-muted/20", children: [
|
|
1924
|
+
isVideo2 ? /* @__PURE__ */ jsx("video", { src: post.mediaUrl, controls: true, className: "max-h-72 w-full object-contain" }) : /* @__PURE__ */ jsx("img", { src: post.mediaUrl, alt: "", className: "max-h-72 w-full object-contain" }),
|
|
1925
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-border p-2", children: [
|
|
1926
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: pick, disabled: busy, children: [
|
|
1927
|
+
busy ? /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(ImagePlus, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
1928
|
+
t("marketing.content.mediaReplace")
|
|
1929
|
+
] }),
|
|
1930
|
+
/* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => onChanged(null), disabled: busy, children: [
|
|
1931
|
+
/* @__PURE__ */ jsx(X, { className: "mr-1 h-3.5 w-3.5" }),
|
|
1932
|
+
t("marketing.content.mediaRemove")
|
|
352
1933
|
] })
|
|
353
1934
|
] })
|
|
354
|
-
] })
|
|
355
|
-
|
|
356
|
-
|
|
1935
|
+
] }) : /* @__PURE__ */ jsxs(
|
|
1936
|
+
"button",
|
|
1937
|
+
{
|
|
1938
|
+
type: "button",
|
|
1939
|
+
onClick: pick,
|
|
1940
|
+
disabled: busy,
|
|
1941
|
+
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",
|
|
1942
|
+
children: [
|
|
1943
|
+
busy ? /* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 animate-spin" }) : /* @__PURE__ */ jsx(ImagePlus, { className: "h-5 w-5" }),
|
|
1944
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: t("marketing.content.mediaUpload") }),
|
|
1945
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs", children: t("marketing.content.mediaHint") })
|
|
1946
|
+
]
|
|
1947
|
+
}
|
|
1948
|
+
)
|
|
357
1949
|
] });
|
|
358
1950
|
}
|
|
359
|
-
function
|
|
1951
|
+
function toDraft2(post) {
|
|
1952
|
+
return {
|
|
1953
|
+
title: post.title,
|
|
1954
|
+
format: post.format,
|
|
1955
|
+
status: post.status,
|
|
1956
|
+
scheduledDate: post.scheduledDate ?? "",
|
|
1957
|
+
platforms: post.platforms,
|
|
1958
|
+
hook: post.hook ?? "",
|
|
1959
|
+
cta: post.cta ?? "",
|
|
1960
|
+
contentMd: post.contentMd
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
function PostPage({ postId, onBack }) {
|
|
360
1964
|
const t = useTranslation();
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
1965
|
+
const provider = useContentPlannerProvider();
|
|
1966
|
+
const storePost = useContentPlannerStore((s) => s.posts.find((p) => p.id === postId));
|
|
1967
|
+
const savePost = useContentPlannerStore((s) => s.savePost);
|
|
1968
|
+
const deletePost = useContentPlannerStore((s) => s.deletePost);
|
|
1969
|
+
const accounts = useContentPlannerStore((s) => s.accounts);
|
|
1970
|
+
const activeAccountId = useContentPlannerStore((s) => s.activeAccountId);
|
|
1971
|
+
const [post, setPost] = React8.useState(storePost ?? null);
|
|
1972
|
+
const [draft, setDraft] = React8.useState(storePost ? toDraft2(storePost) : null);
|
|
1973
|
+
const [saving, setSaving] = React8.useState(false);
|
|
1974
|
+
const [confirmDelete, setConfirmDelete] = React8.useState(false);
|
|
1975
|
+
React8.useEffect(() => {
|
|
1976
|
+
if (post?.id === postId) return;
|
|
1977
|
+
if (storePost) {
|
|
1978
|
+
setPost(storePost);
|
|
1979
|
+
setDraft(toDraft2(storePost));
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
void provider.getPost(postId).then((fetched) => {
|
|
1983
|
+
if (fetched) {
|
|
1984
|
+
setPost(fetched);
|
|
1985
|
+
setDraft(toDraft2(fetched));
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1988
|
+
}, [postId, storePost, post?.id, provider]);
|
|
1989
|
+
if (!post || !draft) {
|
|
1990
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-3xl space-y-5", children: [
|
|
1991
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1992
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-56" }),
|
|
1993
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-40" })
|
|
370
1994
|
] }),
|
|
371
|
-
/* @__PURE__ */ jsx(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
1995
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }),
|
|
1996
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-3", children: Array.from({ length: 3 }, (_, i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-14" }, i)) }),
|
|
1997
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-64 w-full" })
|
|
1998
|
+
] });
|
|
1999
|
+
}
|
|
2000
|
+
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;
|
|
2001
|
+
const patch = (partial) => setDraft((d) => d ? { ...d, ...partial } : d);
|
|
2002
|
+
const save = async () => {
|
|
2003
|
+
setSaving(true);
|
|
2004
|
+
try {
|
|
2005
|
+
const updated = await savePost({
|
|
2006
|
+
id: post.id,
|
|
2007
|
+
title: draft.title,
|
|
2008
|
+
format: draft.format,
|
|
2009
|
+
status: draft.status,
|
|
2010
|
+
scheduledDate: draft.scheduledDate || null,
|
|
2011
|
+
platforms: draft.platforms,
|
|
2012
|
+
hook: draft.hook,
|
|
2013
|
+
cta: draft.cta,
|
|
2014
|
+
contentMd: draft.contentMd
|
|
2015
|
+
});
|
|
2016
|
+
setPost(updated);
|
|
2017
|
+
setDraft(toDraft2(updated));
|
|
2018
|
+
} finally {
|
|
2019
|
+
setSaving(false);
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
const insertTemplate = () => {
|
|
2023
|
+
patch({ contentMd: POST_TEMPLATES[draft.format](draft.title || t("marketing.content.untitled")) });
|
|
2024
|
+
};
|
|
2025
|
+
const isStatic = draft.format === "static";
|
|
2026
|
+
const account = accounts.find((a) => a.id === activeAccountId) ?? null;
|
|
2027
|
+
const now = /* @__PURE__ */ new Date();
|
|
2028
|
+
const todayISO = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
2029
|
+
const isReleaseDay = !!post.scheduledDate && post.scheduledDate === todayISO;
|
|
2030
|
+
const handleMediaChanged = (mediaUrl) => {
|
|
2031
|
+
void savePost({ id: post.id, mediaUrl }).then((updated) => {
|
|
2032
|
+
setPost(updated);
|
|
2033
|
+
setDraft((d) => d ? { ...d } : d);
|
|
2034
|
+
});
|
|
2035
|
+
};
|
|
2036
|
+
return /* @__PURE__ */ jsx(
|
|
2037
|
+
ContentSplit,
|
|
2038
|
+
{
|
|
2039
|
+
className: "mx-auto max-w-5xl",
|
|
2040
|
+
aside: /* @__PURE__ */ jsx(RecordingChecklist, { post }),
|
|
2041
|
+
asideTitle: t("marketing.content.checklistTitle"),
|
|
2042
|
+
asideDescription: t("marketing.content.checklistDescription"),
|
|
2043
|
+
collapsible: true,
|
|
2044
|
+
defaultCollapsed: !isReleaseDay,
|
|
2045
|
+
children: /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
|
|
2046
|
+
/* @__PURE__ */ jsx(
|
|
2047
|
+
SubpageHeader,
|
|
2048
|
+
{
|
|
2049
|
+
title: draft.title || t("marketing.content.untitled"),
|
|
2050
|
+
parentLabel: t("marketing.content.title"),
|
|
2051
|
+
onBack,
|
|
2052
|
+
actions: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2053
|
+
/* @__PURE__ */ jsx(ContentSplitTrigger, { label: t("marketing.content.checklistOpen") }),
|
|
2054
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => setConfirmDelete(true), children: t("marketing.content.deletePost") }),
|
|
2055
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => void save(), disabled: !dirty || saving, children: t("marketing.content.save") })
|
|
2056
|
+
] })
|
|
2057
|
+
}
|
|
2058
|
+
),
|
|
2059
|
+
/* @__PURE__ */ jsx(
|
|
2060
|
+
Input,
|
|
2061
|
+
{
|
|
2062
|
+
value: draft.title,
|
|
2063
|
+
onChange: (e) => patch({ title: e.target.value }),
|
|
2064
|
+
placeholder: t("marketing.content.titlePlaceholder"),
|
|
2065
|
+
className: "text-base font-medium"
|
|
2066
|
+
}
|
|
2067
|
+
),
|
|
2068
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-3", children: [
|
|
2069
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
2070
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.formatLabel") }),
|
|
2071
|
+
/* @__PURE__ */ jsxs(Select, { value: draft.format, onValueChange: (v) => patch({ format: v }), children: [
|
|
2072
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
2073
|
+
/* @__PURE__ */ jsx(SelectContent, { children: POST_FORMATS.map((f) => /* @__PURE__ */ jsx(SelectItem, { value: f, children: t(`marketing.content.format.${f}`) }, f)) })
|
|
390
2074
|
] })
|
|
391
2075
|
] }),
|
|
392
|
-
/* @__PURE__ */
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
2076
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
2077
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.statusLabel") }),
|
|
2078
|
+
/* @__PURE__ */ jsxs(Select, { value: draft.status, onValueChange: (v) => patch({ status: v }), children: [
|
|
2079
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
2080
|
+
/* @__PURE__ */ jsx(SelectContent, { children: POST_STATUS_ORDER.map((s) => /* @__PURE__ */ jsx(SelectItem, { value: s, children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
2081
|
+
/* @__PURE__ */ jsx(StatusDot, { status: s }),
|
|
2082
|
+
t(`marketing.content.status.${s}`)
|
|
2083
|
+
] }) }, s)) })
|
|
2084
|
+
] })
|
|
2085
|
+
] }),
|
|
2086
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
2087
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.scheduledDate") }),
|
|
2088
|
+
/* @__PURE__ */ jsx(
|
|
2089
|
+
Input,
|
|
2090
|
+
{
|
|
2091
|
+
type: "date",
|
|
2092
|
+
value: draft.scheduledDate,
|
|
2093
|
+
onChange: (e) => patch({ scheduledDate: e.target.value })
|
|
2094
|
+
}
|
|
2095
|
+
)
|
|
2096
|
+
] }),
|
|
2097
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1 sm:col-span-2", children: [
|
|
2098
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.hook") }),
|
|
2099
|
+
/* @__PURE__ */ jsx(
|
|
2100
|
+
Input,
|
|
2101
|
+
{
|
|
2102
|
+
value: draft.hook,
|
|
2103
|
+
onChange: (e) => patch({ hook: e.target.value }),
|
|
2104
|
+
placeholder: t("marketing.content.hookPlaceholder")
|
|
2105
|
+
}
|
|
2106
|
+
)
|
|
2107
|
+
] }),
|
|
2108
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
2109
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.cta") }),
|
|
2110
|
+
/* @__PURE__ */ jsx(
|
|
2111
|
+
Input,
|
|
2112
|
+
{
|
|
2113
|
+
value: draft.cta,
|
|
2114
|
+
onChange: (e) => patch({ cta: e.target.value }),
|
|
2115
|
+
placeholder: t("marketing.content.ctaPlaceholder")
|
|
2116
|
+
}
|
|
2117
|
+
)
|
|
2118
|
+
] }),
|
|
2119
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1 sm:col-span-3", children: [
|
|
2120
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.platformsLabel") }),
|
|
2121
|
+
/* @__PURE__ */ jsx(PlatformPicker, { value: draft.platforms, onChange: (platforms) => patch({ platforms }) })
|
|
2122
|
+
] })
|
|
399
2123
|
] }),
|
|
400
|
-
/* @__PURE__ */
|
|
2124
|
+
isStatic && /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
2125
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.mediaLabel") }),
|
|
2126
|
+
/* @__PURE__ */ jsx(MediaAsset, { post, onChanged: handleMediaChanged })
|
|
2127
|
+
] }),
|
|
2128
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
2129
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
2130
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: isStatic ? t("marketing.content.captionLabel") : t("marketing.content.scriptLabel") }),
|
|
2131
|
+
draft.contentMd.trim() === "" && /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: insertTemplate, children: t("marketing.content.insertTemplate") })
|
|
2132
|
+
] }),
|
|
2133
|
+
/* @__PURE__ */ jsx(
|
|
2134
|
+
MarkdownEditor,
|
|
2135
|
+
{
|
|
2136
|
+
value: draft.contentMd,
|
|
2137
|
+
onChange: (contentMd) => patch({ contentMd }),
|
|
2138
|
+
placeholder: isStatic ? t("marketing.content.captionPlaceholder") : t("marketing.content.bodyPlaceholder"),
|
|
2139
|
+
editLabel: t("marketing.content.edit"),
|
|
2140
|
+
previewLabel: t("marketing.content.preview"),
|
|
2141
|
+
renderPreview: isStatic ? (value) => /* @__PURE__ */ jsx(
|
|
2142
|
+
PostSocialPreview,
|
|
2143
|
+
{
|
|
2144
|
+
caption: value,
|
|
2145
|
+
mediaUrl: post.mediaUrl,
|
|
2146
|
+
accountName: account?.name ?? "@conta",
|
|
2147
|
+
accountHandle: account?.handle,
|
|
2148
|
+
platforms: draft.platforms.length > 0 ? draft.platforms : account?.platforms ?? []
|
|
2149
|
+
}
|
|
2150
|
+
) : void 0
|
|
2151
|
+
}
|
|
2152
|
+
)
|
|
2153
|
+
] }),
|
|
2154
|
+
/* @__PURE__ */ jsx(
|
|
2155
|
+
ConfirmDialog,
|
|
2156
|
+
{
|
|
2157
|
+
open: confirmDelete,
|
|
2158
|
+
variant: "destructive",
|
|
2159
|
+
title: t("marketing.content.deletePost"),
|
|
2160
|
+
description: t("marketing.content.deletePostConfirm"),
|
|
2161
|
+
confirmLabel: t("marketing.content.deletePost"),
|
|
2162
|
+
cancelLabel: t("marketing.content.cancel"),
|
|
2163
|
+
onCancel: () => setConfirmDelete(false),
|
|
2164
|
+
onConfirm: () => {
|
|
2165
|
+
setConfirmDelete(false);
|
|
2166
|
+
void deletePost(post.id).then(onBack);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
)
|
|
401
2170
|
] })
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
var TYPE_STYLE = {
|
|
406
|
-
Funnel: "bg-indigo-100 text-indigo-700",
|
|
407
|
-
"Landing page": "bg-pink-100 text-pink-700",
|
|
408
|
-
Website: "bg-sky-100 text-sky-700"
|
|
409
|
-
};
|
|
410
|
-
function LandingPagesView() {
|
|
411
|
-
const t = useTranslation();
|
|
412
|
-
const pages = useMarketingStore((s) => s.landingPages);
|
|
413
|
-
const totalVisits = pages.reduce((s, p) => s + p.visits, 0);
|
|
414
|
-
const totalConv = pages.reduce((s, p) => s + p.conversions, 0);
|
|
415
|
-
const avgCvr = totalVisits > 0 ? totalConv / totalVisits * 100 : 0;
|
|
416
|
-
const columns = React4.useMemo(() => [
|
|
417
|
-
{ accessorKey: "name", header: t("marketing.col.page"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
|
|
418
|
-
{ accessorKey: "type", header: t("marketing.col.type"), cell: ({ getValue }) => {
|
|
419
|
-
const v = getValue();
|
|
420
|
-
return /* @__PURE__ */ jsx("span", { className: `rounded-full px-2 py-0.5 text-xs font-medium ${TYPE_STYLE[v] ?? "bg-muted text-muted-foreground"}`, children: v });
|
|
421
|
-
} },
|
|
422
|
-
{ accessorKey: "visits", header: t("marketing.col.visits"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
|
|
423
|
-
{ accessorKey: "conversions", header: t("marketing.col.conversions"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatNumber(getValue()) }) },
|
|
424
|
-
{ accessorKey: "cvr", header: t("marketing.col.cvr"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatPercent(getValue()) }) }
|
|
425
|
-
], [t]);
|
|
426
|
-
return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
|
|
427
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
428
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.landing.subtitle") }),
|
|
429
|
-
/* @__PURE__ */ jsx(RangeTabs, {})
|
|
430
|
-
] }),
|
|
431
|
-
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-4", children: [
|
|
432
|
-
/* @__PURE__ */ jsx(KpiCard, { icon: MousePointerClick, label: t("marketing.metric.totalVisits"), value: formatNumber(totalVisits) }),
|
|
433
|
-
/* @__PURE__ */ jsx(KpiCard, { icon: Layers, label: t("marketing.metric.pages"), value: String(pages.length) }),
|
|
434
|
-
/* @__PURE__ */ jsx(KpiCard, { icon: Percent, label: t("marketing.metric.avgConversion"), value: formatPercent(avgCvr) })
|
|
435
|
-
] }),
|
|
436
|
-
/* @__PURE__ */ jsx(DataTable, { columns, data: pages, emptyMessage: t("marketing.landing.subtitle") })
|
|
437
|
-
] });
|
|
2171
|
+
},
|
|
2172
|
+
post.id
|
|
2173
|
+
);
|
|
438
2174
|
}
|
|
439
2175
|
function buildNav(config, view, navigate) {
|
|
440
2176
|
const items = [
|
|
@@ -458,9 +2194,18 @@ function buildNav(config, view, navigate) {
|
|
|
458
2194
|
if (config.modules.landingPages) {
|
|
459
2195
|
items.push({ id: "landing-pages", label: config.labels.landingPages, icon: "LayoutTemplate", active: view === "landing-pages", onClick: () => navigate("landing-pages") });
|
|
460
2196
|
}
|
|
2197
|
+
if (config.modules.contentPlanner) {
|
|
2198
|
+
items.push({
|
|
2199
|
+
id: "content",
|
|
2200
|
+
label: config.labels.content,
|
|
2201
|
+
icon: "Clapperboard",
|
|
2202
|
+
active: view === "content" || view === "content-brief" || view.startsWith("content-post:"),
|
|
2203
|
+
onClick: () => navigate("content")
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
461
2206
|
return items;
|
|
462
2207
|
}
|
|
463
|
-
function MarketingPage({ config, provider, store }) {
|
|
2208
|
+
function MarketingPage({ config, provider, store, contentProvider, contentStore }) {
|
|
464
2209
|
const t = useTranslation();
|
|
465
2210
|
const { view, direction, navigate } = useModuleNavigation("/marketing", {
|
|
466
2211
|
overview: 0,
|
|
@@ -468,14 +2213,17 @@ function MarketingPage({ config, provider, store }) {
|
|
|
468
2213
|
campaigns: 0,
|
|
469
2214
|
funnel: 0,
|
|
470
2215
|
"landing-pages": 0,
|
|
471
|
-
|
|
2216
|
+
content: 0,
|
|
2217
|
+
"channel-detail": 1,
|
|
2218
|
+
"content-post": 1,
|
|
2219
|
+
"content-brief": 1
|
|
472
2220
|
}, "overview");
|
|
473
|
-
|
|
2221
|
+
React8.useEffect(() => {
|
|
474
2222
|
void store.getState().load();
|
|
475
2223
|
}, [store]);
|
|
476
2224
|
const isOverview = view === "overview";
|
|
477
2225
|
const nav = buildNav(config, view, navigate);
|
|
478
|
-
const quickActions =
|
|
2226
|
+
const quickActions = React8.useMemo(() => [
|
|
479
2227
|
{ id: "new-campaign", label: t("marketing.campaigns.new"), icon: "Megaphone", description: config.labels.campaigns, action: () => navigate("campaigns") }
|
|
480
2228
|
], [t, config.labels.campaigns]);
|
|
481
2229
|
const renderView = createViewRouter([
|
|
@@ -484,9 +2232,12 @@ function MarketingPage({ config, provider, store }) {
|
|
|
484
2232
|
{ id: "campaigns", render: () => /* @__PURE__ */ jsx(CampaignsView, {}) },
|
|
485
2233
|
{ id: "funnel", render: () => /* @__PURE__ */ jsx(FunnelView, {}) },
|
|
486
2234
|
{ id: "landing-pages", render: () => /* @__PURE__ */ jsx(LandingPagesView, {}) },
|
|
2235
|
+
{ id: "content", render: () => /* @__PURE__ */ jsx(ContentView, { onOpenPost: (id) => navigate(`content-post:${id}`) }) },
|
|
2236
|
+
{ id: "content-post", render: ({ id }) => /* @__PURE__ */ jsx(PostPage, { postId: id, onBack: () => navigate("content") }) },
|
|
2237
|
+
{ id: "content-brief", render: () => /* @__PURE__ */ jsx(PlanBriefView, { onBack: () => navigate("content") }) },
|
|
487
2238
|
{ id: "overview", render: () => /* @__PURE__ */ jsx(OverviewView, {}) }
|
|
488
2239
|
], "overview");
|
|
489
|
-
return /* @__PURE__ */ jsx(MarketingContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(
|
|
2240
|
+
return /* @__PURE__ */ jsx(MarketingContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsx(
|
|
490
2241
|
ModulePage,
|
|
491
2242
|
{
|
|
492
2243
|
title: config.labels.pageTitle,
|
|
@@ -505,7 +2256,7 @@ function MarketingPage({ config, provider, store }) {
|
|
|
505
2256
|
),
|
|
506
2257
|
children: renderView(view)
|
|
507
2258
|
}
|
|
508
|
-
) });
|
|
2259
|
+
) }) });
|
|
509
2260
|
}
|
|
510
2261
|
var SOURCE_OPTIONS = [
|
|
511
2262
|
{ value: "crm", label: "CRM" },
|
|
@@ -516,11 +2267,11 @@ var SOURCE_OPTIONS = [
|
|
|
516
2267
|
function SettingsView() {
|
|
517
2268
|
const t = useTranslation();
|
|
518
2269
|
const { conversion, channels } = useMarketingConfig();
|
|
519
|
-
const [source, setSource] =
|
|
520
|
-
const [trackValue, setTrackValue] =
|
|
521
|
-
const [autoSync, setAutoSync] =
|
|
522
|
-
const [assisted, setAssisted] =
|
|
523
|
-
const [tracked, setTracked] =
|
|
2270
|
+
const [source, setSource] = React8.useState(conversion.source);
|
|
2271
|
+
const [trackValue, setTrackValue] = React8.useState(true);
|
|
2272
|
+
const [autoSync, setAutoSync] = React8.useState(true);
|
|
2273
|
+
const [assisted, setAssisted] = React8.useState(false);
|
|
2274
|
+
const [tracked, setTracked] = React8.useState(
|
|
524
2275
|
() => Object.fromEntries(channels.map((c) => [c.id, true]))
|
|
525
2276
|
);
|
|
526
2277
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
@@ -723,9 +2474,9 @@ function CampaignsTable() {
|
|
|
723
2474
|
const recent = campaigns.filter((c) => c.status !== "draft").slice(0, 5);
|
|
724
2475
|
return /* @__PURE__ */ jsx(TableWidget, { title: t("marketing.overview.recent"), icon: "Megaphone", columns, data: recent, emptyMessage: t("marketing.campaigns.empty") });
|
|
725
2476
|
}
|
|
726
|
-
function createMarketingDashboardWidgets(
|
|
2477
|
+
function createMarketingDashboardWidgets(ctx3) {
|
|
727
2478
|
const withCtx = (Inner) => {
|
|
728
|
-
const Wrapped = () => /* @__PURE__ */ jsx(MarketingContextProvider, { config:
|
|
2479
|
+
const Wrapped = () => /* @__PURE__ */ jsx(MarketingContextProvider, { config: ctx3.config, provider: ctx3.provider, store: ctx3.store, children: /* @__PURE__ */ jsx(Inner, {}) });
|
|
729
2480
|
Wrapped.displayName = `MarketingWidget(${Inner.displayName ?? Inner.name})`;
|
|
730
2481
|
return Wrapped;
|
|
731
2482
|
};
|
|
@@ -761,7 +2512,7 @@ var AGENCY = {
|
|
|
761
2512
|
{ id: "referral", label: "Referral", icon: "Users", kind: "referral" },
|
|
762
2513
|
{ id: "direct", label: "Direct", icon: "MousePointerClick", kind: "direct" }
|
|
763
2514
|
],
|
|
764
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, broadcasts: false, journeys: false }
|
|
2515
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, broadcasts: false, journeys: false }
|
|
765
2516
|
};
|
|
766
2517
|
var BEAUTY = {
|
|
767
2518
|
conversion: {
|
|
@@ -779,7 +2530,7 @@ var BEAUTY = {
|
|
|
779
2530
|
{ id: "whatsapp", label: "WhatsApp", icon: "MessageCircle", kind: "social" },
|
|
780
2531
|
{ id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
|
|
781
2532
|
],
|
|
782
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, broadcasts: false, journeys: false }
|
|
2533
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, broadcasts: false, journeys: false }
|
|
783
2534
|
};
|
|
784
2535
|
var RESTO = {
|
|
785
2536
|
conversion: {
|
|
@@ -797,7 +2548,7 @@ var RESTO = {
|
|
|
797
2548
|
{ id: "instagram", label: "Instagram", icon: "Instagram", kind: "social" },
|
|
798
2549
|
{ id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
|
|
799
2550
|
],
|
|
800
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, broadcasts: false, journeys: false }
|
|
2551
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, broadcasts: false, journeys: false }
|
|
801
2552
|
};
|
|
802
2553
|
var MARKETING_PRESETS = {
|
|
803
2554
|
agency: AGENCY,
|
|
@@ -936,7 +2687,7 @@ function createMockMarketingProvider(cfg) {
|
|
|
936
2687
|
const base = channelBase30(cfg.channels, cfg.conversion);
|
|
937
2688
|
const campaigns = seedCampaigns(cfg);
|
|
938
2689
|
const landing = seedLandingPages();
|
|
939
|
-
let
|
|
2690
|
+
let counter2 = 100;
|
|
940
2691
|
async function channelPerformance(query) {
|
|
941
2692
|
const list = base.map((b) => toPerformance(b, query.range));
|
|
942
2693
|
return query.channelId ? list.filter((c) => c.channelId === query.channelId) : list;
|
|
@@ -1001,7 +2752,7 @@ function createMockMarketingProvider(cfg) {
|
|
|
1001
2752
|
const b = base.find((x) => x.channelId === input.channelId) ?? base[0];
|
|
1002
2753
|
const active = input.status === "active";
|
|
1003
2754
|
const created = {
|
|
1004
|
-
id: `cmp-${++
|
|
2755
|
+
id: `cmp-${++counter2}`,
|
|
1005
2756
|
name: input.name,
|
|
1006
2757
|
channelId: input.channelId,
|
|
1007
2758
|
status: input.status,
|
|
@@ -1030,6 +2781,425 @@ function createMockMarketingProvider(cfg) {
|
|
|
1030
2781
|
}
|
|
1031
2782
|
};
|
|
1032
2783
|
}
|
|
2784
|
+
|
|
2785
|
+
// src/data/contentMock.ts
|
|
2786
|
+
var counter = 0;
|
|
2787
|
+
var nextId = (prefix) => `${prefix}-${++counter}`;
|
|
2788
|
+
var SAMPLE_SCRIPT = `# 5 sinais de que sua queda capilar n\xE3o \xE9 normal
|
|
2789
|
+
|
|
2790
|
+
**TEMA:** 5 SINAIS DE QUE SUA QUEDA CAPILAR N\xC3O \xC9 NORMAL
|
|
2791
|
+
**Formato:** Reel \xB7 **Dura\xE7\xE3o:** 45 a 60 segundos
|
|
2792
|
+
|
|
2793
|
+
---
|
|
2794
|
+
|
|
2795
|
+
## TAKE 1 \u2013 GANCHO (0s a 5s)
|
|
2796
|
+
|
|
2797
|
+
*Dra. andando at\xE9 a c\xE2mera.*
|
|
2798
|
+
|
|
2799
|
+
**Texto na tela:** \u26A0\uFE0F 5 sinais de alerta
|
|
2800
|
+
|
|
2801
|
+
**Fala:** "Seu cabelo est\xE1 caindo e voc\xEA n\xE3o sabe se isso \xE9 normal? Existem sinais que merecem aten\xE7\xE3o."
|
|
2802
|
+
|
|
2803
|
+
---
|
|
2804
|
+
|
|
2805
|
+
## TAKE 2 \u2013 SINAL 1 (5s a 12s)
|
|
2806
|
+
|
|
2807
|
+
*Mostrar a Dra. segurando alguns fios de cabelo ou imagem ilustrativa.*
|
|
2808
|
+
|
|
2809
|
+
**Texto na tela:** Excesso de fios caindo.
|
|
2810
|
+
|
|
2811
|
+
**Fala:** "Primeiro sinal: voc\xEA percebe muitos fios no travesseiro, no banho ou ao pentear os cabelos."
|
|
2812
|
+
|
|
2813
|
+
---
|
|
2814
|
+
|
|
2815
|
+
## TAKE 3 \u2013 CTA (40s a 60s)
|
|
2816
|
+
|
|
2817
|
+
**Fala:** "Se voc\xEA se identificou com algum desses sinais, agende uma avalia\xE7\xE3o capilar."
|
|
2818
|
+
|
|
2819
|
+
**CTA:** Agende sua avalia\xE7\xE3o
|
|
2820
|
+
`;
|
|
2821
|
+
var WEEK_TITLES = [
|
|
2822
|
+
["5 sinais de que sua queda capilar n\xE3o \xE9 normal", "Antes e depois de tratamento capilar"],
|
|
2823
|
+
["O que \xE9 DHT?", "Tour pela cl\xEDnica"],
|
|
2824
|
+
["3 erros que aceleram a calv\xEDcie", "Depoimento de paciente"],
|
|
2825
|
+
["Como funciona uma avalia\xE7\xE3o capilar", "Mitos sobre queda de cabelo"],
|
|
2826
|
+
["Por que seu tratamento anterior n\xE3o funcionou?", "Ozonioterapia para gordura localizada"],
|
|
2827
|
+
["Diferen\xE7a entre dermatologista e tricologista", "Caso real de evolu\xE7\xE3o"],
|
|
2828
|
+
["Alopecia feminina explicada", "Bastidores da cl\xEDnica"],
|
|
2829
|
+
["Quando procurar ajuda para queda capilar", "Convite para avalia\xE7\xE3o gratuita"]
|
|
2830
|
+
];
|
|
2831
|
+
function buildSeed() {
|
|
2832
|
+
const account = {
|
|
2833
|
+
id: nextId("acc"),
|
|
2834
|
+
name: "Conta demo",
|
|
2835
|
+
handle: "@clinica.demo",
|
|
2836
|
+
platforms: ["instagram"],
|
|
2837
|
+
isActive: true
|
|
2838
|
+
};
|
|
2839
|
+
const plan = {
|
|
2840
|
+
id: nextId("plan"),
|
|
2841
|
+
accountId: account.id,
|
|
2842
|
+
name: "Plano de conte\xFAdo \u2014 8 semanas",
|
|
2843
|
+
status: "active",
|
|
2844
|
+
weeksCount: 8,
|
|
2845
|
+
objective: "Atrair pacientes qualificados para avalia\xE7\xE3o capilar",
|
|
2846
|
+
tone: "Autoridade acess\xEDvel \u2014 t\xE9cnica, mas em linguagem simples",
|
|
2847
|
+
pillars: ["Educa\xE7\xE3o", "Autoridade", "Prova social", "Bastidores", "Convers\xE3o"],
|
|
2848
|
+
formats: ["reel", "static"],
|
|
2849
|
+
weeklyFrequency: 3,
|
|
2850
|
+
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"
|
|
2851
|
+
};
|
|
2852
|
+
const posts = [];
|
|
2853
|
+
WEEK_TITLES.forEach(([reel1, reel2], i) => {
|
|
2854
|
+
const week = i + 1;
|
|
2855
|
+
posts.push(
|
|
2856
|
+
{
|
|
2857
|
+
id: nextId("post"),
|
|
2858
|
+
planId: plan.id,
|
|
2859
|
+
weekNumber: week,
|
|
2860
|
+
position: 0,
|
|
2861
|
+
title: reel1,
|
|
2862
|
+
format: "reel",
|
|
2863
|
+
status: week === 1 ? "script" : "idea",
|
|
2864
|
+
platforms: [],
|
|
2865
|
+
checklist: [],
|
|
2866
|
+
contentMd: week === 1 ? SAMPLE_SCRIPT : ""
|
|
2867
|
+
},
|
|
2868
|
+
{
|
|
2869
|
+
id: nextId("post"),
|
|
2870
|
+
planId: plan.id,
|
|
2871
|
+
weekNumber: week,
|
|
2872
|
+
position: 1,
|
|
2873
|
+
title: reel2,
|
|
2874
|
+
format: "reel",
|
|
2875
|
+
status: "idea",
|
|
2876
|
+
platforms: [],
|
|
2877
|
+
checklist: [],
|
|
2878
|
+
contentMd: ""
|
|
2879
|
+
},
|
|
2880
|
+
{
|
|
2881
|
+
id: nextId("post"),
|
|
2882
|
+
planId: plan.id,
|
|
2883
|
+
weekNumber: week,
|
|
2884
|
+
position: 2,
|
|
2885
|
+
title: "Est\xE1tico",
|
|
2886
|
+
format: "static",
|
|
2887
|
+
status: "idea",
|
|
2888
|
+
platforms: [],
|
|
2889
|
+
checklist: [],
|
|
2890
|
+
contentMd: ""
|
|
2891
|
+
}
|
|
2892
|
+
);
|
|
2893
|
+
});
|
|
2894
|
+
return { accounts: [account], plans: [plan], posts };
|
|
2895
|
+
}
|
|
2896
|
+
function createMockContentPlannerProvider() {
|
|
2897
|
+
const db = buildSeed();
|
|
2898
|
+
return {
|
|
2899
|
+
async listAccounts() {
|
|
2900
|
+
return [...db.accounts];
|
|
2901
|
+
},
|
|
2902
|
+
async saveAccount(input) {
|
|
2903
|
+
if (input.id) {
|
|
2904
|
+
const acc2 = db.accounts.find((a) => a.id === input.id);
|
|
2905
|
+
if (!acc2) throw new Error("Account not found");
|
|
2906
|
+
Object.assign(acc2, {
|
|
2907
|
+
name: input.name ?? acc2.name,
|
|
2908
|
+
handle: input.handle ?? acc2.handle,
|
|
2909
|
+
platforms: input.platforms ?? acc2.platforms,
|
|
2910
|
+
isActive: input.isActive ?? acc2.isActive
|
|
2911
|
+
});
|
|
2912
|
+
return { ...acc2 };
|
|
2913
|
+
}
|
|
2914
|
+
const acc = {
|
|
2915
|
+
id: nextId("acc"),
|
|
2916
|
+
name: input.name,
|
|
2917
|
+
handle: input.handle,
|
|
2918
|
+
platforms: input.platforms ?? ["instagram"],
|
|
2919
|
+
isActive: input.isActive ?? true
|
|
2920
|
+
};
|
|
2921
|
+
db.accounts.push(acc);
|
|
2922
|
+
return { ...acc };
|
|
2923
|
+
},
|
|
2924
|
+
async deleteAccount(id) {
|
|
2925
|
+
db.accounts = db.accounts.filter((a) => a.id !== id);
|
|
2926
|
+
const planIds = new Set(db.plans.filter((p) => p.accountId === id).map((p) => p.id));
|
|
2927
|
+
db.plans = db.plans.filter((p) => p.accountId !== id);
|
|
2928
|
+
db.posts = db.posts.filter((p) => !planIds.has(p.planId));
|
|
2929
|
+
},
|
|
2930
|
+
async listPlans(accountId) {
|
|
2931
|
+
return db.plans.filter((p) => p.accountId === accountId).map((p) => ({ ...p }));
|
|
2932
|
+
},
|
|
2933
|
+
async getPlan(id) {
|
|
2934
|
+
const plan = db.plans.find((p) => p.id === id);
|
|
2935
|
+
return plan ? { ...plan } : null;
|
|
2936
|
+
},
|
|
2937
|
+
async savePlan(input) {
|
|
2938
|
+
if (input.id) {
|
|
2939
|
+
const plan2 = db.plans.find((p) => p.id === input.id);
|
|
2940
|
+
if (!plan2) throw new Error("Plan not found");
|
|
2941
|
+
Object.assign(plan2, {
|
|
2942
|
+
...input.name !== void 0 && { name: input.name },
|
|
2943
|
+
...input.status !== void 0 && { status: input.status },
|
|
2944
|
+
...input.weeksCount !== void 0 && { weeksCount: input.weeksCount },
|
|
2945
|
+
...input.startDate !== void 0 && { startDate: input.startDate ?? void 0 },
|
|
2946
|
+
...input.objective !== void 0 && { objective: input.objective },
|
|
2947
|
+
...input.tone !== void 0 && { tone: input.tone },
|
|
2948
|
+
...input.pillars !== void 0 && { pillars: input.pillars },
|
|
2949
|
+
...input.formats !== void 0 && { formats: input.formats },
|
|
2950
|
+
...input.weeklyFrequency !== void 0 && { weeklyFrequency: input.weeklyFrequency },
|
|
2951
|
+
...input.briefMd !== void 0 && { briefMd: input.briefMd }
|
|
2952
|
+
});
|
|
2953
|
+
return { ...plan2 };
|
|
2954
|
+
}
|
|
2955
|
+
if (!input.accountId) throw new Error("accountId is required");
|
|
2956
|
+
const plan = {
|
|
2957
|
+
id: nextId("plan"),
|
|
2958
|
+
accountId: input.accountId,
|
|
2959
|
+
name: input.name ?? "Novo plano",
|
|
2960
|
+
status: input.status ?? "draft",
|
|
2961
|
+
weeksCount: input.weeksCount ?? 4,
|
|
2962
|
+
startDate: input.startDate ?? void 0,
|
|
2963
|
+
objective: input.objective,
|
|
2964
|
+
tone: input.tone,
|
|
2965
|
+
pillars: input.pillars ?? [],
|
|
2966
|
+
formats: input.formats ?? ["reel", "static"],
|
|
2967
|
+
weeklyFrequency: input.weeklyFrequency ?? 3,
|
|
2968
|
+
briefMd: input.briefMd ?? ""
|
|
2969
|
+
};
|
|
2970
|
+
db.plans.push(plan);
|
|
2971
|
+
return { ...plan };
|
|
2972
|
+
},
|
|
2973
|
+
async deletePlan(id) {
|
|
2974
|
+
db.plans = db.plans.filter((p) => p.id !== id);
|
|
2975
|
+
db.posts = db.posts.filter((p) => p.planId !== id);
|
|
2976
|
+
},
|
|
2977
|
+
async listPosts(planId) {
|
|
2978
|
+
return db.posts.filter((p) => p.planId === planId).sort((a, b) => a.weekNumber - b.weekNumber || a.position - b.position).map((p) => ({ ...p }));
|
|
2979
|
+
},
|
|
2980
|
+
async getPost(id) {
|
|
2981
|
+
const post = db.posts.find((p) => p.id === id);
|
|
2982
|
+
return post ? { ...post } : null;
|
|
2983
|
+
},
|
|
2984
|
+
async savePost(input) {
|
|
2985
|
+
if (input.id) {
|
|
2986
|
+
const post2 = db.posts.find((p) => p.id === input.id);
|
|
2987
|
+
if (!post2) throw new Error("Post not found");
|
|
2988
|
+
Object.assign(post2, {
|
|
2989
|
+
...input.weekNumber !== void 0 && { weekNumber: input.weekNumber },
|
|
2990
|
+
...input.position !== void 0 && { position: input.position },
|
|
2991
|
+
...input.title !== void 0 && { title: input.title },
|
|
2992
|
+
...input.format !== void 0 && { format: input.format },
|
|
2993
|
+
...input.status !== void 0 && { status: input.status },
|
|
2994
|
+
...input.scheduledDate !== void 0 && { scheduledDate: input.scheduledDate ?? void 0 },
|
|
2995
|
+
...input.platforms !== void 0 && { platforms: input.platforms },
|
|
2996
|
+
...input.checklist !== void 0 && { checklist: input.checklist },
|
|
2997
|
+
...input.mediaUrl !== void 0 && { mediaUrl: input.mediaUrl ?? void 0 },
|
|
2998
|
+
...input.hook !== void 0 && { hook: input.hook },
|
|
2999
|
+
...input.cta !== void 0 && { cta: input.cta },
|
|
3000
|
+
...input.contentMd !== void 0 && { contentMd: input.contentMd }
|
|
3001
|
+
});
|
|
3002
|
+
return { ...post2 };
|
|
3003
|
+
}
|
|
3004
|
+
if (!input.planId) throw new Error("planId is required");
|
|
3005
|
+
const week = input.weekNumber ?? 1;
|
|
3006
|
+
const siblings = db.posts.filter((p) => p.planId === input.planId && p.weekNumber === week);
|
|
3007
|
+
const post = {
|
|
3008
|
+
id: nextId("post"),
|
|
3009
|
+
planId: input.planId,
|
|
3010
|
+
weekNumber: week,
|
|
3011
|
+
position: input.position ?? siblings.length,
|
|
3012
|
+
title: input.title ?? "",
|
|
3013
|
+
format: input.format ?? "reel",
|
|
3014
|
+
status: input.status ?? "idea",
|
|
3015
|
+
scheduledDate: input.scheduledDate ?? void 0,
|
|
3016
|
+
platforms: input.platforms ?? [],
|
|
3017
|
+
checklist: input.checklist ?? [],
|
|
3018
|
+
mediaUrl: input.mediaUrl ?? void 0,
|
|
3019
|
+
hook: input.hook,
|
|
3020
|
+
cta: input.cta,
|
|
3021
|
+
contentMd: input.contentMd ?? ""
|
|
3022
|
+
};
|
|
3023
|
+
db.posts.push(post);
|
|
3024
|
+
return { ...post };
|
|
3025
|
+
},
|
|
3026
|
+
async deletePost(id) {
|
|
3027
|
+
db.posts = db.posts.filter((p) => p.id !== id);
|
|
3028
|
+
}
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
function getClient() {
|
|
3032
|
+
const supabase = getSupabaseClientOptional();
|
|
3033
|
+
if (!supabase) throw new Error("Supabase not initialized");
|
|
3034
|
+
return supabase;
|
|
3035
|
+
}
|
|
3036
|
+
function requireTenantId() {
|
|
3037
|
+
const tenantId = getActiveTenantId();
|
|
3038
|
+
if (!tenantId) throw new Error("No active tenant");
|
|
3039
|
+
return tenantId;
|
|
3040
|
+
}
|
|
3041
|
+
function snakeToCamel(obj) {
|
|
3042
|
+
const result = {};
|
|
3043
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
3044
|
+
result[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = value ?? void 0;
|
|
3045
|
+
}
|
|
3046
|
+
return result;
|
|
3047
|
+
}
|
|
3048
|
+
function toAccount(row) {
|
|
3049
|
+
const account = snakeToCamel(row);
|
|
3050
|
+
if (!Array.isArray(account.platforms)) account.platforms = [];
|
|
3051
|
+
return account;
|
|
3052
|
+
}
|
|
3053
|
+
function toPlan(row) {
|
|
3054
|
+
const plan = snakeToCamel(row);
|
|
3055
|
+
if (!Array.isArray(plan.pillars)) plan.pillars = [];
|
|
3056
|
+
if (!Array.isArray(plan.formats)) plan.formats = [];
|
|
3057
|
+
plan.briefMd = plan.briefMd ?? "";
|
|
3058
|
+
return plan;
|
|
3059
|
+
}
|
|
3060
|
+
function toPost(row) {
|
|
3061
|
+
const post = snakeToCamel(row);
|
|
3062
|
+
post.contentMd = post.contentMd ?? "";
|
|
3063
|
+
if (!Array.isArray(post.platforms)) post.platforms = [];
|
|
3064
|
+
if (!Array.isArray(post.checklist)) post.checklist = [];
|
|
3065
|
+
return post;
|
|
3066
|
+
}
|
|
3067
|
+
function createSupabaseContentPlannerProvider() {
|
|
3068
|
+
return {
|
|
3069
|
+
async listAccounts() {
|
|
3070
|
+
const tenantId = getActiveTenantId();
|
|
3071
|
+
if (!tenantId) return [];
|
|
3072
|
+
const { data } = await getClient().from("mkt_social_accounts").select("*").eq("tenant_id", tenantId).eq("is_active", true).order("created_at", { ascending: true });
|
|
3073
|
+
return (data ?? []).map(toAccount);
|
|
3074
|
+
},
|
|
3075
|
+
async saveAccount(input) {
|
|
3076
|
+
if (input.id) {
|
|
3077
|
+
const payload = {};
|
|
3078
|
+
if (input.name !== void 0) payload.name = input.name;
|
|
3079
|
+
if (input.handle !== void 0) payload.handle = input.handle;
|
|
3080
|
+
if (input.platforms !== void 0) payload.platforms = input.platforms;
|
|
3081
|
+
if (input.isActive !== void 0) payload.is_active = input.isActive;
|
|
3082
|
+
const { data: data2, error: error2 } = await getClient().from("mkt_social_accounts").update(payload).eq("id", input.id).select("*").single();
|
|
3083
|
+
if (error2) throw error2;
|
|
3084
|
+
return toAccount(data2);
|
|
3085
|
+
}
|
|
3086
|
+
const { data, error } = await getClient().from("mkt_social_accounts").insert({
|
|
3087
|
+
tenant_id: requireTenantId(),
|
|
3088
|
+
name: input.name,
|
|
3089
|
+
handle: input.handle ?? null,
|
|
3090
|
+
platforms: input.platforms ?? ["instagram"],
|
|
3091
|
+
is_active: input.isActive ?? true
|
|
3092
|
+
}).select("*").single();
|
|
3093
|
+
if (error) throw error;
|
|
3094
|
+
return toAccount(data);
|
|
3095
|
+
},
|
|
3096
|
+
async deleteAccount(id) {
|
|
3097
|
+
const { error } = await getClient().from("mkt_social_accounts").delete().eq("id", id);
|
|
3098
|
+
if (error) throw error;
|
|
3099
|
+
},
|
|
3100
|
+
async listPlans(accountId) {
|
|
3101
|
+
const tenantId = getActiveTenantId();
|
|
3102
|
+
if (!tenantId) return [];
|
|
3103
|
+
const { data } = await getClient().from("mkt_content_plans").select("*").eq("tenant_id", tenantId).eq("account_id", accountId).order("created_at", { ascending: false });
|
|
3104
|
+
return (data ?? []).map(toPlan);
|
|
3105
|
+
},
|
|
3106
|
+
async getPlan(id) {
|
|
3107
|
+
const { data } = await getClient().from("mkt_content_plans").select("*").eq("id", id).maybeSingle();
|
|
3108
|
+
return data ? toPlan(data) : null;
|
|
3109
|
+
},
|
|
3110
|
+
async savePlan(input) {
|
|
3111
|
+
if (input.id) {
|
|
3112
|
+
const payload = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3113
|
+
if (input.name !== void 0) payload.name = input.name;
|
|
3114
|
+
if (input.status !== void 0) payload.status = input.status;
|
|
3115
|
+
if (input.weeksCount !== void 0) payload.weeks_count = input.weeksCount;
|
|
3116
|
+
if (input.startDate !== void 0) payload.start_date = input.startDate;
|
|
3117
|
+
if (input.objective !== void 0) payload.objective = input.objective;
|
|
3118
|
+
if (input.tone !== void 0) payload.tone = input.tone;
|
|
3119
|
+
if (input.pillars !== void 0) payload.pillars = input.pillars;
|
|
3120
|
+
if (input.formats !== void 0) payload.formats = input.formats;
|
|
3121
|
+
if (input.weeklyFrequency !== void 0) payload.weekly_frequency = input.weeklyFrequency;
|
|
3122
|
+
if (input.briefMd !== void 0) payload.brief_md = input.briefMd;
|
|
3123
|
+
const { data: data2, error: error2 } = await getClient().from("mkt_content_plans").update(payload).eq("id", input.id).select("*").single();
|
|
3124
|
+
if (error2) throw error2;
|
|
3125
|
+
return toPlan(data2);
|
|
3126
|
+
}
|
|
3127
|
+
if (!input.accountId) throw new Error("accountId is required");
|
|
3128
|
+
const { data, error } = await getClient().from("mkt_content_plans").insert({
|
|
3129
|
+
tenant_id: requireTenantId(),
|
|
3130
|
+
account_id: input.accountId,
|
|
3131
|
+
name: input.name ?? "Novo plano",
|
|
3132
|
+
status: input.status ?? "draft",
|
|
3133
|
+
weeks_count: input.weeksCount ?? 4,
|
|
3134
|
+
start_date: input.startDate ?? null,
|
|
3135
|
+
objective: input.objective ?? null,
|
|
3136
|
+
tone: input.tone ?? null,
|
|
3137
|
+
pillars: input.pillars ?? [],
|
|
3138
|
+
formats: input.formats ?? ["reel", "static"],
|
|
3139
|
+
weekly_frequency: input.weeklyFrequency ?? 3,
|
|
3140
|
+
brief_md: input.briefMd ?? ""
|
|
3141
|
+
}).select("*").single();
|
|
3142
|
+
if (error) throw error;
|
|
3143
|
+
return toPlan(data);
|
|
3144
|
+
},
|
|
3145
|
+
async deletePlan(id) {
|
|
3146
|
+
await getClient().from("mkt_content_plans").delete().eq("id", id);
|
|
3147
|
+
},
|
|
3148
|
+
async listPosts(planId) {
|
|
3149
|
+
const { data } = await getClient().from("mkt_content_posts").select("*").eq("plan_id", planId).order("week_number", { ascending: true }).order("position", { ascending: true });
|
|
3150
|
+
return (data ?? []).map(toPost);
|
|
3151
|
+
},
|
|
3152
|
+
async getPost(id) {
|
|
3153
|
+
const { data } = await getClient().from("mkt_content_posts").select("*").eq("id", id).maybeSingle();
|
|
3154
|
+
return data ? toPost(data) : null;
|
|
3155
|
+
},
|
|
3156
|
+
async savePost(input) {
|
|
3157
|
+
if (input.id) {
|
|
3158
|
+
const payload = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3159
|
+
if (input.weekNumber !== void 0) payload.week_number = input.weekNumber;
|
|
3160
|
+
if (input.position !== void 0) payload.position = input.position;
|
|
3161
|
+
if (input.title !== void 0) payload.title = input.title;
|
|
3162
|
+
if (input.format !== void 0) payload.format = input.format;
|
|
3163
|
+
if (input.status !== void 0) payload.status = input.status;
|
|
3164
|
+
if (input.scheduledDate !== void 0) payload.scheduled_date = input.scheduledDate;
|
|
3165
|
+
if (input.platforms !== void 0) payload.platforms = input.platforms;
|
|
3166
|
+
if (input.checklist !== void 0) payload.checklist = input.checklist;
|
|
3167
|
+
if (input.mediaUrl !== void 0) payload.media_url = input.mediaUrl;
|
|
3168
|
+
if (input.hook !== void 0) payload.hook = input.hook;
|
|
3169
|
+
if (input.cta !== void 0) payload.cta = input.cta;
|
|
3170
|
+
if (input.contentMd !== void 0) payload.content_md = input.contentMd;
|
|
3171
|
+
const { data: data2, error: error2 } = await getClient().from("mkt_content_posts").update(payload).eq("id", input.id).select("*").single();
|
|
3172
|
+
if (error2) throw error2;
|
|
3173
|
+
return toPost(data2);
|
|
3174
|
+
}
|
|
3175
|
+
if (!input.planId) throw new Error("planId is required");
|
|
3176
|
+
const week = input.weekNumber ?? 1;
|
|
3177
|
+
const { data: maxData } = await getClient().from("mkt_content_posts").select("position").eq("plan_id", input.planId).eq("week_number", week).order("position", { ascending: false }).limit(1);
|
|
3178
|
+
const maxPos = maxData?.[0]?.position ?? -1;
|
|
3179
|
+
const { data, error } = await getClient().from("mkt_content_posts").insert({
|
|
3180
|
+
tenant_id: requireTenantId(),
|
|
3181
|
+
plan_id: input.planId,
|
|
3182
|
+
week_number: week,
|
|
3183
|
+
position: input.position ?? maxPos + 1,
|
|
3184
|
+
title: input.title ?? "",
|
|
3185
|
+
format: input.format ?? "reel",
|
|
3186
|
+
status: input.status ?? "idea",
|
|
3187
|
+
scheduled_date: input.scheduledDate ?? null,
|
|
3188
|
+
platforms: input.platforms ?? [],
|
|
3189
|
+
checklist: input.checklist ?? [],
|
|
3190
|
+
media_url: input.mediaUrl ?? null,
|
|
3191
|
+
hook: input.hook ?? null,
|
|
3192
|
+
cta: input.cta ?? null,
|
|
3193
|
+
content_md: input.contentMd ?? ""
|
|
3194
|
+
}).select("*").single();
|
|
3195
|
+
if (error) throw error;
|
|
3196
|
+
return toPost(data);
|
|
3197
|
+
},
|
|
3198
|
+
async deletePost(id) {
|
|
3199
|
+
await getClient().from("mkt_content_posts").delete().eq("id", id);
|
|
3200
|
+
}
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
1033
3203
|
function createMarketingStore(provider) {
|
|
1034
3204
|
return createStore((set, get) => ({
|
|
1035
3205
|
range: "30d",
|
|
@@ -1066,6 +3236,261 @@ function createMarketingStore(provider) {
|
|
|
1066
3236
|
}
|
|
1067
3237
|
}));
|
|
1068
3238
|
}
|
|
3239
|
+
function createContentPlannerStore(provider) {
|
|
3240
|
+
async function loadPlansFor(accountId) {
|
|
3241
|
+
const plans = await provider.listPlans(accountId);
|
|
3242
|
+
const activePlan = plans.find((p) => p.status === "active") ?? plans[0] ?? null;
|
|
3243
|
+
const posts = activePlan ? await provider.listPosts(activePlan.id) : [];
|
|
3244
|
+
return { plans, activePlan, posts };
|
|
3245
|
+
}
|
|
3246
|
+
return createStore((set, get) => ({
|
|
3247
|
+
loaded: false,
|
|
3248
|
+
loading: false,
|
|
3249
|
+
loadedTenantId: null,
|
|
3250
|
+
viewMode: "board",
|
|
3251
|
+
setViewMode(mode) {
|
|
3252
|
+
set({ viewMode: mode });
|
|
3253
|
+
},
|
|
3254
|
+
accounts: [],
|
|
3255
|
+
activeAccountId: null,
|
|
3256
|
+
plans: [],
|
|
3257
|
+
activePlan: null,
|
|
3258
|
+
posts: [],
|
|
3259
|
+
async load() {
|
|
3260
|
+
if (get().loading) return;
|
|
3261
|
+
set({ loading: true });
|
|
3262
|
+
try {
|
|
3263
|
+
const accounts = await provider.listAccounts();
|
|
3264
|
+
const current = get().activeAccountId;
|
|
3265
|
+
const activeAccountId = (current && accounts.some((a) => a.id === current) ? current : accounts[0]?.id) ?? null;
|
|
3266
|
+
const { plans, activePlan, posts } = activeAccountId ? await loadPlansFor(activeAccountId) : { plans: [], activePlan: null, posts: [] };
|
|
3267
|
+
set({
|
|
3268
|
+
accounts,
|
|
3269
|
+
activeAccountId,
|
|
3270
|
+
plans,
|
|
3271
|
+
activePlan,
|
|
3272
|
+
posts,
|
|
3273
|
+
loaded: true,
|
|
3274
|
+
loading: false,
|
|
3275
|
+
loadedTenantId: getActiveTenantId() ?? null
|
|
3276
|
+
});
|
|
3277
|
+
} catch (err) {
|
|
3278
|
+
set({ loading: false, loaded: true });
|
|
3279
|
+
throw err;
|
|
3280
|
+
}
|
|
3281
|
+
},
|
|
3282
|
+
async setAccount(accountId) {
|
|
3283
|
+
set({ activeAccountId: accountId, loading: true });
|
|
3284
|
+
try {
|
|
3285
|
+
const { plans, activePlan, posts } = await loadPlansFor(accountId);
|
|
3286
|
+
set({ plans, activePlan, posts, loading: false });
|
|
3287
|
+
} catch (err) {
|
|
3288
|
+
set({ loading: false });
|
|
3289
|
+
throw err;
|
|
3290
|
+
}
|
|
3291
|
+
},
|
|
3292
|
+
async selectPlan(planId) {
|
|
3293
|
+
const plan = get().plans.find((p) => p.id === planId) ?? null;
|
|
3294
|
+
const posts = plan ? await provider.listPosts(plan.id) : [];
|
|
3295
|
+
set({ activePlan: plan, posts });
|
|
3296
|
+
},
|
|
3297
|
+
async saveAccount(input) {
|
|
3298
|
+
const account = await provider.saveAccount(input);
|
|
3299
|
+
const accounts = await provider.listAccounts();
|
|
3300
|
+
set({ accounts });
|
|
3301
|
+
if (!get().activeAccountId) await get().setAccount(account.id);
|
|
3302
|
+
return account;
|
|
3303
|
+
},
|
|
3304
|
+
async deleteAccount(id) {
|
|
3305
|
+
await provider.deleteAccount(id);
|
|
3306
|
+
const accounts = await provider.listAccounts();
|
|
3307
|
+
const wasActive = get().activeAccountId === id;
|
|
3308
|
+
set({ accounts });
|
|
3309
|
+
if (wasActive) {
|
|
3310
|
+
const next = accounts[0]?.id ?? null;
|
|
3311
|
+
if (next) {
|
|
3312
|
+
await get().setAccount(next);
|
|
3313
|
+
} else {
|
|
3314
|
+
set({ activeAccountId: null, plans: [], activePlan: null, posts: [] });
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
},
|
|
3318
|
+
async savePlan(input) {
|
|
3319
|
+
const plan = await provider.savePlan(input);
|
|
3320
|
+
const accountId = get().activeAccountId;
|
|
3321
|
+
if (accountId) {
|
|
3322
|
+
const plans = await provider.listPlans(accountId);
|
|
3323
|
+
const activePlan = plans.find((p) => p.id === plan.id) ?? get().activePlan;
|
|
3324
|
+
const posts = activePlan ? await provider.listPosts(activePlan.id) : [];
|
|
3325
|
+
set({ plans, activePlan, posts });
|
|
3326
|
+
}
|
|
3327
|
+
return plan;
|
|
3328
|
+
},
|
|
3329
|
+
async savePost(input) {
|
|
3330
|
+
const post = await provider.savePost(input);
|
|
3331
|
+
const planId = get().activePlan?.id;
|
|
3332
|
+
if (planId && post.planId === planId) {
|
|
3333
|
+
const posts = await provider.listPosts(planId);
|
|
3334
|
+
set({ posts });
|
|
3335
|
+
}
|
|
3336
|
+
return post;
|
|
3337
|
+
},
|
|
3338
|
+
async deletePost(id) {
|
|
3339
|
+
await provider.deletePost(id);
|
|
3340
|
+
const planId = get().activePlan?.id;
|
|
3341
|
+
if (planId) {
|
|
3342
|
+
const posts = await provider.listPosts(planId);
|
|
3343
|
+
set({ posts });
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
}));
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
// src/migrations/index.ts
|
|
3350
|
+
var MIGRATION_001_CONTENT_PLANNER = `-- Marketing Plugin: Content Planner
|
|
3351
|
+
-- Prefix: mkt_
|
|
3352
|
+
-- Social accounts + content plans + posts (Notion-style markdown pages).
|
|
3353
|
+
|
|
3354
|
+
CREATE TABLE IF NOT EXISTS public.mkt_social_accounts (
|
|
3355
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3356
|
+
tenant_id uuid NOT NULL REFERENCES saas_core.tenants(id) ON DELETE CASCADE,
|
|
3357
|
+
name text NOT NULL,
|
|
3358
|
+
handle text,
|
|
3359
|
+
platform text NOT NULL DEFAULT 'instagram'
|
|
3360
|
+
CHECK (platform IN ('instagram', 'tiktok', 'youtube', 'facebook', 'linkedin')),
|
|
3361
|
+
is_active boolean NOT NULL DEFAULT true,
|
|
3362
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
3363
|
+
);
|
|
3364
|
+
ALTER TABLE public.mkt_social_accounts ENABLE ROW LEVEL SECURITY;
|
|
3365
|
+
CREATE INDEX IF NOT EXISTS idx_mkt_social_accounts_tenant ON public.mkt_social_accounts(tenant_id);
|
|
3366
|
+
|
|
3367
|
+
CREATE TABLE IF NOT EXISTS public.mkt_content_plans (
|
|
3368
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3369
|
+
tenant_id uuid NOT NULL REFERENCES saas_core.tenants(id) ON DELETE CASCADE,
|
|
3370
|
+
account_id uuid NOT NULL REFERENCES public.mkt_social_accounts(id) ON DELETE CASCADE,
|
|
3371
|
+
name text NOT NULL,
|
|
3372
|
+
status text NOT NULL DEFAULT 'active'
|
|
3373
|
+
CHECK (status IN ('draft', 'active', 'archived')),
|
|
3374
|
+
weeks_count integer NOT NULL DEFAULT 8,
|
|
3375
|
+
start_date date,
|
|
3376
|
+
objective text,
|
|
3377
|
+
tone text,
|
|
3378
|
+
pillars jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
3379
|
+
formats jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
3380
|
+
weekly_frequency integer NOT NULL DEFAULT 3,
|
|
3381
|
+
brief_md text NOT NULL DEFAULT '',
|
|
3382
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3383
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
3384
|
+
);
|
|
3385
|
+
ALTER TABLE public.mkt_content_plans ENABLE ROW LEVEL SECURITY;
|
|
3386
|
+
CREATE INDEX IF NOT EXISTS idx_mkt_content_plans_tenant ON public.mkt_content_plans(tenant_id);
|
|
3387
|
+
CREATE INDEX IF NOT EXISTS idx_mkt_content_plans_account ON public.mkt_content_plans(account_id);
|
|
3388
|
+
|
|
3389
|
+
CREATE TABLE IF NOT EXISTS public.mkt_content_posts (
|
|
3390
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3391
|
+
tenant_id uuid NOT NULL REFERENCES saas_core.tenants(id) ON DELETE CASCADE,
|
|
3392
|
+
plan_id uuid NOT NULL REFERENCES public.mkt_content_plans(id) ON DELETE CASCADE,
|
|
3393
|
+
week_number integer NOT NULL DEFAULT 1,
|
|
3394
|
+
position integer NOT NULL DEFAULT 0,
|
|
3395
|
+
title text NOT NULL,
|
|
3396
|
+
format text NOT NULL DEFAULT 'reel'
|
|
3397
|
+
CHECK (format IN ('reel', 'static', 'carousel', 'story')),
|
|
3398
|
+
status text NOT NULL DEFAULT 'idea'
|
|
3399
|
+
CHECK (status IN ('idea', 'script', 'recording', 'editing', 'scheduled', 'published')),
|
|
3400
|
+
scheduled_date date,
|
|
3401
|
+
hook text,
|
|
3402
|
+
cta text,
|
|
3403
|
+
content_md text NOT NULL DEFAULT '',
|
|
3404
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3405
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
3406
|
+
);
|
|
3407
|
+
ALTER TABLE public.mkt_content_posts ENABLE ROW LEVEL SECURITY;
|
|
3408
|
+
CREATE INDEX IF NOT EXISTS idx_mkt_content_posts_tenant ON public.mkt_content_posts(tenant_id);
|
|
3409
|
+
CREATE INDEX IF NOT EXISTS idx_mkt_content_posts_plan ON public.mkt_content_posts(plan_id, week_number, position);
|
|
3410
|
+
CREATE INDEX IF NOT EXISTS idx_mkt_content_posts_status ON public.mkt_content_posts(tenant_id, status);
|
|
3411
|
+
|
|
3412
|
+
-- RLS policies for mkt_social_accounts
|
|
3413
|
+
DROP POLICY IF EXISTS mkt_social_accounts_select ON public.mkt_social_accounts;
|
|
3414
|
+
DROP POLICY IF EXISTS mkt_social_accounts_insert ON public.mkt_social_accounts;
|
|
3415
|
+
DROP POLICY IF EXISTS mkt_social_accounts_update ON public.mkt_social_accounts;
|
|
3416
|
+
DROP POLICY IF EXISTS mkt_social_accounts_delete ON public.mkt_social_accounts;
|
|
3417
|
+
CREATE POLICY mkt_social_accounts_select ON public.mkt_social_accounts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3418
|
+
CREATE POLICY mkt_social_accounts_insert ON public.mkt_social_accounts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3419
|
+
CREATE POLICY mkt_social_accounts_update ON public.mkt_social_accounts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3420
|
+
CREATE POLICY mkt_social_accounts_delete ON public.mkt_social_accounts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3421
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.mkt_social_accounts TO authenticated;
|
|
3422
|
+
|
|
3423
|
+
-- RLS policies for mkt_content_plans
|
|
3424
|
+
DROP POLICY IF EXISTS mkt_content_plans_select ON public.mkt_content_plans;
|
|
3425
|
+
DROP POLICY IF EXISTS mkt_content_plans_insert ON public.mkt_content_plans;
|
|
3426
|
+
DROP POLICY IF EXISTS mkt_content_plans_update ON public.mkt_content_plans;
|
|
3427
|
+
DROP POLICY IF EXISTS mkt_content_plans_delete ON public.mkt_content_plans;
|
|
3428
|
+
CREATE POLICY mkt_content_plans_select ON public.mkt_content_plans FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3429
|
+
CREATE POLICY mkt_content_plans_insert ON public.mkt_content_plans FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3430
|
+
CREATE POLICY mkt_content_plans_update ON public.mkt_content_plans FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3431
|
+
CREATE POLICY mkt_content_plans_delete ON public.mkt_content_plans FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3432
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.mkt_content_plans TO authenticated;
|
|
3433
|
+
|
|
3434
|
+
-- RLS policies for mkt_content_posts
|
|
3435
|
+
DROP POLICY IF EXISTS mkt_content_posts_select ON public.mkt_content_posts;
|
|
3436
|
+
DROP POLICY IF EXISTS mkt_content_posts_insert ON public.mkt_content_posts;
|
|
3437
|
+
DROP POLICY IF EXISTS mkt_content_posts_update ON public.mkt_content_posts;
|
|
3438
|
+
DROP POLICY IF EXISTS mkt_content_posts_delete ON public.mkt_content_posts;
|
|
3439
|
+
CREATE POLICY mkt_content_posts_select ON public.mkt_content_posts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3440
|
+
CREATE POLICY mkt_content_posts_insert ON public.mkt_content_posts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3441
|
+
CREATE POLICY mkt_content_posts_update ON public.mkt_content_posts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3442
|
+
CREATE POLICY mkt_content_posts_delete ON public.mkt_content_posts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3443
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.mkt_content_posts TO authenticated;`;
|
|
3444
|
+
var MIGRATION_002_MULTI_PLATFORM = `-- Marketing Plugin: multi-platform accounts + per-post targets.
|
|
3445
|
+
-- An account is a brand/profile that publishes to MANY platforms (Instagram,
|
|
3446
|
+
-- YouTube, TikTok, ...). Posts optionally narrow the target platforms; an
|
|
3447
|
+
-- empty array means "inherit the account's platforms".
|
|
3448
|
+
|
|
3449
|
+
ALTER TABLE public.mkt_social_accounts
|
|
3450
|
+
ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{instagram}';
|
|
3451
|
+
|
|
3452
|
+
-- Backfill from the legacy single-platform column, then drop it (also drops
|
|
3453
|
+
-- its CHECK constraint \u2014 the platform list is UI-driven from here on).
|
|
3454
|
+
DO $$
|
|
3455
|
+
BEGIN
|
|
3456
|
+
IF EXISTS (
|
|
3457
|
+
SELECT 1 FROM information_schema.columns
|
|
3458
|
+
WHERE table_schema = 'public' AND table_name = 'mkt_social_accounts' AND column_name = 'platform'
|
|
3459
|
+
) THEN
|
|
3460
|
+
UPDATE public.mkt_social_accounts
|
|
3461
|
+
SET platforms = ARRAY[platform]
|
|
3462
|
+
WHERE platform IS NOT NULL AND platforms = '{instagram}';
|
|
3463
|
+
ALTER TABLE public.mkt_social_accounts DROP COLUMN platform;
|
|
3464
|
+
END IF;
|
|
3465
|
+
END $$;
|
|
3466
|
+
|
|
3467
|
+
ALTER TABLE public.mkt_content_posts
|
|
3468
|
+
ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{}';`;
|
|
3469
|
+
var MIGRATION_003_RECORDING_OPS = `-- Marketing Plugin: recording-day ops on posts.
|
|
3470
|
+
-- checklist: shooting checklist items [{ id, text, done }] \u2014 the on-set
|
|
3471
|
+
-- companion for recording day. media_url: uploaded asset (static posts get a
|
|
3472
|
+
-- caption + final art instead of a script).
|
|
3473
|
+
|
|
3474
|
+
ALTER TABLE public.mkt_content_posts
|
|
3475
|
+
ADD COLUMN IF NOT EXISTS checklist jsonb NOT NULL DEFAULT '[]';
|
|
3476
|
+
|
|
3477
|
+
ALTER TABLE public.mkt_content_posts
|
|
3478
|
+
ADD COLUMN IF NOT EXISTS media_url text;
|
|
3479
|
+
|
|
3480
|
+
-- Public media bucket for content assets (mirrors the avatars-bucket pattern:
|
|
3481
|
+
-- public read, authenticated write). Path convention: content/{tenantId}/{postId}-{ts}.{ext}
|
|
3482
|
+
INSERT INTO storage.buckets (id, name, public)
|
|
3483
|
+
VALUES ('mkt-media', 'mkt-media', true)
|
|
3484
|
+
ON CONFLICT (id) DO NOTHING;
|
|
3485
|
+
|
|
3486
|
+
DROP POLICY IF EXISTS mkt_media_read ON storage.objects;
|
|
3487
|
+
DROP POLICY IF EXISTS mkt_media_insert ON storage.objects;
|
|
3488
|
+
DROP POLICY IF EXISTS mkt_media_update ON storage.objects;
|
|
3489
|
+
DROP POLICY IF EXISTS mkt_media_delete ON storage.objects;
|
|
3490
|
+
CREATE POLICY mkt_media_read ON storage.objects FOR SELECT TO public USING (bucket_id = 'mkt-media');
|
|
3491
|
+
CREATE POLICY mkt_media_insert ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'mkt-media');
|
|
3492
|
+
CREATE POLICY mkt_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
|
|
3493
|
+
CREATE POLICY mkt_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');`;
|
|
1069
3494
|
|
|
1070
3495
|
// src/locales/en.ts
|
|
1071
3496
|
var en = {
|
|
@@ -1140,7 +3565,102 @@ var en = {
|
|
|
1140
3565
|
"marketing.settings.autoSyncDesc": "Pull conversions automatically from the source.",
|
|
1141
3566
|
"marketing.settings.assisted": "Count assisted conversions",
|
|
1142
3567
|
"marketing.settings.assistedDesc": "Credit channels that contributed to a conversion.",
|
|
1143
|
-
"marketing.settings.channelTrackDesc": "Track this acquisition source."
|
|
3568
|
+
"marketing.settings.channelTrackDesc": "Track this acquisition source.",
|
|
3569
|
+
// content planner
|
|
3570
|
+
"marketing.content.title": "Content",
|
|
3571
|
+
"marketing.content.week": "Week",
|
|
3572
|
+
"marketing.content.month": "Month",
|
|
3573
|
+
"marketing.content.newPost": "New post",
|
|
3574
|
+
"marketing.content.untitled": "Untitled",
|
|
3575
|
+
"marketing.content.account": "Account",
|
|
3576
|
+
"marketing.content.addAccount": "Add account",
|
|
3577
|
+
"marketing.content.previewNow": "Just now",
|
|
3578
|
+
"marketing.content.previewLike": "Like",
|
|
3579
|
+
"marketing.content.previewComment": "Comment",
|
|
3580
|
+
"marketing.content.previewShare": "Share",
|
|
3581
|
+
"marketing.content.showLess": "show less",
|
|
3582
|
+
"marketing.content.checklistTitle": "Recording checklist",
|
|
3583
|
+
"marketing.content.checklistDescription": "Shooting-day operations",
|
|
3584
|
+
"marketing.content.checklistOpen": "Checklist",
|
|
3585
|
+
"marketing.content.checklistEmpty": "No items yet \u2014 generate them from the script or add your own below.",
|
|
3586
|
+
"marketing.content.checklistAddPlaceholder": "New item\u2026",
|
|
3587
|
+
"marketing.content.checklistAdd": "Add item",
|
|
3588
|
+
"marketing.content.checklistGenerate": "Generate from script",
|
|
3589
|
+
"marketing.content.scriptLabel": "Script",
|
|
3590
|
+
"marketing.content.captionLabel": "Caption",
|
|
3591
|
+
"marketing.content.captionPlaceholder": "Write the post caption\u2026",
|
|
3592
|
+
"marketing.content.mediaLabel": "Media asset",
|
|
3593
|
+
"marketing.content.mediaUpload": "Upload media",
|
|
3594
|
+
"marketing.content.mediaHint": "Image or video for this post",
|
|
3595
|
+
"marketing.content.mediaReplace": "Replace",
|
|
3596
|
+
"marketing.content.mediaRemove": "Remove",
|
|
3597
|
+
"marketing.content.mediaUnavailable": "Media upload requires a connected Supabase project.",
|
|
3598
|
+
"marketing.content.editAccountTitle": "Account settings",
|
|
3599
|
+
"marketing.content.accountSettings": "Account settings",
|
|
3600
|
+
"marketing.content.deleteAccount": "Delete account",
|
|
3601
|
+
"marketing.content.deleteAccountConfirm": "The account and all of its content plans and posts will be permanently deleted.",
|
|
3602
|
+
"marketing.content.noAccountsAvailable": "No accounts available",
|
|
3603
|
+
"marketing.content.addAccountTitle": "New account",
|
|
3604
|
+
"marketing.content.nameLabel": "Name",
|
|
3605
|
+
"marketing.content.handleLabel": "Handle",
|
|
3606
|
+
"marketing.content.handlePlaceholder": "@handle (optional)",
|
|
3607
|
+
"marketing.content.viewBoard": "Weekly",
|
|
3608
|
+
"marketing.content.viewCalendar": "Monthly",
|
|
3609
|
+
"marketing.content.viewList": "List",
|
|
3610
|
+
"marketing.content.unscheduled": "Unscheduled",
|
|
3611
|
+
"marketing.content.noPosts": "No posts yet \u2014 add your first one from any week.",
|
|
3612
|
+
"marketing.content.listTitle": "Title",
|
|
3613
|
+
"marketing.content.listWeek": "Week",
|
|
3614
|
+
"marketing.content.listDate": "Date",
|
|
3615
|
+
"marketing.content.listFormat": "Format",
|
|
3616
|
+
"marketing.content.listStatus": "Status",
|
|
3617
|
+
"marketing.content.noAccountsHint": "Create the account (e.g. your Instagram profile) to start planning content.",
|
|
3618
|
+
"marketing.content.accountNamePlaceholder": "e.g. @myclinic",
|
|
3619
|
+
"marketing.content.createAccount": "Create account",
|
|
3620
|
+
"marketing.content.defaultPlanName": "Content plan \u2014 8 weeks",
|
|
3621
|
+
"marketing.content.brief": "Brief",
|
|
3622
|
+
"marketing.content.planName": "Plan name",
|
|
3623
|
+
"marketing.content.objective": "Objective",
|
|
3624
|
+
"marketing.content.objectivePlaceholder": "e.g. generate evaluation bookings",
|
|
3625
|
+
"marketing.content.tone": "Tone of voice",
|
|
3626
|
+
"marketing.content.tonePlaceholder": "e.g. approachable authority, plain language",
|
|
3627
|
+
"marketing.content.pillars": "Content pillars",
|
|
3628
|
+
"marketing.content.pillarPlaceholder": "Add pillar\u2026",
|
|
3629
|
+
"marketing.content.formatsLabel": "Formats",
|
|
3630
|
+
"marketing.content.weeklyFrequency": "Posts per week",
|
|
3631
|
+
"marketing.content.weeksCount": "Weeks",
|
|
3632
|
+
"marketing.content.briefBody": "Brief (markdown)",
|
|
3633
|
+
"marketing.content.briefPlaceholder": "Describe the strategy, references and what the content should achieve\u2026",
|
|
3634
|
+
"marketing.content.formatLabel": "Format",
|
|
3635
|
+
"marketing.content.statusLabel": "Status",
|
|
3636
|
+
"marketing.content.scheduledDate": "Planned date",
|
|
3637
|
+
"marketing.content.hook": "Hook",
|
|
3638
|
+
"marketing.content.hookPlaceholder": "The first line that grabs attention\u2026",
|
|
3639
|
+
"marketing.content.cta": "CTA",
|
|
3640
|
+
"marketing.content.ctaPlaceholder": "e.g. Book your evaluation",
|
|
3641
|
+
"marketing.content.titlePlaceholder": "Post title\u2026",
|
|
3642
|
+
"marketing.content.bodyPlaceholder": "Post script / plan in markdown\u2026",
|
|
3643
|
+
"marketing.content.insertTemplate": "Insert script template",
|
|
3644
|
+
"marketing.content.save": "Save",
|
|
3645
|
+
"marketing.content.cancel": "Cancel",
|
|
3646
|
+
"marketing.content.edit": "Edit",
|
|
3647
|
+
"marketing.content.preview": "Preview",
|
|
3648
|
+
"marketing.content.deletePost": "Delete post",
|
|
3649
|
+
"marketing.content.deletePostConfirm": "This post and its script will be permanently removed.",
|
|
3650
|
+
"marketing.content.format.reel": "Reel",
|
|
3651
|
+
"marketing.content.format.static": "Static",
|
|
3652
|
+
"marketing.content.format.carousel": "Carousel",
|
|
3653
|
+
"marketing.content.format.story": "Story",
|
|
3654
|
+
"marketing.content.format.video": "Video",
|
|
3655
|
+
"marketing.content.format.live": "Live",
|
|
3656
|
+
"marketing.content.format.article": "Article",
|
|
3657
|
+
"marketing.content.platformsLabel": "Platforms",
|
|
3658
|
+
"marketing.content.status.idea": "idea",
|
|
3659
|
+
"marketing.content.status.script": "script",
|
|
3660
|
+
"marketing.content.status.recording": "recording",
|
|
3661
|
+
"marketing.content.status.editing": "editing",
|
|
3662
|
+
"marketing.content.status.scheduled": "scheduled",
|
|
3663
|
+
"marketing.content.status.published": "published"
|
|
1144
3664
|
};
|
|
1145
3665
|
|
|
1146
3666
|
// src/locales/pt-BR.ts
|
|
@@ -1216,7 +3736,102 @@ var ptBR = {
|
|
|
1216
3736
|
"marketing.settings.autoSyncDesc": "Importar convers\xF5es automaticamente da fonte.",
|
|
1217
3737
|
"marketing.settings.assisted": "Contar convers\xF5es assistidas",
|
|
1218
3738
|
"marketing.settings.assistedDesc": "Creditar canais que contribu\xEDram para uma convers\xE3o.",
|
|
1219
|
-
"marketing.settings.channelTrackDesc": "Monitorar esta fonte de aquisi\xE7\xE3o."
|
|
3739
|
+
"marketing.settings.channelTrackDesc": "Monitorar esta fonte de aquisi\xE7\xE3o.",
|
|
3740
|
+
// content planner
|
|
3741
|
+
"marketing.content.title": "Conte\xFAdo",
|
|
3742
|
+
"marketing.content.week": "Semana",
|
|
3743
|
+
"marketing.content.month": "M\xEAs",
|
|
3744
|
+
"marketing.content.newPost": "Novo post",
|
|
3745
|
+
"marketing.content.untitled": "Sem t\xEDtulo",
|
|
3746
|
+
"marketing.content.account": "Conta",
|
|
3747
|
+
"marketing.content.addAccount": "Adicionar conta",
|
|
3748
|
+
"marketing.content.previewNow": "Agora",
|
|
3749
|
+
"marketing.content.previewLike": "Curtir",
|
|
3750
|
+
"marketing.content.previewComment": "Comentar",
|
|
3751
|
+
"marketing.content.previewShare": "Compartilhar",
|
|
3752
|
+
"marketing.content.showLess": "mostrar menos",
|
|
3753
|
+
"marketing.content.checklistTitle": "Checklist de grava\xE7\xE3o",
|
|
3754
|
+
"marketing.content.checklistDescription": "Opera\xE7\xE3o do dia de grava\xE7\xE3o",
|
|
3755
|
+
"marketing.content.checklistOpen": "Checklist",
|
|
3756
|
+
"marketing.content.checklistEmpty": "Nenhum item ainda \u2014 gere a partir do roteiro ou adicione o seu abaixo.",
|
|
3757
|
+
"marketing.content.checklistAddPlaceholder": "Novo item\u2026",
|
|
3758
|
+
"marketing.content.checklistAdd": "Adicionar item",
|
|
3759
|
+
"marketing.content.checklistGenerate": "Gerar do roteiro",
|
|
3760
|
+
"marketing.content.scriptLabel": "Roteiro",
|
|
3761
|
+
"marketing.content.captionLabel": "Legenda",
|
|
3762
|
+
"marketing.content.captionPlaceholder": "Escreva a legenda do post\u2026",
|
|
3763
|
+
"marketing.content.mediaLabel": "Arte / m\xEDdia",
|
|
3764
|
+
"marketing.content.mediaUpload": "Enviar m\xEDdia",
|
|
3765
|
+
"marketing.content.mediaHint": "Imagem ou v\xEDdeo deste post",
|
|
3766
|
+
"marketing.content.mediaReplace": "Substituir",
|
|
3767
|
+
"marketing.content.mediaRemove": "Remover",
|
|
3768
|
+
"marketing.content.mediaUnavailable": "O upload de m\xEDdia requer um projeto Supabase conectado.",
|
|
3769
|
+
"marketing.content.editAccountTitle": "Configura\xE7\xF5es da conta",
|
|
3770
|
+
"marketing.content.accountSettings": "Configura\xE7\xF5es da conta",
|
|
3771
|
+
"marketing.content.deleteAccount": "Excluir conta",
|
|
3772
|
+
"marketing.content.deleteAccountConfirm": "A conta e todos os planos e posts de conte\xFAdo dela ser\xE3o exclu\xEDdos permanentemente.",
|
|
3773
|
+
"marketing.content.noAccountsAvailable": "Nenhuma conta dispon\xEDvel",
|
|
3774
|
+
"marketing.content.addAccountTitle": "Nova conta",
|
|
3775
|
+
"marketing.content.nameLabel": "Nome",
|
|
3776
|
+
"marketing.content.handleLabel": "Usu\xE1rio",
|
|
3777
|
+
"marketing.content.handlePlaceholder": "@usuario (opcional)",
|
|
3778
|
+
"marketing.content.viewBoard": "Semanal",
|
|
3779
|
+
"marketing.content.viewCalendar": "Mensal",
|
|
3780
|
+
"marketing.content.viewList": "Lista",
|
|
3781
|
+
"marketing.content.unscheduled": "Sem data",
|
|
3782
|
+
"marketing.content.noPosts": "Nenhum post ainda \u2014 adicione o primeiro em qualquer semana.",
|
|
3783
|
+
"marketing.content.listTitle": "T\xEDtulo",
|
|
3784
|
+
"marketing.content.listWeek": "Semana",
|
|
3785
|
+
"marketing.content.listDate": "Data",
|
|
3786
|
+
"marketing.content.listFormat": "Formato",
|
|
3787
|
+
"marketing.content.listStatus": "Status",
|
|
3788
|
+
"marketing.content.noAccountsHint": "Crie a conta (ex.: o perfil do Instagram) para come\xE7ar a planejar o conte\xFAdo.",
|
|
3789
|
+
"marketing.content.accountNamePlaceholder": "ex.: @minhaclinica",
|
|
3790
|
+
"marketing.content.createAccount": "Criar conta",
|
|
3791
|
+
"marketing.content.defaultPlanName": "Plano de conte\xFAdo \u2014 8 semanas",
|
|
3792
|
+
"marketing.content.brief": "Briefing",
|
|
3793
|
+
"marketing.content.planName": "Nome do plano",
|
|
3794
|
+
"marketing.content.objective": "Objetivo",
|
|
3795
|
+
"marketing.content.objectivePlaceholder": "ex.: gerar agendamentos de avalia\xE7\xE3o",
|
|
3796
|
+
"marketing.content.tone": "Tom de voz",
|
|
3797
|
+
"marketing.content.tonePlaceholder": "ex.: autoridade acess\xEDvel, linguagem simples",
|
|
3798
|
+
"marketing.content.pillars": "Pilares de conte\xFAdo",
|
|
3799
|
+
"marketing.content.pillarPlaceholder": "Adicionar pilar\u2026",
|
|
3800
|
+
"marketing.content.formatsLabel": "Formatos",
|
|
3801
|
+
"marketing.content.weeklyFrequency": "Posts por semana",
|
|
3802
|
+
"marketing.content.weeksCount": "Semanas",
|
|
3803
|
+
"marketing.content.briefBody": "Briefing (markdown)",
|
|
3804
|
+
"marketing.content.briefPlaceholder": "Descreva a estrat\xE9gia, refer\xEAncias e o que se quer com o conte\xFAdo\u2026",
|
|
3805
|
+
"marketing.content.formatLabel": "Formato",
|
|
3806
|
+
"marketing.content.statusLabel": "Status",
|
|
3807
|
+
"marketing.content.scheduledDate": "Data prevista",
|
|
3808
|
+
"marketing.content.hook": "Gancho",
|
|
3809
|
+
"marketing.content.hookPlaceholder": "A primeira frase que segura a aten\xE7\xE3o\u2026",
|
|
3810
|
+
"marketing.content.cta": "CTA",
|
|
3811
|
+
"marketing.content.ctaPlaceholder": "ex.: Agende sua avalia\xE7\xE3o",
|
|
3812
|
+
"marketing.content.titlePlaceholder": "T\xEDtulo do post\u2026",
|
|
3813
|
+
"marketing.content.bodyPlaceholder": "Roteiro / plano do post em markdown\u2026",
|
|
3814
|
+
"marketing.content.insertTemplate": "Inserir modelo de roteiro",
|
|
3815
|
+
"marketing.content.save": "Salvar",
|
|
3816
|
+
"marketing.content.cancel": "Cancelar",
|
|
3817
|
+
"marketing.content.edit": "Editar",
|
|
3818
|
+
"marketing.content.preview": "Visualizar",
|
|
3819
|
+
"marketing.content.deletePost": "Excluir post",
|
|
3820
|
+
"marketing.content.deletePostConfirm": "Este post e seu roteiro ser\xE3o removidos permanentemente.",
|
|
3821
|
+
"marketing.content.format.reel": "Reel",
|
|
3822
|
+
"marketing.content.format.static": "Est\xE1tico",
|
|
3823
|
+
"marketing.content.format.carousel": "Carrossel",
|
|
3824
|
+
"marketing.content.format.story": "Story",
|
|
3825
|
+
"marketing.content.format.video": "V\xEDdeo",
|
|
3826
|
+
"marketing.content.format.live": "Live",
|
|
3827
|
+
"marketing.content.format.article": "Artigo",
|
|
3828
|
+
"marketing.content.platformsLabel": "Plataformas",
|
|
3829
|
+
"marketing.content.status.idea": "ideia",
|
|
3830
|
+
"marketing.content.status.script": "roteiro",
|
|
3831
|
+
"marketing.content.status.recording": "grava\xE7\xE3o",
|
|
3832
|
+
"marketing.content.status.editing": "edi\xE7\xE3o",
|
|
3833
|
+
"marketing.content.status.scheduled": "agendado",
|
|
3834
|
+
"marketing.content.status.published": "publicado"
|
|
1220
3835
|
};
|
|
1221
3836
|
|
|
1222
3837
|
// src/locales/index.ts
|
|
@@ -1234,6 +3849,7 @@ var DEFAULT_LABELS = {
|
|
|
1234
3849
|
campaigns: "Campaigns",
|
|
1235
3850
|
funnel: "Funnel",
|
|
1236
3851
|
landingPages: "Landing pages",
|
|
3852
|
+
content: "Content",
|
|
1237
3853
|
settings: "Settings"
|
|
1238
3854
|
};
|
|
1239
3855
|
function resolveConfig(options) {
|
|
@@ -1258,20 +3874,28 @@ function createMarketingPlugin(options) {
|
|
|
1258
3874
|
const provider = options?.dataProvider ?? createSafeProvider(config, domain);
|
|
1259
3875
|
const store = createMarketingStore(provider);
|
|
1260
3876
|
const dashboardWidgets = createMarketingDashboardWidgets({ config, provider, store });
|
|
1261
|
-
const
|
|
3877
|
+
const contentProvider = options?.contentPlanner?.dataProvider ?? createSafeDataProvider(
|
|
3878
|
+
() => createSupabaseContentPlannerProvider(),
|
|
3879
|
+
() => createMockContentPlannerProvider()
|
|
3880
|
+
);
|
|
3881
|
+
const contentStore = createContentPlannerStore(contentProvider);
|
|
3882
|
+
const PageComponent = () => React8.createElement(
|
|
1262
3883
|
MarketingContextProvider,
|
|
1263
3884
|
{ config, provider, store },
|
|
1264
|
-
|
|
3885
|
+
React8.createElement(MarketingPage, { config, provider, store, contentProvider, contentStore })
|
|
1265
3886
|
);
|
|
1266
3887
|
PageComponent.displayName = "MarketingPage";
|
|
1267
|
-
const marketingRegistries =
|
|
1268
|
-
|
|
3888
|
+
const marketingRegistries = [
|
|
3889
|
+
...buildMarketingRegistries(config.channels),
|
|
3890
|
+
...options?.settingsRegistries ?? []
|
|
3891
|
+
];
|
|
3892
|
+
const SettingsComponent = () => React8.createElement(
|
|
1269
3893
|
MarketingContextProvider,
|
|
1270
3894
|
{ config, provider, store },
|
|
1271
|
-
|
|
3895
|
+
React8.createElement(PluginSettingsPanel, {
|
|
1272
3896
|
title: config.labels.settings ?? config.labels.pageTitle,
|
|
1273
3897
|
subtitle: config.labels.pageSubtitle,
|
|
1274
|
-
generalSettings:
|
|
3898
|
+
generalSettings: React8.createElement(SettingsView),
|
|
1275
3899
|
registries: marketingRegistries,
|
|
1276
3900
|
routeBase: "/settings/marketing"
|
|
1277
3901
|
})
|
|
@@ -1288,7 +3912,8 @@ function createMarketingPlugin(options) {
|
|
|
1288
3912
|
dependencies: [],
|
|
1289
3913
|
declaredFeatures: [
|
|
1290
3914
|
{ id: "marketing", label: config.labels.pageTitle, group: "Engage" },
|
|
1291
|
-
...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages, group: "Engage" }] : []
|
|
3915
|
+
...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages, group: "Engage" }] : [],
|
|
3916
|
+
...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : []
|
|
1292
3917
|
],
|
|
1293
3918
|
navigation: [
|
|
1294
3919
|
{
|
|
@@ -1313,7 +3938,30 @@ function createMarketingPlugin(options) {
|
|
|
1313
3938
|
{ name: "marketing.conversion.tracked", description: "A conversion was attributed to a channel/campaign" },
|
|
1314
3939
|
{ name: "marketing.campaign.created", description: "A campaign was created" },
|
|
1315
3940
|
{ name: "marketing.campaign.updated", description: "A campaign was updated" },
|
|
1316
|
-
{ name: "marketing.channel.synced", description: "A channel pulled fresh attribution data" }
|
|
3941
|
+
{ name: "marketing.channel.synced", description: "A channel pulled fresh attribution data" },
|
|
3942
|
+
{ name: "marketing.content.plan.updated", description: "A content plan (brief/config) was updated" },
|
|
3943
|
+
{ name: "marketing.content.post.created", description: "A content post was created" },
|
|
3944
|
+
{ name: "marketing.content.post.updated", description: "A content post was updated" }
|
|
3945
|
+
],
|
|
3946
|
+
migrations: [
|
|
3947
|
+
{
|
|
3948
|
+
id: "marketing-001-content-planner",
|
|
3949
|
+
version: "1.0.0",
|
|
3950
|
+
sql: MIGRATION_001_CONTENT_PLANNER,
|
|
3951
|
+
description: "Create mkt_social_accounts, mkt_content_plans and mkt_content_posts tables"
|
|
3952
|
+
},
|
|
3953
|
+
{
|
|
3954
|
+
id: "marketing-002-multi-platform",
|
|
3955
|
+
version: "1.1.0",
|
|
3956
|
+
sql: MIGRATION_002_MULTI_PLATFORM,
|
|
3957
|
+
description: "Accounts publish to many platforms (platforms[]); posts gain optional platform targets"
|
|
3958
|
+
},
|
|
3959
|
+
{
|
|
3960
|
+
id: "marketing-003-recording-ops",
|
|
3961
|
+
version: "1.2.0",
|
|
3962
|
+
sql: MIGRATION_003_RECORDING_OPS,
|
|
3963
|
+
description: "Recording-day checklist + media asset URL on posts; mkt-media storage bucket"
|
|
3964
|
+
}
|
|
1317
3965
|
],
|
|
1318
3966
|
aiTools: [
|
|
1319
3967
|
{
|
|
@@ -1385,6 +4033,6 @@ function createMarketingPlugin(options) {
|
|
|
1385
4033
|
};
|
|
1386
4034
|
}
|
|
1387
4035
|
|
|
1388
|
-
export { MARKETING_PRESETS, createMarketingPlugin, createMockMarketingProvider };
|
|
4036
|
+
export { MARKETING_PRESETS, createMarketingPlugin, createMockContentPlannerProvider, createMockMarketingProvider, createSupabaseContentPlannerProvider };
|
|
1389
4037
|
//# sourceMappingURL=index.js.map
|
|
1390
4038
|
//# sourceMappingURL=index.js.map
|