@growth-labs/cms 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -4
- package/dist/engine/publisher.d.ts +14 -3
- package/dist/engine/publisher.d.ts.map +1 -1
- package/dist/engine/publisher.js +11 -5
- package/dist/engine/publisher.js.map +1 -1
- package/dist/routes/content.d.ts +1 -1
- package/dist/routes/content.d.ts.map +1 -1
- package/dist/routes/content.js +30 -23
- package/dist/routes/content.js.map +1 -1
- package/dist/schema/insights-ingest.d.ts +16 -16
- package/dist/schema/migrations.d.ts +5 -3
- package/dist/schema/migrations.d.ts.map +1 -1
- package/dist/schema/migrations.js +148 -4
- package/dist/schema/migrations.js.map +1 -1
- package/dist/schema/types.d.ts +2 -2
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/ui/commands.d.ts +2 -1
- package/dist/ui/commands.d.ts.map +1 -1
- package/dist/ui/commands.js +1 -2
- package/dist/ui/commands.js.map +1 -1
- package/dist/ui/components/SharePickers.d.ts.map +1 -1
- package/dist/ui/components/SharePickers.js +11 -7
- package/dist/ui/components/SharePickers.js.map +1 -1
- package/dist/ui/editor/ContentForm.d.ts +1 -1
- package/dist/ui/editor/ContentForm.d.ts.map +1 -1
- package/dist/ui/editor/ContentForm.js +5 -5
- package/dist/ui/editor/ContentForm.js.map +1 -1
- package/dist/ui/editor/content-payload.js +2 -2
- package/dist/ui/editor/content-payload.js.map +1 -1
- package/dist/ui/preview/draft-page.js +5 -4
- package/dist/ui/preview/draft-page.js.map +1 -1
- package/dist/ui/screens/EditorScreen.d.ts.map +1 -1
- package/dist/ui/screens/EditorScreen.js +4 -1
- package/dist/ui/screens/EditorScreen.js.map +1 -1
- package/dist/ui/screens/LibraryScreen.d.ts.map +1 -1
- package/dist/ui/screens/LibraryScreen.js +6 -3
- package/dist/ui/screens/LibraryScreen.js.map +1 -1
- package/dist/ui/screens/SocialShareScreen.d.ts.map +1 -1
- package/dist/ui/screens/SocialShareScreen.js +4 -3
- package/dist/ui/screens/SocialShareScreen.js.map +1 -1
- package/dist/ui/screens/content-view.d.ts +2 -1
- package/dist/ui/screens/content-view.d.ts.map +1 -1
- package/dist/ui/screens/content-view.js.map +1 -1
- package/dist/ui/screens/public-url.d.ts.map +1 -1
- package/dist/ui/screens/public-url.js +2 -0
- package/dist/ui/screens/public-url.js.map +1 -1
- package/dist/ui/screens/social-share-data.d.ts +2 -2
- package/dist/ui/screens/social-share-data.d.ts.map +1 -1
- package/dist/ui/screens/social-share-data.js +3 -12
- package/dist/ui/screens/social-share-data.js.map +1 -1
- package/migrations/0001_create_cms_tables.sql +4 -4
- package/migrations/0017_content_pages.sql +138 -0
- package/package.json +1 -1
- package/src/engine/publisher.ts +45 -9
- package/src/routes/content.ts +40 -26
- package/src/schema/migrations.ts +148 -4
- package/src/schema/types.ts +2 -2
- package/src/ui/commands.ts +3 -1
- package/src/ui/components/SharePickers.tsx +13 -10
- package/src/ui/editor/ContentForm.tsx +7 -7
- package/src/ui/editor/content-payload.ts +2 -2
- package/src/ui/preview/draft-page.tsx +7 -4
- package/src/ui/screens/EditorScreen.tsx +13 -7
- package/src/ui/screens/LibraryScreen.tsx +6 -3
- package/src/ui/screens/SocialShareScreen.tsx +4 -2
- package/src/ui/screens/content-view.ts +3 -1
- package/src/ui/screens/public-url.ts +2 -0
- package/src/ui/screens/social-share-data.ts +4 -13
package/src/ui/commands.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/ui/commands.ts — the ⌘K command set + the pure filter. "create"/"jump"
|
|
2
2
|
// are static; "doc" entries are fed in from the /admin/api/search results.
|
|
3
|
+
import type { ContentType } from '../schema/types.js'
|
|
3
4
|
import { ROUTES, type Route } from './hash-router.js'
|
|
4
5
|
|
|
5
6
|
export type Command =
|
|
@@ -8,7 +9,7 @@ export type Command =
|
|
|
8
9
|
label: string
|
|
9
10
|
icon: string
|
|
10
11
|
hint: string
|
|
11
|
-
contentType:
|
|
12
|
+
contentType: ContentType
|
|
12
13
|
}
|
|
13
14
|
| { kind: 'jump'; label: string; icon: string; hint: string; route: Route }
|
|
14
15
|
| { kind: 'doc'; label: string; icon: string; hint: string; id: string }
|
|
@@ -44,6 +45,7 @@ export const baseCommands: Command[] = [
|
|
|
44
45
|
{ kind: 'create', label: 'New article', icon: 'doc', hint: 'Create', contentType: 'article' },
|
|
45
46
|
{ kind: 'create', label: 'New video', icon: 'video', hint: 'Create', contentType: 'video' },
|
|
46
47
|
{ kind: 'create', label: 'New podcast', icon: 'mic', hint: 'Create', contentType: 'podcast' },
|
|
48
|
+
{ kind: 'create', label: 'New page', icon: 'doc', hint: 'Create', contentType: 'page' },
|
|
47
49
|
...ROUTES.map(
|
|
48
50
|
(r): Command => ({
|
|
49
51
|
kind: 'jump',
|
|
@@ -64,7 +64,7 @@ interface ShareLinksListResponse {
|
|
|
64
64
|
// SOURCE OF TRUTH: packages/cms/src/ui/screens/library-data.ts LibraryRow
|
|
65
65
|
interface ContentRow {
|
|
66
66
|
id: string
|
|
67
|
-
type: 'article' | 'video' | 'podcast' | 'newsletter'
|
|
67
|
+
type: 'article' | 'video' | 'podcast' | 'newsletter' | 'page'
|
|
68
68
|
slug: string
|
|
69
69
|
title: string
|
|
70
70
|
authorId: string | null
|
|
@@ -185,6 +185,17 @@ export function ContentPicker({ siteBase, dispatch }: ContentPickerProps) {
|
|
|
185
185
|
setResults([])
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
const iconForContentType = (type: ContentRow['type']): Parameters<typeof Icon>[0]['name'] => {
|
|
189
|
+
const icons: Record<ContentRow['type'], Parameters<typeof Icon>[0]['name']> = {
|
|
190
|
+
article: 'doc',
|
|
191
|
+
video: 'video',
|
|
192
|
+
podcast: 'mic',
|
|
193
|
+
newsletter: 'mail',
|
|
194
|
+
page: 'doc',
|
|
195
|
+
}
|
|
196
|
+
return icons[type]
|
|
197
|
+
}
|
|
198
|
+
|
|
188
199
|
return (
|
|
189
200
|
<div>
|
|
190
201
|
<FieldLabel>Content (pre-fill)</FieldLabel>
|
|
@@ -265,15 +276,7 @@ export function ContentPicker({ siteBase, dispatch }: ContentPickerProps) {
|
|
|
265
276
|
style={dropdownItemStyle}
|
|
266
277
|
>
|
|
267
278
|
<Icon
|
|
268
|
-
name={
|
|
269
|
-
(row.type === 'article'
|
|
270
|
-
? 'doc'
|
|
271
|
-
: row.type === 'video'
|
|
272
|
-
? 'video'
|
|
273
|
-
: row.type === 'podcast'
|
|
274
|
-
? 'mic'
|
|
275
|
-
: 'mail') as Parameters<typeof Icon>[0]['name']
|
|
276
|
-
}
|
|
279
|
+
name={iconForContentType(row.type)}
|
|
277
280
|
size={13}
|
|
278
281
|
style={{ color: 'var(--ink-muted)', flexShrink: 0 }}
|
|
279
282
|
/>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// src/ui/editor/ContentForm.tsx —
|
|
1
|
+
// src/ui/editor/ContentForm.tsx — content form with Tiptap RTE.
|
|
2
2
|
//
|
|
3
3
|
// Handles: title, dek (excerpt), type-specific body block:
|
|
4
|
-
// - article / newsletter → <Rte/> (Tiptap body)
|
|
4
|
+
// - article / newsletter / page → <Rte/> (Tiptap body)
|
|
5
5
|
// - video → media/video block (URL input; the actual R2 multipart upload
|
|
6
6
|
// is wired through the existing /media/multipart routes; here
|
|
7
7
|
// we store a source URL or R2 key)
|
|
@@ -44,7 +44,7 @@ import { docToMarkdown } from './serialize.js'
|
|
|
44
44
|
export interface ContentFormProps {
|
|
45
45
|
/** Existing content id (null = new unsaved doc). */
|
|
46
46
|
docId: string | null
|
|
47
|
-
/** Content type (article | video | podcast | newsletter). */
|
|
47
|
+
/** Content type (article | video | podcast | newsletter | page). */
|
|
48
48
|
contentType: ContentType
|
|
49
49
|
/** Called with the live resolved id once the doc is saved for the first time. */
|
|
50
50
|
onDocCreated?: (id: string) => void
|
|
@@ -825,7 +825,7 @@ export function ContentForm({
|
|
|
825
825
|
/>
|
|
826
826
|
</div>
|
|
827
827
|
) : (
|
|
828
|
-
/* article | newsletter */
|
|
828
|
+
/* article | newsletter | page */
|
|
829
829
|
<>
|
|
830
830
|
{contentType === 'article' && (
|
|
831
831
|
<div style={{ marginBottom: 14 }}>
|
|
@@ -855,8 +855,8 @@ export function ContentForm({
|
|
|
855
855
|
)}
|
|
856
856
|
</div>
|
|
857
857
|
|
|
858
|
-
{/* Footer: word count + read time (only for
|
|
859
|
-
{(contentType === 'article' || contentType === 'newsletter') && (
|
|
858
|
+
{/* Footer: word count + read time (only for prose content) */}
|
|
859
|
+
{(contentType === 'article' || contentType === 'newsletter' || contentType === 'page') && (
|
|
860
860
|
<div style={footer}>
|
|
861
861
|
<Icon name="edit" size={12} style={{ color: 'var(--ink-faint)' }} />
|
|
862
862
|
<span>
|
|
@@ -1216,7 +1216,7 @@ function contentResponseToFormState(
|
|
|
1216
1216
|
next.heroImageId = heroImageId
|
|
1217
1217
|
next.heroImageUrl = heroImageUrl || (heroImageId ? mediaUrlFromId(heroImageId) : null)
|
|
1218
1218
|
|
|
1219
|
-
if (contentType === 'article' || contentType === 'newsletter') {
|
|
1219
|
+
if (contentType === 'article' || contentType === 'newsletter' || contentType === 'page') {
|
|
1220
1220
|
next.body = toStringValue(content.body_markdown) || toStringValue(content.body_html)
|
|
1221
1221
|
}
|
|
1222
1222
|
|
|
@@ -28,7 +28,7 @@ export function canCreateContentDraft(
|
|
|
28
28
|
draft: ContentDraftFields,
|
|
29
29
|
): boolean {
|
|
30
30
|
if (draft.title.trim().length < 3) return false
|
|
31
|
-
if (contentType === 'article' || contentType === 'newsletter') {
|
|
31
|
+
if (contentType === 'article' || contentType === 'newsletter' || contentType === 'page') {
|
|
32
32
|
return draft.body.trim().length > 0
|
|
33
33
|
}
|
|
34
34
|
if (contentType === 'video') return draft.videoUrl.trim().length > 0
|
|
@@ -81,7 +81,7 @@ export function buildContentUpdatePayload(
|
|
|
81
81
|
payload.heroImageId = heroImageId
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
if (contentType === 'article' || contentType === 'newsletter') {
|
|
84
|
+
if (contentType === 'article' || contentType === 'newsletter' || contentType === 'page') {
|
|
85
85
|
const wordCount = countWords(draft.body)
|
|
86
86
|
payload.content = {
|
|
87
87
|
bodyMarkdown: draft.body,
|
|
@@ -111,7 +111,7 @@ export function DraftPage({ doc, device }: DraftPageProps) {
|
|
|
111
111
|
</main>
|
|
112
112
|
|
|
113
113
|
{/* Premium paywall overlay */}
|
|
114
|
-
{layout.paywall && <PaywallOverlay />}
|
|
114
|
+
{layout.paywall && <PaywallOverlay type={doc.type} />}
|
|
115
115
|
</div>
|
|
116
116
|
</div>
|
|
117
117
|
)
|
|
@@ -193,7 +193,7 @@ function BodyPreview({ body, type }: { body: string; type: ContentType }) {
|
|
|
193
193
|
)
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
// article / newsletter — render body as preformatted prose preview
|
|
196
|
+
// article / newsletter / page — render body as preformatted prose preview
|
|
197
197
|
return <div style={proseStyle}>{body}</div>
|
|
198
198
|
}
|
|
199
199
|
|
|
@@ -220,7 +220,10 @@ function EmptyBody({ type }: { type: ContentType }) {
|
|
|
220
220
|
)
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
function PaywallOverlay() {
|
|
223
|
+
function PaywallOverlay({ type }: { type: ContentType }) {
|
|
224
|
+
const subscribeCopy =
|
|
225
|
+
type === 'page' ? 'Subscribe to read the full page.' : 'Subscribe to read the full article.'
|
|
226
|
+
|
|
224
227
|
return (
|
|
225
228
|
<div
|
|
226
229
|
style={{
|
|
@@ -253,7 +256,7 @@ function PaywallOverlay() {
|
|
|
253
256
|
Premium content
|
|
254
257
|
</div>
|
|
255
258
|
<div style={{ fontSize: 12, color: 'var(--ink-muted)', marginBottom: 14 }}>
|
|
256
|
-
|
|
259
|
+
{subscribeCopy}
|
|
257
260
|
</div>
|
|
258
261
|
<button
|
|
259
262
|
type="button"
|
|
@@ -51,6 +51,7 @@ const CONTENT_TYPE_LABELS: Record<ContentType, string> = {
|
|
|
51
51
|
video: 'Video',
|
|
52
52
|
podcast: 'Podcast',
|
|
53
53
|
newsletter: 'Newsletter',
|
|
54
|
+
page: 'Page',
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
const STATUS_PILL_COLORS: Record<ContentStatus, string> = {
|
|
@@ -67,6 +68,7 @@ const STATUS_PILL_COLORS: Record<ContentStatus, string> = {
|
|
|
67
68
|
|
|
68
69
|
export function EditorScreen({ docRef, onClose }: EditorScreenProps) {
|
|
69
70
|
const contentType: ContentType = (docRef.contentType as ContentType | undefined) ?? 'article'
|
|
71
|
+
const siteBase = typeof window !== 'undefined' ? window.location.origin : undefined
|
|
70
72
|
|
|
71
73
|
// Resolved doc id — starts as null for new docs; set by onDocCreated
|
|
72
74
|
const [docId, setDocId] = useState<string | null>(docRef.id)
|
|
@@ -385,13 +387,16 @@ export function EditorScreen({ docRef, onClose }: EditorScreenProps) {
|
|
|
385
387
|
{/* Share modal — pre-filled from the open doc's slug + type */}
|
|
386
388
|
{shareOpen && (
|
|
387
389
|
<ShareModal
|
|
388
|
-
prefill={buildSharePrefill(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
390
|
+
prefill={buildSharePrefill(
|
|
391
|
+
{
|
|
392
|
+
type: previewDoc.type,
|
|
393
|
+
slug: previewDoc.slug,
|
|
394
|
+
authorId: previewDoc.authorId,
|
|
395
|
+
authorName: previewDoc.authorName,
|
|
396
|
+
authorSlug: previewDoc.authorSlug,
|
|
397
|
+
},
|
|
398
|
+
siteBase,
|
|
399
|
+
)}
|
|
395
400
|
onClose={() => setShareOpen(false)}
|
|
396
401
|
/>
|
|
397
402
|
)}
|
|
@@ -468,6 +473,7 @@ function contentTypeIcon(type: ContentType): import('../icons.js').IconName {
|
|
|
468
473
|
video: 'video',
|
|
469
474
|
podcast: 'mic',
|
|
470
475
|
newsletter: 'mail',
|
|
476
|
+
page: 'doc',
|
|
471
477
|
}
|
|
472
478
|
return map[type]
|
|
473
479
|
}
|
|
@@ -50,6 +50,7 @@ const TYPE_OPTIONS: Array<{ value: ContentType | ''; label: string }> = [
|
|
|
50
50
|
{ value: 'video', label: 'Video' },
|
|
51
51
|
{ value: 'podcast', label: 'Podcast' },
|
|
52
52
|
{ value: 'newsletter', label: 'Newsletter' },
|
|
53
|
+
{ value: 'page', label: 'Page' },
|
|
53
54
|
]
|
|
54
55
|
|
|
55
56
|
const CONTENT_TYPE_ICONS: Record<ContentType, string> = {
|
|
@@ -57,6 +58,7 @@ const CONTENT_TYPE_ICONS: Record<ContentType, string> = {
|
|
|
57
58
|
video: 'video',
|
|
58
59
|
podcast: 'mic',
|
|
59
60
|
newsletter: 'mail',
|
|
61
|
+
page: 'doc',
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
// ---------------------------------------------------------------------------
|
|
@@ -198,6 +200,7 @@ function getVisibleRows(state: LibraryState): LibraryRow[] {
|
|
|
198
200
|
|
|
199
201
|
export function LibraryScreen({ openDoc }: LibraryScreenProps) {
|
|
200
202
|
const { state: ws } = useWorkspace()
|
|
203
|
+
const siteBase = typeof window !== 'undefined' ? window.location.origin : undefined
|
|
201
204
|
const [lib, dispatch] = useReducer(libraryReducer, undefined, initLibrary)
|
|
202
205
|
const searchRef = useRef<HTMLInputElement>(null)
|
|
203
206
|
|
|
@@ -260,7 +263,7 @@ export function LibraryScreen({ openDoc }: LibraryScreenProps) {
|
|
|
260
263
|
<div>
|
|
261
264
|
<h1 className="page-title">Content</h1>
|
|
262
265
|
<div className="page-sub">
|
|
263
|
-
{lib.counts.all} items across articles, video, podcasts and
|
|
266
|
+
{lib.counts.all} items across articles, video, podcasts, newsletters and pages.
|
|
264
267
|
</div>
|
|
265
268
|
</div>
|
|
266
269
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
@@ -483,7 +486,7 @@ export function LibraryScreen({ openDoc }: LibraryScreenProps) {
|
|
|
483
486
|
{/* Share modal — opened by the per-row Share button */}
|
|
484
487
|
{lib.shareRow && (
|
|
485
488
|
<ShareModal
|
|
486
|
-
prefill={buildSharePrefill(lib.shareRow)}
|
|
489
|
+
prefill={buildSharePrefill(lib.shareRow, siteBase)}
|
|
487
490
|
onClose={() => dispatch({ type: 'close-share' })}
|
|
488
491
|
/>
|
|
489
492
|
)}
|
|
@@ -508,7 +511,7 @@ function NewContentMenu({ openDoc }: { openDoc: LibraryScreenProps['openDoc'] })
|
|
|
508
511
|
return () => document.removeEventListener('mousedown', handler)
|
|
509
512
|
}, [open])
|
|
510
513
|
|
|
511
|
-
const types: ContentType[] = ['article', 'video', 'podcast', 'newsletter']
|
|
514
|
+
const types: ContentType[] = ['article', 'video', 'podcast', 'newsletter', 'page']
|
|
512
515
|
|
|
513
516
|
return (
|
|
514
517
|
<div ref={ref} style={{ position: 'relative' }}>
|
|
@@ -105,9 +105,10 @@ export function SocialShareScreen() {
|
|
|
105
105
|
void fetchRecentLinks()
|
|
106
106
|
}, [fetchRecentLinks])
|
|
107
107
|
|
|
108
|
+
const siteBase = typeof window !== 'undefined' ? window.location.origin : undefined
|
|
108
109
|
// Derive goBase from the current origin — relative /go path until cutover.
|
|
109
110
|
// At cutover this becomes 'https://go.fronts.co' via workspace/site config.
|
|
110
|
-
const goBase =
|
|
111
|
+
const goBase = siteBase ? `${siteBase}/go` : '/go'
|
|
111
112
|
|
|
112
113
|
const fields = relevantFields(state.mode)
|
|
113
114
|
|
|
@@ -187,7 +188,7 @@ export function SocialShareScreen() {
|
|
|
187
188
|
</div>
|
|
188
189
|
|
|
189
190
|
{/* Content picker — pre-fills destination URL + slug + type from content library */}
|
|
190
|
-
<ContentPicker dispatch={dispatch} />
|
|
191
|
+
<ContentPicker siteBase={siteBase} dispatch={dispatch} />
|
|
191
192
|
|
|
192
193
|
{/* Destination URL — always visible; may be pre-filled by ContentPicker */}
|
|
193
194
|
<div>
|
|
@@ -410,6 +411,7 @@ function PerModeFields({
|
|
|
410
411
|
<option value="video">Video</option>
|
|
411
412
|
<option value="podcast">Podcast</option>
|
|
412
413
|
<option value="newsletter">Newsletter</option>
|
|
414
|
+
<option value="page">Page</option>
|
|
413
415
|
</select>
|
|
414
416
|
</div>
|
|
415
417
|
</div>
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// ContentRoute owns a ContentView state; dispatching actions moves between the
|
|
3
3
|
// library list (open === null) and the editor (open !== null).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import type { ContentType } from '../../schema/types.js'
|
|
6
|
+
|
|
7
|
+
export type { ContentType } from '../../schema/types.js'
|
|
6
8
|
|
|
7
9
|
export interface ContentView {
|
|
8
10
|
/** null = create new; string = open existing by id */
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// video → /video/<slug> (src/pages/video/[slug].astro)
|
|
11
11
|
// newsletter → /warfronts-weekly/<slug>
|
|
12
12
|
// (src/pages/warfronts-weekly/[slug].astro)
|
|
13
|
+
// page → /<slug>
|
|
13
14
|
//
|
|
14
15
|
// SOURCE OF TRUTH for the path scheme: the Fronts site page structure at
|
|
15
16
|
// /Users/zephyr/projects/fulcrum-labs/fronts/src/pages/
|
|
@@ -44,6 +45,7 @@ const CONTENT_PUBLIC_PATH: Record<ContentType, (slug: string) => string> = {
|
|
|
44
45
|
// SOURCE: fronts/src/pages/warfronts-weekly/[slug].astro
|
|
45
46
|
// fronts/src/lib/content.ts ~L779
|
|
46
47
|
newsletter: (slug) => `/warfronts-weekly/${slug}`,
|
|
48
|
+
page: (slug) => `/${slug.replace(/^\/+/, '')}`,
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
// ---------------------------------------------------------------------------
|
|
@@ -358,7 +358,7 @@ export interface AuthorSocialBatchPayload {
|
|
|
358
358
|
property?: string
|
|
359
359
|
sourcePath?: string
|
|
360
360
|
author: AuthorRef
|
|
361
|
-
handle
|
|
361
|
+
handle?: HandleRef
|
|
362
362
|
}
|
|
363
363
|
|
|
364
364
|
/** owned-social batch request payload */
|
|
@@ -474,7 +474,7 @@ export function buildBatchPayloadFrom(state: ShareFormState): BuildPayloadResult
|
|
|
474
474
|
...(state.boostMode && state.boostMode !== 'none' ? { boostMode: state.boostMode } : {}),
|
|
475
475
|
...(nonEmpty(state.promoCode) ? { promoCode: state.promoCode } : {}),
|
|
476
476
|
author: state.author,
|
|
477
|
-
handle: state.handle,
|
|
477
|
+
...(refHasContent(state.handle) ? { handle: state.handle } : {}),
|
|
478
478
|
}
|
|
479
479
|
return { ok: true, payload }
|
|
480
480
|
}
|
|
@@ -701,7 +701,7 @@ export function toPlainText(output: ShareOutput): string {
|
|
|
701
701
|
* given mode. The React form renders only these inputs; the request builders
|
|
702
702
|
* read only these fields.
|
|
703
703
|
*
|
|
704
|
-
* 'author-social' → campaign, platform, author,
|
|
704
|
+
* 'author-social' → campaign, platform, author, destinationUrl, advanced
|
|
705
705
|
* 'owned-social' → campaign, platform, handle, (optional author), destinationUrl, advanced
|
|
706
706
|
* 'youtube' → youtubeVideoId, placements, (optional handle/author), destinationUrl (NO campaign)
|
|
707
707
|
* 'newsletter' → issue, (optional author/handle), destinationUrl (NO campaign/platform/boostMode)
|
|
@@ -711,16 +711,7 @@ export function relevantFields(mode: ShareMode): string[] {
|
|
|
711
711
|
|
|
712
712
|
switch (mode) {
|
|
713
713
|
case 'author-social':
|
|
714
|
-
return [
|
|
715
|
-
...shared,
|
|
716
|
-
'campaign',
|
|
717
|
-
'platform',
|
|
718
|
-
'author',
|
|
719
|
-
'handle',
|
|
720
|
-
'placement',
|
|
721
|
-
'boostMode',
|
|
722
|
-
'promoCode',
|
|
723
|
-
]
|
|
714
|
+
return [...shared, 'campaign', 'platform', 'author', 'placement', 'boostMode', 'promoCode']
|
|
724
715
|
|
|
725
716
|
case 'owned-social':
|
|
726
717
|
return [
|