@fayz-ai/plugin-marketing 0.9.0 → 0.9.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 (88) hide show
  1. package/README.md +3 -3
  2. package/dist/{MarketingContext.d.ts → context.d.ts} +5 -3
  3. package/dist/context.d.ts.map +1 -0
  4. package/dist/data/mock.d.ts +1 -1
  5. package/dist/data/mock.d.ts.map +1 -1
  6. package/dist/{registries.d.ts → data/registries.d.ts} +1 -1
  7. package/dist/data/registries.d.ts.map +1 -0
  8. package/dist/index.d.ts +4 -4
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +17 -22
  11. package/dist/index.js.map +1 -1
  12. package/dist/lib/format.d.ts.map +1 -0
  13. package/dist/{presets.d.ts → lib/presets.d.ts} +1 -1
  14. package/dist/lib/presets.d.ts.map +1 -0
  15. package/dist/{MarketingPage.d.ts → views/MarketingPage.d.ts} +5 -5
  16. package/dist/views/MarketingPage.d.ts.map +1 -0
  17. package/dist/views/blog/BlogAdminView.d.ts.map +1 -1
  18. package/dist/views/content/ContentPlannerContext.d.ts +1 -1
  19. package/dist/views/content/ContentPlannerContext.d.ts.map +1 -1
  20. package/dist/views/dashboardWidgets.d.ts +1 -1
  21. package/dist/views/dashboardWidgets.d.ts.map +1 -1
  22. package/package.json +6 -9
  23. package/dist/MarketingContext.d.ts.map +0 -1
  24. package/dist/MarketingPage.d.ts.map +0 -1
  25. package/dist/format.d.ts.map +0 -1
  26. package/dist/presets.d.ts.map +0 -1
  27. package/dist/registries.d.ts.map +0 -1
  28. package/src/MarketingContext.tsx +0 -42
  29. package/src/MarketingPage.tsx +0 -139
  30. package/src/components/MarketingBits.tsx +0 -131
  31. package/src/components/icons.tsx +0 -36
  32. package/src/data/blogEntities.tsx +0 -91
  33. package/src/data/capability.test.ts +0 -92
  34. package/src/data/contentMedia.ts +0 -25
  35. package/src/data/contentMock.ts +0 -253
  36. package/src/data/contentSupabase.ts +0 -263
  37. package/src/data/contentTypes.ts +0 -138
  38. package/src/data/mock.ts +0 -308
  39. package/src/data/supabase.ts +0 -613
  40. package/src/data/tables.ts +0 -33
  41. package/src/data/types.ts +0 -97
  42. package/src/format.ts +0 -40
  43. package/src/index.ts +0 -457
  44. package/src/integrations/ranklayer/RANKLAYER.md +0 -40
  45. package/src/integrations/ranklayer/connectorDef.tsx +0 -102
  46. package/src/integrations/ranklayer/data/supabase.ts +0 -106
  47. package/src/integrations/ranklayer/types.ts +0 -16
  48. package/src/locales/en.ts +0 -192
  49. package/src/locales/index.ts +0 -7
  50. package/src/locales/pt-BR.ts +0 -192
  51. package/src/migrations/000_plg_rename.sql +0 -15
  52. package/src/migrations/001_content_planner.sql +0 -94
  53. package/src/migrations/002_multi_platform.sql +0 -25
  54. package/src/migrations/003_recording_ops.sql +0 -25
  55. package/src/migrations/004_ranklayer.sql +0 -50
  56. package/src/migrations/005_analytics_base.sql +0 -60
  57. package/src/migrations/006_analytics_views.sql +0 -68
  58. package/src/migrations/007_agent_rpcs.sql +0 -119
  59. package/src/migrations/008_landing_pages.sql +0 -112
  60. package/src/migrations/index.ts +0 -601
  61. package/src/presets.ts +0 -119
  62. package/src/registries.ts +0 -62
  63. package/src/schema/index.ts +0 -106
  64. package/src/store.ts +0 -102
  65. package/src/types.ts +0 -153
  66. package/src/views/CampaignComposer.tsx +0 -89
  67. package/src/views/CampaignsView.tsx +0 -59
  68. package/src/views/ChannelDetailView.tsx +0 -59
  69. package/src/views/ChannelsView.tsx +0 -46
  70. package/src/views/FunnelView.tsx +0 -56
  71. package/src/views/LandingPageComposer.tsx +0 -201
  72. package/src/views/LandingPagesView.tsx +0 -172
  73. package/src/views/OverviewView.tsx +0 -24
  74. package/src/views/SettingsView.tsx +0 -72
  75. package/src/views/blog/BlogAdminView.tsx +0 -99
  76. package/src/views/blog/BlogEntityForm.tsx +0 -101
  77. package/src/views/blog/useEntityCrud.ts +0 -50
  78. package/src/views/content/ContentPlannerContext.tsx +0 -13
  79. package/src/views/content/ContentView.tsx +0 -908
  80. package/src/views/content/PlanBriefView.tsx +0 -268
  81. package/src/views/content/PostPage.tsx +0 -370
  82. package/src/views/content/PostSocialPreview.tsx +0 -212
  83. package/src/views/content/RecordingChecklist.tsx +0 -145
  84. package/src/views/content/contentBits.tsx +0 -114
  85. package/src/views/content/contentStore.ts +0 -160
  86. package/src/views/content/templates.ts +0 -172
  87. package/src/views/dashboardWidgets.tsx +0 -173
  88. /package/dist/{format.d.ts → lib/format.d.ts} +0 -0
