@growth-labs/cms 0.8.19 → 0.8.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/foundry-emit.d.ts +14 -0
- package/dist/cli/foundry-emit.d.ts.map +1 -0
- package/dist/cli/foundry-emit.js +69 -0
- package/dist/cli/foundry-emit.js.map +1 -0
- package/dist/engine/fronts-publish-intent.d.ts +1 -1
- package/dist/engine/fronts-publish-intent.d.ts.map +1 -1
- package/dist/engine/fronts-publish-intent.js +12 -1
- package/dist/engine/fronts-publish-intent.js.map +1 -1
- package/dist/engine/publication-receipt/receipt.d.ts +20 -20
- package/dist/engine/publisher.d.ts +16 -1
- package/dist/engine/publisher.d.ts.map +1 -1
- package/dist/engine/publisher.js +55 -4
- package/dist/engine/publisher.js.map +1 -1
- package/dist/engine/tags.d.ts.map +1 -1
- package/dist/engine/tags.js +18 -1
- package/dist/engine/tags.js.map +1 -1
- package/dist/engine/topics.d.ts.map +1 -1
- package/dist/engine/topics.js +4 -1
- package/dist/engine/topics.js.map +1 -1
- package/dist/engine/video-source.d.ts +21 -0
- package/dist/engine/video-source.d.ts.map +1 -1
- package/dist/engine/video-source.js +40 -0
- package/dist/engine/video-source.js.map +1 -1
- package/dist/foundry-emit.d.ts +146 -0
- package/dist/foundry-emit.d.ts.map +1 -0
- package/dist/foundry-emit.js +189 -0
- package/dist/foundry-emit.js.map +1 -0
- package/dist/integration/index.d.ts.map +1 -1
- package/dist/integration/index.js +9 -0
- package/dist/integration/index.js.map +1 -1
- package/dist/integration/options.d.ts +24 -0
- package/dist/integration/options.d.ts.map +1 -1
- package/dist/integration/options.js +22 -0
- package/dist/integration/options.js.map +1 -1
- package/dist/lib/publish-timezone.d.ts +38 -0
- package/dist/lib/publish-timezone.d.ts.map +1 -0
- package/dist/lib/publish-timezone.js +135 -0
- package/dist/lib/publish-timezone.js.map +1 -0
- package/dist/providers/null.d.ts.map +1 -1
- package/dist/providers/null.js +6 -0
- package/dist/providers/null.js.map +1 -1
- package/dist/providers/types.d.ts +56 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/routes/ai.d.ts.map +1 -1
- package/dist/routes/ai.js +21 -13
- package/dist/routes/ai.js.map +1 -1
- package/dist/routes/analytics.d.ts +1 -0
- package/dist/routes/analytics.d.ts.map +1 -1
- package/dist/routes/analytics.js +20 -0
- package/dist/routes/analytics.js.map +1 -1
- package/dist/routes/content.d.ts +27 -0
- package/dist/routes/content.d.ts.map +1 -1
- package/dist/routes/content.js +60 -114
- package/dist/routes/content.js.map +1 -1
- package/dist/routes/foundry-publish.d.ts +50 -0
- package/dist/routes/foundry-publish.d.ts.map +1 -1
- package/dist/routes/foundry-publish.js +540 -27
- package/dist/routes/foundry-publish.js.map +1 -1
- package/dist/routes/machine-context.d.ts +6 -0
- package/dist/routes/machine-context.d.ts.map +1 -0
- package/dist/routes/machine-context.js +20 -0
- package/dist/routes/machine-context.js.map +1 -0
- package/dist/routes/surveys.d.ts +14 -1
- package/dist/routes/surveys.d.ts.map +1 -1
- package/dist/routes/surveys.js +515 -10
- package/dist/routes/surveys.js.map +1 -1
- package/dist/schema/insights-ingest.d.ts +8 -8
- package/dist/schema/migrations.d.ts.map +1 -1
- package/dist/schema/migrations.js +15 -0
- package/dist/schema/migrations.js.map +1 -1
- package/dist/schema/tables.d.ts +1 -1
- package/dist/schema/tables.d.ts.map +1 -1
- package/dist/schema/tables.js +2 -0
- package/dist/schema/tables.js.map +1 -1
- package/dist/surveys/access.d.ts +1 -0
- package/dist/surveys/access.d.ts.map +1 -1
- package/dist/surveys/access.js +13 -4
- package/dist/surveys/access.js.map +1 -1
- package/dist/surveys/auto-label.d.ts +100 -0
- package/dist/surveys/auto-label.d.ts.map +1 -0
- package/dist/surveys/auto-label.js +325 -0
- package/dist/surveys/auto-label.js.map +1 -0
- package/dist/surveys/schema.d.ts +38 -12
- package/dist/surveys/schema.d.ts.map +1 -1
- package/dist/surveys/schema.js +18 -1
- package/dist/surveys/schema.js.map +1 -1
- package/dist/ui/api/_content-config.d.ts.map +1 -1
- package/dist/ui/api/_content-config.js +4 -0
- package/dist/ui/api/_content-config.js.map +1 -1
- package/dist/ui/api/analytics.d.ts.map +1 -1
- package/dist/ui/api/analytics.js +5 -2
- package/dist/ui/api/analytics.js.map +1 -1
- package/dist/ui/api/foundry/articles/[external_id]/publish.d.ts +3 -0
- package/dist/ui/api/foundry/articles/[external_id]/publish.d.ts.map +1 -0
- package/dist/ui/api/foundry/articles/[external_id]/publish.js +14 -0
- package/dist/ui/api/foundry/articles/[external_id]/publish.js.map +1 -0
- package/dist/ui/api/surveys/[surveyId]/answers/label.d.ts +3 -0
- package/dist/ui/api/surveys/[surveyId]/answers/label.d.ts.map +1 -0
- package/dist/ui/api/surveys/[surveyId]/answers/label.js +17 -0
- package/dist/ui/api/surveys/[surveyId]/answers/label.js.map +1 -0
- package/dist/ui/api/surveys/[surveyId].d.ts.map +1 -1
- package/dist/ui/api/surveys/[surveyId].js +28 -1
- package/dist/ui/api/surveys/[surveyId].js.map +1 -1
- package/dist/ui/commands.d.ts.map +1 -1
- package/dist/ui/commands.js +2 -0
- package/dist/ui/commands.js.map +1 -1
- package/dist/ui/hash-router.d.ts +1 -1
- package/dist/ui/hash-router.d.ts.map +1 -1
- package/dist/ui/hash-router.js +1 -0
- package/dist/ui/hash-router.js.map +1 -1
- package/dist/ui/inspector/PublishTab.d.ts +7 -1
- package/dist/ui/inspector/PublishTab.d.ts.map +1 -1
- package/dist/ui/inspector/PublishTab.js +24 -26
- package/dist/ui/inspector/PublishTab.js.map +1 -1
- package/dist/ui/nav.d.ts.map +1 -1
- package/dist/ui/nav.js +1 -0
- package/dist/ui/nav.js.map +1 -1
- package/dist/ui/screens/AnalyticsScreen.d.ts +6 -1
- package/dist/ui/screens/AnalyticsScreen.d.ts.map +1 -1
- package/dist/ui/screens/AnalyticsScreen.js +13 -8
- package/dist/ui/screens/AnalyticsScreen.js.map +1 -1
- package/dist/ui/screens/PodcastListeningScreen.d.ts +6 -0
- package/dist/ui/screens/PodcastListeningScreen.d.ts.map +1 -0
- package/dist/ui/screens/PodcastListeningScreen.js +244 -0
- package/dist/ui/screens/PodcastListeningScreen.js.map +1 -0
- package/dist/ui/screens/SurveyResponsesScreen.d.ts +4 -2
- package/dist/ui/screens/SurveyResponsesScreen.d.ts.map +1 -1
- package/dist/ui/screens/SurveyResponsesScreen.js +71 -12
- package/dist/ui/screens/SurveyResponsesScreen.js.map +1 -1
- package/dist/ui/screens/analytics-data.d.ts +14 -0
- package/dist/ui/screens/analytics-data.d.ts.map +1 -1
- package/dist/ui/screens/analytics-data.js +16 -0
- package/dist/ui/screens/analytics-data.js.map +1 -1
- package/dist/ui/screens/podcast-listening-data.d.ts +19 -0
- package/dist/ui/screens/podcast-listening-data.d.ts.map +1 -0
- package/dist/ui/screens/podcast-listening-data.js +98 -0
- package/dist/ui/screens/podcast-listening-data.js.map +1 -0
- package/dist/ui/screens/registry.d.ts.map +1 -1
- package/dist/ui/screens/registry.js +2 -0
- package/dist/ui/screens/registry.js.map +1 -1
- package/docs/foundry-publish.md +208 -0
- package/migrations/0033_survey_answer_labels.sql +15 -0
- package/package.json +9 -3
- package/src/cli/foundry-emit.ts +85 -0
- package/src/engine/fronts-publish-intent.ts +23 -1
- package/src/engine/publisher.ts +80 -4
- package/src/engine/tags.ts +21 -1
- package/src/engine/topics.ts +4 -1
- package/src/engine/video-source.ts +44 -0
- package/src/foundry-emit.ts +255 -0
- package/src/integration/index.ts +9 -0
- package/src/integration/options.ts +23 -0
- package/src/lib/publish-timezone.ts +154 -0
- package/src/providers/null.ts +6 -0
- package/src/providers/types.ts +52 -0
- package/src/routes/ai.ts +29 -17
- package/src/routes/analytics.ts +21 -0
- package/src/routes/content.ts +86 -131
- package/src/routes/foundry-publish.ts +746 -29
- package/src/routes/machine-context.ts +24 -0
- package/src/routes/surveys.ts +587 -12
- package/src/schema/migrations.ts +15 -0
- package/src/schema/tables.ts +2 -0
- package/src/surveys/access.ts +14 -4
- package/src/surveys/auto-label.ts +420 -0
- package/src/surveys/schema.ts +25 -1
- package/src/ui/api/_content-config.ts +6 -0
- package/src/ui/api/analytics.ts +5 -2
- package/src/ui/api/foundry/articles/[external_id]/publish.ts +18 -0
- package/src/ui/api/surveys/[surveyId]/answers/label.ts +23 -0
- package/src/ui/api/surveys/[surveyId].ts +37 -3
- package/src/ui/commands.ts +2 -0
- package/src/ui/hash-router.ts +1 -0
- package/src/ui/inspector/PublishTab.tsx +33 -25
- package/src/ui/nav.ts +1 -0
- package/src/ui/screens/AnalyticsScreen.tsx +34 -6
- package/src/ui/screens/PodcastListeningScreen.tsx +529 -0
- package/src/ui/screens/SurveyResponsesScreen.tsx +179 -13
- package/src/ui/screens/analytics-data.ts +18 -0
- package/src/ui/screens/podcast-listening-data.ts +114 -0
- package/src/ui/screens/registry.tsx +2 -0
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// Compile-gated (tsc, jsx: react-jsx). No vitest — pure shapers are in
|
|
11
11
|
// inspector-data.ts and tested there.
|
|
12
12
|
|
|
13
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
13
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
14
|
+
import { formatInstantInZone, PUBLISH_TIMEZONE_FALLBACK } from '../../lib/publish-timezone.js'
|
|
14
15
|
import type { ContentStatus } from '../../schema/types.js'
|
|
15
16
|
import { apiErrorMessage } from '../api-error.js'
|
|
16
17
|
import { Icon } from '../icons.js'
|
|
@@ -40,6 +41,12 @@ export interface PublishTabProps {
|
|
|
40
41
|
authorId: string | null
|
|
41
42
|
/** Current scheduled publish time (ISO string, null if not scheduled). */
|
|
42
43
|
publishAt: string | null
|
|
44
|
+
/**
|
|
45
|
+
* Timezone the editor schedules in. The same zone is used to DISPLAY the
|
|
46
|
+
* stored instant and to SAVE what is typed, so a resave of untouched fields
|
|
47
|
+
* is a true no-op. Defaults to the publication timezone.
|
|
48
|
+
*/
|
|
49
|
+
publishTimezone?: string
|
|
43
50
|
/** Called after a status/field change so the parent can refresh. */
|
|
44
51
|
onChanged?: () => void
|
|
45
52
|
}
|
|
@@ -85,6 +92,7 @@ export function PublishTab({
|
|
|
85
92
|
visibility,
|
|
86
93
|
authorId,
|
|
87
94
|
publishAt,
|
|
95
|
+
publishTimezone = PUBLISH_TIMEZONE_FALLBACK,
|
|
88
96
|
onChanged,
|
|
89
97
|
}: PublishTabProps): React.ReactElement {
|
|
90
98
|
const [saving, setSaving] = useState(false)
|
|
@@ -92,16 +100,20 @@ export function PublishTab({
|
|
|
92
100
|
const [actionError, setActionError] = useState<string | null>(null)
|
|
93
101
|
|
|
94
102
|
// Local schedule date/time inputs.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (!
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// The editor thinks in publication-local wall clock, so that is what these
|
|
104
|
+
// inputs show. Falling back to the raw UTC slice keeps an unusable zone
|
|
105
|
+
// readable rather than blank.
|
|
106
|
+
const scheduled = useMemo(() => {
|
|
107
|
+
if (!publishAt) return null
|
|
108
|
+
const ms = Date.parse(publishAt)
|
|
109
|
+
if (!Number.isFinite(ms)) return null
|
|
110
|
+
return formatInstantInZone(ms, publishTimezone)
|
|
111
|
+
}, [publishAt, publishTimezone])
|
|
112
|
+
|
|
113
|
+
const [schedDate, setSchedDate] = useState(() => scheduled?.date ?? publishAt?.slice(0, 10) ?? '')
|
|
114
|
+
const [schedTime, setSchedTime] = useState(
|
|
115
|
+
() => scheduled?.time ?? publishAt?.slice(11, 16) ?? '12:00',
|
|
116
|
+
)
|
|
105
117
|
|
|
106
118
|
// Author list (fetched once on mount).
|
|
107
119
|
const [authors, setAuthors] = useState<AuthorOption[]>([])
|
|
@@ -172,18 +184,14 @@ export function PublishTab({
|
|
|
172
184
|
setSaving(true)
|
|
173
185
|
setActionError(null)
|
|
174
186
|
try {
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
// them
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
// in that zone's offset, silently shifting publish_at by that zone's
|
|
182
|
-
// current UTC offset on every resave -- including an incidental
|
|
183
|
-
// resave that never touched these fields. 'UTC' makes display and
|
|
184
|
-
// save agree, so a resave of untouched fields is a true no-op.
|
|
187
|
+
// These digits are publication-local wall clock (see the initializers),
|
|
188
|
+
// so they are saved under that SAME zone. Display and save must agree:
|
|
189
|
+
// sending digits under a zone they were not rendered in made the server
|
|
190
|
+
// reinterpret them and shift publish_at by that zone's offset on every
|
|
191
|
+
// resave, including an incidental one that never touched these fields
|
|
192
|
+
// (the 2026-09-08 P0). Agreement is what keeps a resave a no-op.
|
|
185
193
|
const localDateTime = `${schedDate}T${schedTime}`
|
|
186
|
-
const body = buildScheduleBody(localDateTime,
|
|
194
|
+
const body = buildScheduleBody(localDateTime, publishTimezone)
|
|
187
195
|
await runAction(docId, 'schedule', body as unknown as Record<string, unknown>)
|
|
188
196
|
setSelectedStatus('scheduled')
|
|
189
197
|
onChanged?.()
|
|
@@ -192,7 +200,7 @@ export function PublishTab({
|
|
|
192
200
|
} finally {
|
|
193
201
|
setSaving(false)
|
|
194
202
|
}
|
|
195
|
-
}, [docId, schedDate, schedTime, saving, onChanged])
|
|
203
|
+
}, [docId, schedDate, schedTime, saving, onChanged, publishTimezone])
|
|
196
204
|
|
|
197
205
|
const handleVisibilityChange = useCallback(
|
|
198
206
|
async (newVis: 'free' | 'premium') => {
|
|
@@ -317,7 +325,7 @@ export function PublishTab({
|
|
|
317
325
|
{/* Scheduled date + time inputs */}
|
|
318
326
|
{showScheduleControls && (
|
|
319
327
|
<div style={schedGrid} className="fade-only">
|
|
320
|
-
<Field label=
|
|
328
|
+
<Field label={`Date (${publishTimezone})`}>
|
|
321
329
|
<input
|
|
322
330
|
className="input"
|
|
323
331
|
type="date"
|
|
@@ -325,7 +333,7 @@ export function PublishTab({
|
|
|
325
333
|
onChange={(e) => setSchedDate(e.target.value)}
|
|
326
334
|
/>
|
|
327
335
|
</Field>
|
|
328
|
-
<Field label=
|
|
336
|
+
<Field label={`Time (${publishTimezone})`}>
|
|
329
337
|
<input
|
|
330
338
|
className="input"
|
|
331
339
|
type="time"
|
package/src/ui/nav.ts
CHANGED
|
@@ -26,6 +26,7 @@ export const NAV_MODEL: NavGroup[] = [
|
|
|
26
26
|
{ id: 'calendar', label: 'Calendar', icon: 'calendar' },
|
|
27
27
|
{ id: 'media', label: 'Media', icon: 'media' },
|
|
28
28
|
{ id: 'analytics', label: 'Analytics', icon: 'chart' },
|
|
29
|
+
{ id: 'podcasts', label: 'Podcasts', icon: 'mic' },
|
|
29
30
|
],
|
|
30
31
|
},
|
|
31
32
|
{
|
|
@@ -32,7 +32,9 @@ import {
|
|
|
32
32
|
normalizeAnalyticsData,
|
|
33
33
|
normalizeArticleAnalyticsDetail,
|
|
34
34
|
type ReadsPoint,
|
|
35
|
+
TRAFFIC_SOURCES_CAPTION,
|
|
35
36
|
type TrafficSource,
|
|
37
|
+
uniqueVisitorsKpiLabel,
|
|
36
38
|
} from './analytics-data.js'
|
|
37
39
|
import {
|
|
38
40
|
formatKpi,
|
|
@@ -201,7 +203,9 @@ export function AnalyticsScreen() {
|
|
|
201
203
|
</div>
|
|
202
204
|
)}
|
|
203
205
|
|
|
204
|
-
{!s.loading && !s.error && s.data &&
|
|
206
|
+
{!s.loading && !s.error && s.data && (
|
|
207
|
+
<AnalyticsBody data={s.data} isPartial={isPartial} rangeDays={Number.parseInt(range, 10)} />
|
|
208
|
+
)}
|
|
205
209
|
</div>
|
|
206
210
|
)
|
|
207
211
|
}
|
|
@@ -579,7 +583,15 @@ function RangeSegment({
|
|
|
579
583
|
// AnalyticsBody
|
|
580
584
|
// ---------------------------------------------------------------------------
|
|
581
585
|
|
|
582
|
-
function AnalyticsBody({
|
|
586
|
+
function AnalyticsBody({
|
|
587
|
+
data,
|
|
588
|
+
isPartial,
|
|
589
|
+
rangeDays,
|
|
590
|
+
}: {
|
|
591
|
+
data: AnalyticsData
|
|
592
|
+
isPartial: boolean
|
|
593
|
+
rangeDays: number
|
|
594
|
+
}) {
|
|
583
595
|
return (
|
|
584
596
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--gap)' }}>
|
|
585
597
|
{isPartial && (
|
|
@@ -587,7 +599,7 @@ function AnalyticsBody({ data, isPartial }: { data: AnalyticsData; isPartial: bo
|
|
|
587
599
|
)}
|
|
588
600
|
|
|
589
601
|
{/* KPI cards */}
|
|
590
|
-
<KpiCards kpis={data.kpis} />
|
|
602
|
+
<KpiCards kpis={data.kpis} rangeDays={rangeDays} />
|
|
591
603
|
|
|
592
604
|
{/* Reads over time + traffic sources */}
|
|
593
605
|
<div className="grid-split-wide">
|
|
@@ -638,7 +650,13 @@ const KPI_DEFS: Array<{
|
|
|
638
650
|
},
|
|
639
651
|
]
|
|
640
652
|
|
|
641
|
-
export function KpiCards({
|
|
653
|
+
export function KpiCards({
|
|
654
|
+
kpis,
|
|
655
|
+
rangeDays = 7,
|
|
656
|
+
}: {
|
|
657
|
+
kpis: ProviderResult<Record<string, number | null>>
|
|
658
|
+
rangeDays?: number
|
|
659
|
+
}) {
|
|
642
660
|
const kpiStatus = selectKpiState(kpis)
|
|
643
661
|
|
|
644
662
|
if (kpiStatus === 'empty') {
|
|
@@ -666,7 +684,7 @@ export function KpiCards({ kpis }: { kpis: ProviderResult<Record<string, number
|
|
|
666
684
|
return (
|
|
667
685
|
<KpiCard
|
|
668
686
|
key={def.key}
|
|
669
|
-
label={def.label}
|
|
687
|
+
label={def.key === 'uniqueVisitors' ? uniqueVisitorsKpiLabel(rangeDays) : def.label}
|
|
670
688
|
status={isUnmeasured ? 'partial' : kpiStatus}
|
|
671
689
|
value={displayValue}
|
|
672
690
|
note={displayNote}
|
|
@@ -813,7 +831,7 @@ function ReadsChart({ series }: { series: ProviderResult<ReadsPoint[]> }) {
|
|
|
813
831
|
// TrafficSources
|
|
814
832
|
// ---------------------------------------------------------------------------
|
|
815
833
|
|
|
816
|
-
function TrafficSources({ result }: { result: ProviderResult<TrafficSource[]> }) {
|
|
834
|
+
export function TrafficSources({ result }: { result: ProviderResult<TrafficSource[]> }) {
|
|
817
835
|
if (!result || result.status === 'empty' || !result.data || result.data.length === 0) {
|
|
818
836
|
return <EmptyPanel icon="globe" message="No traffic source data yet." />
|
|
819
837
|
}
|
|
@@ -836,6 +854,16 @@ function TrafficSources({ result }: { result: ProviderResult<TrafficSource[]> })
|
|
|
836
854
|
<span style={{ fontSize: 11, fontWeight: 600, color: 'var(--amber)' }}>Partial</span>
|
|
837
855
|
)}
|
|
838
856
|
</div>
|
|
857
|
+
<div
|
|
858
|
+
style={{
|
|
859
|
+
fontSize: 11,
|
|
860
|
+
color: 'var(--ink-faint)',
|
|
861
|
+
marginTop: -10,
|
|
862
|
+
marginBottom: 14,
|
|
863
|
+
}}
|
|
864
|
+
>
|
|
865
|
+
{TRAFFIC_SOURCES_CAPTION}
|
|
866
|
+
</div>
|
|
839
867
|
{sources.map((src) => {
|
|
840
868
|
const pct = (src.sessions / maxSessions) * 100
|
|
841
869
|
return (
|