From 38f55d9204541ec41df960a441d38906397841a2 Mon Sep 17 00:00:00 2001 From: Kevin Lago Date: Thu, 30 Jul 2026 13:51:14 -0400 Subject: [PATCH] feat(marketer): campaign/content draft-approve-schedule-publish loop (#3148/#3149) Adds the Marketer feature's frontend: a pure domain model + state machine for campaigns/content items (draft -> approved -> scheduled -> published, #3148), compliance guardrails that block approval on noncompliant content (missing unsubscribe/sender identity, PII, over-length social posts, #3150), a channel-status view over the mcp feature's server catalog (#3146), an analytics read-back with graceful degrade when the backend isn't wired yet (#3149), and the workspace UI tying it together. Not yet mounted to a rail Workspace/Settings page or folded into the composed app store -- both are narrow follow-up integration steps outside this feature's write scope. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01WDc7B2fkQxzAjhZPRfTRQ7 --- src/features/marketer/ContentDrawer.tsx | 131 +++++++++++ src/features/marketer/MarketerStatus.tsx | 27 +++ src/features/marketer/MarketerWorkspace.tsx | 219 +++++++++++++++++++ src/features/marketer/index.ts | 19 ++ src/features/marketer/lib/api.test.ts | 52 +++++ src/features/marketer/lib/api.ts | 46 ++++ src/features/marketer/lib/campaign.test.ts | 104 +++++++++ src/features/marketer/lib/campaign.ts | 133 +++++++++++ src/features/marketer/lib/channels.test.ts | 45 ++++ src/features/marketer/lib/channels.ts | 51 +++++ src/features/marketer/lib/compliance.test.ts | 59 +++++ src/features/marketer/lib/compliance.ts | 61 ++++++ src/features/marketer/lib/metrics.test.ts | 45 ++++ src/features/marketer/lib/metrics.ts | 49 +++++ src/features/marketer/marketer.test.tsx | 72 ++++++ src/features/marketer/store.test.ts | 103 +++++++++ src/features/marketer/store.ts | 147 +++++++++++++ 17 files changed, 1363 insertions(+) create mode 100644 src/features/marketer/ContentDrawer.tsx create mode 100644 src/features/marketer/MarketerStatus.tsx create mode 100644 src/features/marketer/MarketerWorkspace.tsx create mode 100644 src/features/marketer/index.ts create mode 100644 src/features/marketer/lib/api.test.ts create mode 100644 src/features/marketer/lib/api.ts create mode 100644 src/features/marketer/lib/campaign.test.ts create mode 100644 src/features/marketer/lib/campaign.ts create mode 100644 src/features/marketer/lib/channels.test.ts create mode 100644 src/features/marketer/lib/channels.ts create mode 100644 src/features/marketer/lib/compliance.test.ts create mode 100644 src/features/marketer/lib/compliance.ts create mode 100644 src/features/marketer/lib/metrics.test.ts create mode 100644 src/features/marketer/lib/metrics.ts create mode 100644 src/features/marketer/marketer.test.tsx create mode 100644 src/features/marketer/store.test.ts create mode 100644 src/features/marketer/store.ts diff --git a/src/features/marketer/ContentDrawer.tsx b/src/features/marketer/ContentDrawer.tsx new file mode 100644 index 00000000..042e5d3a --- /dev/null +++ b/src/features/marketer/ContentDrawer.tsx @@ -0,0 +1,131 @@ +import { useState } from "react"; +import { Pane } from "@/shared/ui/overlay/Pane"; +import { Chip } from "@/shared/ui/data/Chip"; +import { InlineError } from "@/shared/ui/feedback/InlineError"; +import { Button } from "@/shared/ui/controls/Button"; +import { TextField, TextArea, Field } from "@/shared/ui/controls/Field"; +import { SegmentedControl } from "@/shared/ui/controls/SegmentedControl"; +import { Stack } from "@/shared/ui/layout/Stack"; +import { Row } from "@/shared/ui/layout/Row"; +import { Box } from "@/shared/ui/layout/Box"; +import { Text } from "@/shared/ui/typography/Text"; +import { complianceViolations } from "./lib/compliance"; +import type { ContentItem, ContentStatus, ChannelKind } from "./lib/campaign"; + +const STATUS_TONE: Record = { + draft: "neutral", + approved: "info", + scheduled: "accent", + published: "success", +}; + +export interface ContentDrawerProps { + item: ContentItem | null; + onClose: () => void; + onPatch: (patch: Partial) => void; + onApprove: () => { ok: boolean }; + onSchedule: (whenIso: string) => void; + onPublish: () => void; + publishing?: boolean; +} + +/** The compose/approve/schedule/publish editor for one content item (#3148's loop). Shows the + * compliance guardrail violations (#3150) blocking approval, and the action available for the + * item's current status — never lets the user reach for an action the state machine forbids. */ +export function ContentDrawer({ item, onClose, onPatch, onApprove, onSchedule, onPublish, publishing }: ContentDrawerProps) { + const [scheduleAt, setScheduleAt] = useState(""); + const violations = item ? complianceViolations(item) : []; + const editable = item?.status === "draft"; + + return ( + + {item.status} + {item.channel} + + )} + body={item && ( + + {editable && ( + + ({ + label: k, on: item.channelKind === k, onClick: () => onPatch({ channelKind: k }), + }))} + /> + + )} + {item.channelKind === "email" && ( + onPatch({ subject: v })} disabled={!editable} /> + )} +