@@ -1,268 +0,0 @@
1
- import React from 'react'
2
- import { X } from 'lucide-react'
3
- import {
4
- Button, Input, MarkdownEditor, SubpageHeader, Checkbox,
5
- Sheet, SheetContent, SheetHeader, SheetBody, SheetFooter, SheetTitle, SheetDescription,
6
- } from '@fayz-ai/ui'
7
- import { useTranslation } from '@fayz-ai/core'
8
- import type { ContentPlan, PostFormat } from '../../data/contentTypes'
9
- import { useContentPlannerStore } from './ContentPlannerContext'
10
- import { POST_FORMATS } from './contentBits'
11
-
12
- // ---------------------------------------------------------------------------
13
- // Plan brief — the "what we want" page: structured strategy fields (pillars,
14
- // objective, tone, formats, cadence) plus a free-form markdown brief.
15
- // ---------------------------------------------------------------------------
16
-
17
- interface Draft {
18
- name: string
19
- objective: string
20
- tone: string
21
- pillars: string[]
22
- formats: PostFormat[]
23
- weeklyFrequency: number
24
- weeksCount: number
25
- briefMd: string
26
- }
27
-
28
- function toDraft(plan: ContentPlan): Draft {
29
- return {
30
- name: plan.name,
31
- objective: plan.objective ?? '',
32
- tone: plan.tone ?? '',
33
- pillars: plan.pillars,
34
- formats: plan.formats,
35
- weeklyFrequency: plan.weeklyFrequency,
36
- weeksCount: plan.weeksCount,
37
- briefMd: plan.briefMd,
38
- }
39
- }
40
-
41
- function Field({ label, children }: { label: string; children: React.ReactNode }) {
42
- return (
43
- <div className="space-y-1">
44
- <label className="text-xs font-medium text-muted-foreground">{label}</label>
45
- {children}
46
- </div>
47
- )
48
- }
49
-
50
- function useBriefDraft() {
51
- const plan = useContentPlannerStore((s) => s.activePlan)
52
- const savePlan = useContentPlannerStore((s) => s.savePlan)
53
-
54
- const [draft, setDraft] = React.useState<Draft | null>(plan ? toDraft(plan) : null)
55
- const [saving, setSaving] = React.useState(false)
56
-
57
- React.useEffect(() => {
58
- if (plan) setDraft(toDraft(plan))
59
- // eslint-disable-next-line react-hooks/exhaustive-deps
60
- }, [plan?.id])
61
-
62
- const patch = (partial: Partial<Draft>) => setDraft((d) => (d ? { ...d, ...partial } : d))
63
- const dirty = !!plan && !!draft && JSON.stringify(draft) !== JSON.stringify(toDraft(plan))
64
-
65
- const save = async () => {
66
- if (!plan || !draft) return
67
- setSaving(true)
68
- try {
69
- await savePlan({
70
- id: plan.id,
71
- name: draft.name,
72
- objective: draft.objective,
73
- tone: draft.tone,
74
- pillars: draft.pillars,
75
- formats: draft.formats,
76
- weeklyFrequency: draft.weeklyFrequency,
77
- weeksCount: draft.weeksCount,
78
- briefMd: draft.briefMd,
79
- })
80
- } finally {
81
- setSaving(false)
82
- }
83
- }
84
-
85
- return { plan, draft, patch, dirty, saving, save }
86
- }
87
-
88
- function BriefFields({ draft, patch }: { draft: Draft; patch: (p: Partial<Draft>) => void }) {
89
- const t = useTranslation()
90
- const [pillarInput, setPillarInput] = React.useState('')
91
-
92
- const addPillar = () => {
93
- const value = pillarInput.trim()
94
- if (!value || draft.pillars.includes(value)) return
95
- patch({ pillars: [...draft.pillars, value] })
96
- setPillarInput('')
97
- }
98
-
99
- const toggleFormat = (format: PostFormat) => {
100
- patch({
101
- formats: draft.formats.includes(format)
102
- ? draft.formats.filter((f) => f !== format)
103
- : [...draft.formats, format],
104
- })
105
- }
106
-
107
- return (
108
- <div className="space-y-5">
109
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
110
- <Field label={t('marketing.content.planName')}>
111
- <Input value={draft.name} onChange={(e) => patch({ name: e.target.value })} />
112
- </Field>
113
- <Field label={t('marketing.content.objective')}>
114
- <Input
115
- value={draft.objective}
116
- onChange={(e) => patch({ objective: e.target.value })}
117
- placeholder={t('marketing.content.objectivePlaceholder')}
118
- />
119
- </Field>
120
- <Field label={t('marketing.content.tone')}>
121
- <Input
122
- value={draft.tone}
123
- onChange={(e) => patch({ tone: e.target.value })}
124
- placeholder={t('marketing.content.tonePlaceholder')}
125
- />
126
- </Field>
127
- <div className="grid grid-cols-2 gap-3">
128
- <Field label={t('marketing.content.weeklyFrequency')}>
129
- <Input
130
- type="number"
131
- min={1}
132
- max={14}
133
- value={draft.weeklyFrequency}
134
- onChange={(e) => patch({ weeklyFrequency: Math.max(1, Number(e.target.value) || 1) })}
135
- />
136
- </Field>
137
- <Field label={t('marketing.content.weeksCount')}>
138
- <Input
139
- type="number"
140
- min={1}
141
- max={52}
142
- value={draft.weeksCount}
143
- onChange={(e) => patch({ weeksCount: Math.max(1, Number(e.target.value) || 1) })}
144
- />
145
- </Field>
146
- </div>
147
- </div>
148
-
149
- <Field label={t('marketing.content.pillars')}>
150
- <div className="flex flex-wrap items-center gap-1.5">
151
- {draft.pillars.map((pillar) => (
152
- <span
153
- key={pillar}
154
- className="inline-flex items-center gap-1 rounded-full bg-muted px-2.5 py-0.5 text-xs font-medium text-foreground"
155
- >
156
- {pillar}
157
- <button
158
- type="button"
159
- onClick={() => patch({ pillars: draft.pillars.filter((p) => p !== pillar) })}
160
- className="text-muted-foreground hover:text-foreground"
161
- >
162
- <X className="h-3 w-3" />
163
- </button>
164
- </span>
165
- ))}
166
- <Input
167
- value={pillarInput}
168
- onChange={(e) => setPillarInput(e.target.value)}
169
- onKeyDown={(e) => {
170
- if (e.key === 'Enter') {
171
- e.preventDefault()
172
- addPillar()
173
- }
174
- }}
175
- onBlur={addPillar}
176
- placeholder={t('marketing.content.pillarPlaceholder')}
177
- className="h-7 w-44 text-xs"
178
- />
179
- </div>
180
- </Field>
181
-
182
- <Field label={t('marketing.content.formatsLabel')}>
183
- <div className="flex flex-wrap gap-4">
184
- {POST_FORMATS.map((format) => (
185
- <label key={format} className="flex items-center gap-2 text-sm text-foreground">
186
- <Checkbox
187
- checked={draft.formats.includes(format)}
188
- onChange={() => toggleFormat(format)}
189
- />
190
- {t(`marketing.content.format.${format}`)}
191
- </label>
192
- ))}
193
- </div>
194
- </Field>
195
-
196
- <Field label={t('marketing.content.briefBody')}>
197
- <MarkdownEditor
198
- value={draft.briefMd}
199
- onChange={(briefMd) => patch({ briefMd })}
200
- placeholder={t('marketing.content.briefPlaceholder')}
201
- editLabel={t('marketing.content.edit')}
202
- previewLabel={t('marketing.content.preview')}
203
- minRows={10}
204
- />
205
- </Field>
206
- </div>
207
- )
208
- }
209
-
210
- /** Full-page variant — kept for the /marketing/content/brief deep link. */
211
- export function PlanBriefView({ onBack }: { onBack: () => void }) {
212
- const t = useTranslation()
213
- const { plan, draft, patch, dirty, saving, save } = useBriefDraft()
214
-
215
- if (!plan || !draft) {
216
- return <div className="mx-auto max-w-3xl text-sm text-muted-foreground">…</div>
217
- }
218
-
219
- return (
220
- <div className="mx-auto max-w-3xl space-y-5">
221
- <SubpageHeader
222
- title={t('marketing.content.brief')}
223
- subtitle={plan.name}
224
- parentLabel={t('marketing.content.title')}
225
- onBack={onBack}
226
- actions={
227
- <Button size="sm" onClick={() => void save()} disabled={!dirty || saving}>
228
- {t('marketing.content.save')}
229
- </Button>
230
- }
231
- />
232
- <BriefFields draft={draft} patch={patch} />
233
- </div>
234
- )
235
- }
236
-
237
- /** Right slide-over variant — the board's Briefing button opens this, keeping
238
- * the calendar in sight while the strategy is edited. */
239
- export function PlanBriefSheet({ open, onClose }: { open: boolean; onClose: () => void }) {
240
- const t = useTranslation()
241
- const { plan, draft, patch, dirty, saving, save } = useBriefDraft()
242
-
243
- return (
244
- <Sheet open={open} onOpenChange={(v) => { if (!v) onClose() }}>
245
- <SheetContent width="max-w-2xl">
246
- <SheetHeader>
247
- <SheetTitle>{t('marketing.content.brief')}</SheetTitle>
248
- {plan && <SheetDescription>{plan.name}</SheetDescription>}
249
- </SheetHeader>
250
- <SheetBody>
251
- {draft ? (
252
- <BriefFields draft={draft} patch={patch} />
253
- ) : (
254
- <p className="text-sm text-muted-foreground">…</p>
255
- )}
256
- </SheetBody>
257
- <SheetFooter>
258
- <Button variant="outline" onClick={onClose} disabled={saving}>
259
- {t('marketing.content.cancel')}
260
- </Button>
261
- <Button onClick={() => void save()} disabled={!dirty || saving}>
262
- {t('marketing.content.save')}
263
- </Button>
264
- </SheetFooter>
265
- </SheetContent>
266
- </Sheet>
267
- )
268
- }
@@ -1,370 +0,0 @@
1
- import React from 'react'
2
- import { ImagePlus, Loader2, X } from 'lucide-react'
3
- import {
4
- Button, Input, MarkdownEditor, SubpageHeader, ConfirmDialog, Skeleton,
5
- ContentSplit, ContentSplitTrigger,
6
- Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
7
- } from '@fayz-ai/ui'
8
- import { useTranslation } from '@fayz-ai/core'
9
- import { canUploadMedia, uploadPostMedia } from '../../data/contentMedia'
10
- import { RecordingChecklist } from './RecordingChecklist'
11
- import { PostSocialPreview } from './PostSocialPreview'
12
- import type { ContentPost, PostFormat, PostStatus } from '../../data/contentTypes'
13
- import { useContentPlannerProvider, useContentPlannerStore } from './ContentPlannerContext'
14
- import { POST_FORMATS, POST_STATUS_ORDER, PlatformPicker, StatusDot } from './contentBits'
15
- import { POST_TEMPLATES } from './templates'
16
-
17
- // ---------------------------------------------------------------------------
18
- // Post page — the Notion-style page behind each board card: title, a compact
19
- // meta row (format / status / date / hook / CTA) and the markdown script body.
20
- // ---------------------------------------------------------------------------
21
-
22
- function MediaAsset({ post, onChanged }: {
23
- post: ContentPost
24
- onChanged: (mediaUrl: string | null) => void
25
- }) {
26
- const t = useTranslation()
27
- const inputRef = React.useRef<HTMLInputElement>(null)
28
- const [busy, setBusy] = React.useState(false)
29
- const isVideo = post.mediaUrl ? /\.(mp4|webm|mov)($|\?)/i.test(post.mediaUrl) : false
30
-
31
- const pick = () => inputRef.current?.click()
32
-
33
- const upload = async (file: File) => {
34
- setBusy(true)
35
- try {
36
- const url = await uploadPostMedia(post.id, file)
37
- onChanged(url)
38
- } finally {
39
- setBusy(false)
40
- }
41
- }
42
-
43
- if (!canUploadMedia()) {
44
- return <p className="text-xs text-muted-foreground">{t('marketing.content.mediaUnavailable')}</p>
45
- }
46
-
47
- return (
48
- <div className="space-y-2">
49
- <input
50
- ref={inputRef}
51
- type="file"
52
- accept="image/*,video/*"
53
- className="hidden"
54
- onChange={(e) => {
55
- const file = e.target.files?.[0]
56
- if (file) void upload(file)
57
- e.target.value = ''
58
- }}
59
- />
60
- {post.mediaUrl ? (
61
- <div className="relative overflow-hidden rounded-card border border-border bg-muted/20">
62
- {isVideo ? (
63
- <video src={post.mediaUrl} controls className="max-h-72 w-full object-contain" />
64
- ) : (
65
- <img src={post.mediaUrl} alt="" className="max-h-72 w-full object-contain" />
66
- )}
67
- <div className="flex items-center justify-end gap-2 border-t border-border p-2">
68
- <Button variant="outline" size="sm" onClick={pick} disabled={busy}>
69
- {busy ? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" /> : <ImagePlus className="mr-1.5 h-3.5 w-3.5" />}
70
- {t('marketing.content.mediaReplace')}
71
- </Button>
72
- <Button variant="ghost" size="sm" onClick={() => onChanged(null)} disabled={busy}>
73
- <X className="mr-1 h-3.5 w-3.5" />
74
- {t('marketing.content.mediaRemove')}
75
- </Button>
76
- </div>
77
- </div>
78
- ) : (
79
- <button
80
- type="button"
81
- onClick={pick}
82
- disabled={busy}
83
- 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"
84
- >
85
- {busy ? <Loader2 className="h-5 w-5 animate-spin" /> : <ImagePlus className="h-5 w-5" />}
86
- <span className="text-sm font-medium">{t('marketing.content.mediaUpload')}</span>
87
- <span className="text-xs">{t('marketing.content.mediaHint')}</span>
88
- </button>
89
- )}
90
- </div>
91
- )
92
- }
93
-
94
- interface Draft {
95
- title: string
96
- format: PostFormat
97
- status: PostStatus
98
- scheduledDate: string
99
- platforms: string[]
100
- hook: string
101
- cta: string
102
- contentMd: string
103
- }
104
-
105
- function toDraft(post: ContentPost): Draft {
106
- return {
107
- title: post.title,
108
- format: post.format,
109
- status: post.status,
110
- scheduledDate: post.scheduledDate ?? '',
111
- platforms: post.platforms,
112
- hook: post.hook ?? '',
113
- cta: post.cta ?? '',
114
- contentMd: post.contentMd,
115
- }
116
- }
117
-
118
- export function PostPage({ postId, onBack }: { postId: string; onBack: () => void }) {
119
- const t = useTranslation()
120
- const provider = useContentPlannerProvider()
121
- const storePost = useContentPlannerStore((s) => s.posts.find((p) => p.id === postId))
122
- const savePost = useContentPlannerStore((s) => s.savePost)
123
- const deletePost = useContentPlannerStore((s) => s.deletePost)
124
- const accounts = useContentPlannerStore((s) => s.accounts)
125
- const activeAccountId = useContentPlannerStore((s) => s.activeAccountId)
126
-
127
- const [post, setPost] = React.useState<ContentPost | null>(storePost ?? null)
128
- const [draft, setDraft] = React.useState<Draft | null>(storePost ? toDraft(storePost) : null)
129
- const [saving, setSaving] = React.useState(false)
130
- const [confirmDelete, setConfirmDelete] = React.useState(false)
131
-
132
- // Deep links land here before the board loaded the plan — fetch directly.
133
- React.useEffect(() => {
134
- if (post?.id === postId) return
135
- if (storePost) {
136
- setPost(storePost)
137
- setDraft(toDraft(storePost))
138
- return
139
- }
140
- void provider.getPost(postId).then((fetched) => {
141
- if (fetched) {
142
- setPost(fetched)
143
- setDraft(toDraft(fetched))
144
- }
145
- })
146
- }, [postId, storePost, post?.id, provider])
147
-
148
- if (!post || !draft) {
149
- return (
150
- <div className="mx-auto max-w-3xl space-y-5">
151
- <div className="flex items-center justify-between">
152
- <Skeleton className="h-8 w-56" />
153
- <Skeleton className="h-8 w-40" />
154
- </div>
155
- <Skeleton className="h-10 w-full" />
156
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
157
- {Array.from({ length: 3 }, (_, i) => <Skeleton key={i} className="h-14" />)}
158
- </div>
159
- <Skeleton className="h-64 w-full" />
160
- </div>
161
- )
162
- }
163
-
164
- const dirty =
165
- draft.title !== post.title ||
166
- draft.format !== post.format ||
167
- draft.status !== post.status ||
168
- draft.scheduledDate !== (post.scheduledDate ?? '') ||
169
- draft.platforms.join(',') !== post.platforms.join(',') ||
170
- draft.hook !== (post.hook ?? '') ||
171
- draft.cta !== (post.cta ?? '') ||
172
- draft.contentMd !== post.contentMd
173
-
174
- const patch = (partial: Partial<Draft>) => setDraft((d) => (d ? { ...d, ...partial } : d))
175
-
176
- const save = async () => {
177
- setSaving(true)
178
- try {
179
- const updated = await savePost({
180
- id: post.id,
181
- title: draft.title,
182
- format: draft.format,
183
- status: draft.status,
184
- scheduledDate: draft.scheduledDate || null,
185
- platforms: draft.platforms,
186
- hook: draft.hook,
187
- cta: draft.cta,
188
- contentMd: draft.contentMd,
189
- })
190
- setPost(updated)
191
- setDraft(toDraft(updated))
192
- } finally {
193
- setSaving(false)
194
- }
195
- }
196
-
197
- const insertTemplate = () => {
198
- patch({ contentMd: POST_TEMPLATES[draft.format](draft.title || t('marketing.content.untitled')) })
199
- }
200
-
201
- const isStatic = draft.format === 'static'
202
- const account = accounts.find((a) => a.id === activeAccountId) ?? null
203
-
204
- // Recording day: the checklist panel starts expanded only when today IS the
205
- // post's scheduled date — otherwise it waits collapsed in the rail.
206
- const now = new Date()
207
- const todayISO = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
208
- const isReleaseDay = !!post.scheduledDate && post.scheduledDate === todayISO
209
-
210
- const handleMediaChanged = (mediaUrl: string | null) => {
211
- void savePost({ id: post.id, mediaUrl }).then((updated) => {
212
- setPost(updated)
213
- setDraft((d) => (d ? { ...d } : d))
214
- })
215
- }
216
-
217
- return (
218
- <ContentSplit
219
- key={post.id}
220
- className="mx-auto max-w-5xl"
221
- aside={<RecordingChecklist post={post} />}
222
- asideTitle={t('marketing.content.checklistTitle')}
223
- asideDescription={t('marketing.content.checklistDescription')}
224
- collapsible
225
- defaultCollapsed={!isReleaseDay}
226
- >
227
- <div className="space-y-5">
228
- <SubpageHeader
229
- title={draft.title || t('marketing.content.untitled')}
230
- parentLabel={t('marketing.content.title')}
231
- onBack={onBack}
232
- actions={
233
- <div className="flex items-center gap-2">
234
- <ContentSplitTrigger label={t('marketing.content.checklistOpen')} />
235
- <Button variant="outline" size="sm" onClick={() => setConfirmDelete(true)}>
236
- {t('marketing.content.deletePost')}
237
- </Button>
238
- <Button size="sm" onClick={() => void save()} disabled={!dirty || saving}>
239
- {t('marketing.content.save')}
240
- </Button>
241
- </div>
242
- }
243
- />
244
-
245
- <Input
246
- value={draft.title}
247
- onChange={(e) => patch({ title: e.target.value })}
248
- placeholder={t('marketing.content.titlePlaceholder')}
249
- className="text-base font-medium"
250
- />
251
-
252
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
253
- <div className="space-y-1">
254
- <label className="text-xs font-medium text-muted-foreground">{t('marketing.content.formatLabel')}</label>
255
- <Select value={draft.format} onValueChange={(v) => patch({ format: v as PostFormat })}>
256
- <SelectTrigger><SelectValue /></SelectTrigger>
257
- <SelectContent>
258
- {POST_FORMATS.map((f) => (
259
- <SelectItem key={f} value={f}>{t(`marketing.content.format.${f}`)}</SelectItem>
260
- ))}
261
- </SelectContent>
262
- </Select>
263
- </div>
264
- <div className="space-y-1">
265
- <label className="text-xs font-medium text-muted-foreground">{t('marketing.content.statusLabel')}</label>
266
- <Select value={draft.status} onValueChange={(v) => patch({ status: v as PostStatus })}>
267
- <SelectTrigger><SelectValue /></SelectTrigger>
268
- <SelectContent>
269
- {POST_STATUS_ORDER.map((s) => (
270
- <SelectItem key={s} value={s}>
271
- <span className="flex items-center gap-2">
272
- <StatusDot status={s} />
273
- {t(`marketing.content.status.${s}`)}
274
- </span>
275
- </SelectItem>
276
- ))}
277
- </SelectContent>
278
- </Select>
279
- </div>
280
- <div className="space-y-1">
281
- <label className="text-xs font-medium text-muted-foreground">{t('marketing.content.scheduledDate')}</label>
282
- <Input
283
- type="date"
284
- value={draft.scheduledDate}
285
- onChange={(e) => patch({ scheduledDate: e.target.value })}
286
- />
287
- </div>
288
- <div className="space-y-1 sm:col-span-2">
289
- <label className="text-xs font-medium text-muted-foreground">{t('marketing.content.hook')}</label>
290
- <Input
291
- value={draft.hook}
292
- onChange={(e) => patch({ hook: e.target.value })}
293
- placeholder={t('marketing.content.hookPlaceholder')}
294
- />
295
- </div>
296
- <div className="space-y-1">
297
- <label className="text-xs font-medium text-muted-foreground">{t('marketing.content.cta')}</label>
298
- <Input
299
- value={draft.cta}
300
- onChange={(e) => patch({ cta: e.target.value })}
301
- placeholder={t('marketing.content.ctaPlaceholder')}
302
- />
303
- </div>
304
- <div className="space-y-1 sm:col-span-3">
305
- <label className="text-xs font-medium text-muted-foreground">{t('marketing.content.platformsLabel')}</label>
306
- <PlatformPicker value={draft.platforms} onChange={(platforms) => patch({ platforms })} />
307
- </div>
308
- </div>
309
-
310
- {isStatic && (
311
- <div className="space-y-1">
312
- <label className="text-xs font-medium text-muted-foreground">
313
- {t('marketing.content.mediaLabel')}
314
- </label>
315
- <MediaAsset post={post} onChanged={handleMediaChanged} />
316
- </div>
317
- )}
318
-
319
- <div className="space-y-1">
320
- <div className="flex items-center justify-between">
321
- <label className="text-xs font-medium text-muted-foreground">
322
- {isStatic ? t('marketing.content.captionLabel') : t('marketing.content.scriptLabel')}
323
- </label>
324
- {draft.contentMd.trim() === '' && (
325
- <Button variant="outline" size="sm" onClick={insertTemplate}>
326
- {t('marketing.content.insertTemplate')}
327
- </Button>
328
- )}
329
- </div>
330
- <MarkdownEditor
331
- value={draft.contentMd}
332
- onChange={(contentMd) => patch({ contentMd })}
333
- placeholder={isStatic ? t('marketing.content.captionPlaceholder') : t('marketing.content.bodyPlaceholder')}
334
- editLabel={t('marketing.content.edit')}
335
- previewLabel={t('marketing.content.preview')}
336
- renderPreview={
337
- isStatic
338
- ? (value) => (
339
- <PostSocialPreview
340
- caption={value}
341
- mediaUrl={post.mediaUrl}
342
- accountName={account?.name ?? '@conta'}
343
- accountHandle={account?.handle}
344
- platforms={
345
- draft.platforms.length > 0 ? draft.platforms : account?.platforms ?? []
346
- }
347
- />
348
- )
349
- : undefined
350
- }
351
- />
352
- </div>
353
-
354
- <ConfirmDialog
355
- open={confirmDelete}
356
- variant="destructive"
357
- title={t('marketing.content.deletePost')}
358
- description={t('marketing.content.deletePostConfirm')}
359
- confirmLabel={t('marketing.content.deletePost')}
360
- cancelLabel={t('marketing.content.cancel')}
361
- onCancel={() => setConfirmDelete(false)}
362
- onConfirm={() => {
363
- setConfirmDelete(false)
364
- void deletePost(post.id).then(onBack)
365
- }}
366
- />
367
- </div>
368
- </ContentSplit>
369
- )
370
- }