@fayz-ai/plugin-marketing 0.2.0 → 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.
Files changed (76) hide show
  1. package/dist/MarketingContext.d.ts +1 -0
  2. package/dist/MarketingContext.d.ts.map +1 -1
  3. package/dist/MarketingPage.d.ts +5 -1
  4. package/dist/MarketingPage.d.ts.map +1 -1
  5. package/dist/components/icons.d.ts +4 -0
  6. package/dist/components/icons.d.ts.map +1 -1
  7. package/dist/data/contentMedia.d.ts +3 -0
  8. package/dist/data/contentMedia.d.ts.map +1 -0
  9. package/dist/data/contentMock.d.ts +3 -0
  10. package/dist/data/contentMock.d.ts.map +1 -0
  11. package/dist/data/contentSupabase.d.ts +3 -0
  12. package/dist/data/contentSupabase.d.ts.map +1 -0
  13. package/dist/data/contentTypes.d.ts +111 -0
  14. package/dist/data/contentTypes.d.ts.map +1 -0
  15. package/dist/index.cjs +2776 -129
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.ts +8 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2778 -133
  20. package/dist/index.js.map +1 -1
  21. package/dist/locales/en.d.ts.map +1 -1
  22. package/dist/locales/pt-BR.d.ts.map +1 -1
  23. package/dist/migrations/index.d.ts +4 -0
  24. package/dist/migrations/index.d.ts.map +1 -0
  25. package/dist/presets.d.ts +2 -0
  26. package/dist/presets.d.ts.map +1 -1
  27. package/dist/schema/index.d.ts +717 -0
  28. package/dist/schema/index.d.ts.map +1 -0
  29. package/dist/schema.cjs +65 -0
  30. package/dist/schema.cjs.map +1 -0
  31. package/dist/schema.js +61 -0
  32. package/dist/schema.js.map +1 -0
  33. package/dist/views/content/ContentPlannerContext.d.ts +13 -0
  34. package/dist/views/content/ContentPlannerContext.d.ts.map +1 -0
  35. package/dist/views/content/ContentView.d.ts +5 -0
  36. package/dist/views/content/ContentView.d.ts.map +1 -0
  37. package/dist/views/content/PlanBriefView.d.ts +12 -0
  38. package/dist/views/content/PlanBriefView.d.ts.map +1 -0
  39. package/dist/views/content/PostPage.d.ts +6 -0
  40. package/dist/views/content/PostPage.d.ts.map +1 -0
  41. package/dist/views/content/PostSocialPreview.d.ts +11 -0
  42. package/dist/views/content/PostSocialPreview.d.ts.map +1 -0
  43. package/dist/views/content/RecordingChecklist.d.ts +6 -0
  44. package/dist/views/content/RecordingChecklist.d.ts.map +1 -0
  45. package/dist/views/content/contentBits.d.ts +28 -0
  46. package/dist/views/content/contentBits.d.ts.map +1 -0
  47. package/dist/views/content/contentStore.d.ts +28 -0
  48. package/dist/views/content/contentStore.d.ts.map +1 -0
  49. package/dist/views/content/templates.d.ts +3 -0
  50. package/dist/views/content/templates.d.ts.map +1 -0
  51. package/package.json +14 -4
  52. package/src/MarketingContext.tsx +1 -0
  53. package/src/MarketingPage.tsx +23 -3
  54. package/src/components/icons.tsx +19 -0
  55. package/src/data/contentMedia.ts +25 -0
  56. package/src/data/contentMock.ts +253 -0
  57. package/src/data/contentSupabase.ts +262 -0
  58. package/src/data/contentTypes.ts +138 -0
  59. package/src/index.ts +55 -2
  60. package/src/locales/en.ts +95 -0
  61. package/src/locales/pt-BR.ts +95 -0
  62. package/src/migrations/001_content_planner.sql +94 -0
  63. package/src/migrations/002_multi_platform.sql +25 -0
  64. package/src/migrations/003_recording_ops.sql +25 -0
  65. package/src/migrations/index.ts +152 -0
  66. package/src/presets.ts +5 -3
  67. package/src/schema/index.ts +69 -0
  68. package/src/views/content/ContentPlannerContext.tsx +13 -0
  69. package/src/views/content/ContentView.tsx +901 -0
  70. package/src/views/content/PlanBriefView.tsx +268 -0
  71. package/src/views/content/PostPage.tsx +370 -0
  72. package/src/views/content/PostSocialPreview.tsx +212 -0
  73. package/src/views/content/RecordingChecklist.tsx +145 -0
  74. package/src/views/content/contentBits.tsx +114 -0
  75. package/src/views/content/contentStore.ts +160 -0
  76. package/src/views/content/templates.ts +172 -0
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import React4, { useEffect } from 'react';
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, Input, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, ModalFooter, KpiCard as KpiCard$1, Card, CardHeader, CardTitle, CardContent, TableWidget } from '@fayz-ai/ui';
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 React4.useMemo(() => new Map(channels.map((c) => [c.id, c])), [channels]);
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 = React4.useMemo(() => [
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] = React4.useState([]);
204
- React4.useEffect(() => {
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 = React4.useMemo(() => [
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] = React4.useState("");
247
- const [channelId, setChannelId] = React4.useState(channels[0]?.id ?? "");
248
- const [status, setStatus] = React4.useState("active");
249
- const [spend, setSpend] = React4.useState("");
250
- const [saving, setSaving] = React4.useState(false);
251
- React4.useEffect(() => {
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] = React4.useState(false);
314
- const columns = React4.useMemo(() => [
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
- /* @__PURE__ */ jsx(DataTable, { columns, data: campaigns, emptyMessage: t("marketing.campaigns.empty") }),
356
- /* @__PURE__ */ jsx(CampaignComposer, { open: composerOpen, onClose: () => setComposerOpen(false) })
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 FunnelView() {
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 { conversion } = useMarketingConfig();
362
- const funnel = useMarketingStore((s) => s.funnel);
363
- const max = Math.max(...funnel.map((s) => s.count), 1);
364
- return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-4xl space-y-4", children: [
365
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
366
- /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
367
- t("marketing.funnel.subtitlePrefix"),
368
- " ",
369
- conversion.label.toLowerCase()
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(RangeTabs, {})
372
- ] }),
373
- /* @__PURE__ */ jsxs("div", { className: "rounded-card border border-border bg-card p-5", children: [
374
- /* @__PURE__ */ jsx("div", { className: "space-y-3", children: funnel.map((stage, i) => {
375
- const pct = max > 0 ? Math.max(stage.count / max * 100, 4) : 0;
376
- const prev = funnel[i - 1];
377
- const stepRate = prev && prev.count > 0 ? stage.count / prev.count * 100 : null;
378
- return /* @__PURE__ */ jsxs("div", { children: [
379
- /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center justify-between text-sm", children: [
380
- /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: stage.label }),
381
- /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
382
- formatNumber(stage.count),
383
- stepRate != null && /* @__PURE__ */ jsxs("span", { className: "ml-2 text-xs", children: [
384
- "(",
385
- formatPercent(stepRate, 0),
386
- " ",
387
- t("marketing.funnel.ofPrev"),
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__ */ 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}` } }) })
393
- ] }, stage.id);
394
- }) }),
395
- /* @__PURE__ */ jsxs("div", { className: "mt-5 border-t border-border pt-4 text-sm", children: [
396
- /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
397
- t("marketing.funnel.overall"),
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__ */ 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" })
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
- "channel-detail": 1
2216
+ content: 0,
2217
+ "channel-detail": 1,
2218
+ "content-post": 1,
2219
+ "content-brief": 1
472
2220
  }, "overview");
473
- React4.useEffect(() => {
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 = React4.useMemo(() => [
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] = React4.useState(conversion.source);
520
- const [trackValue, setTrackValue] = React4.useState(true);
521
- const [autoSync, setAutoSync] = React4.useState(true);
522
- const [assisted, setAssisted] = React4.useState(false);
523
- const [tracked, setTracked] = React4.useState(
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(ctx2) {
2477
+ function createMarketingDashboardWidgets(ctx3) {
727
2478
  const withCtx = (Inner) => {
728
- const Wrapped = () => /* @__PURE__ */ jsx(MarketingContextProvider, { config: ctx2.config, provider: ctx2.provider, store: ctx2.store, children: /* @__PURE__ */ jsx(Inner, {}) });
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 counter = 100;
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-${++counter}`,
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,23 +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 PageComponent = () => React4.createElement(
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
- React4.createElement(MarketingPage, { config, provider, store })
3885
+ React8.createElement(MarketingPage, { config, provider, store, contentProvider, contentStore })
1265
3886
  );
1266
3887
  PageComponent.displayName = "MarketingPage";
1267
3888
  const marketingRegistries = [
1268
3889
  ...buildMarketingRegistries(config.channels),
1269
3890
  ...options?.settingsRegistries ?? []
1270
3891
  ];
1271
- const SettingsComponent = () => React4.createElement(
3892
+ const SettingsComponent = () => React8.createElement(
1272
3893
  MarketingContextProvider,
1273
3894
  { config, provider, store },
1274
- React4.createElement(PluginSettingsPanel, {
3895
+ React8.createElement(PluginSettingsPanel, {
1275
3896
  title: config.labels.settings ?? config.labels.pageTitle,
1276
3897
  subtitle: config.labels.pageSubtitle,
1277
- generalSettings: React4.createElement(SettingsView),
3898
+ generalSettings: React8.createElement(SettingsView),
1278
3899
  registries: marketingRegistries,
1279
3900
  routeBase: "/settings/marketing"
1280
3901
  })
@@ -1291,7 +3912,8 @@ function createMarketingPlugin(options) {
1291
3912
  dependencies: [],
1292
3913
  declaredFeatures: [
1293
3914
  { id: "marketing", label: config.labels.pageTitle, group: "Engage" },
1294
- ...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" }] : []
1295
3917
  ],
1296
3918
  navigation: [
1297
3919
  {
@@ -1316,7 +3938,30 @@ function createMarketingPlugin(options) {
1316
3938
  { name: "marketing.conversion.tracked", description: "A conversion was attributed to a channel/campaign" },
1317
3939
  { name: "marketing.campaign.created", description: "A campaign was created" },
1318
3940
  { name: "marketing.campaign.updated", description: "A campaign was updated" },
1319
- { 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
+ }
1320
3965
  ],
1321
3966
  aiTools: [
1322
3967
  {
@@ -1388,6 +4033,6 @@ function createMarketingPlugin(options) {
1388
4033
  };
1389
4034
  }
1390
4035
 
1391
- export { MARKETING_PRESETS, createMarketingPlugin, createMockMarketingProvider };
4036
+ export { MARKETING_PRESETS, createMarketingPlugin, createMockContentPlannerProvider, createMockMarketingProvider, createSupabaseContentPlannerProvider };
1392
4037
  //# sourceMappingURL=index.js.map
1393
4038
  //# sourceMappingURL=index.js.map