@flamingo-stack/openframe-frontend-core 0.0.270 → 0.0.271-snapshot.20260616021758
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/{chunk-37OJ53QH.js → chunk-4SNHFZQT.js} +3 -3
- package/dist/{chunk-CICK3FUU.cjs → chunk-7NJUPRN6.cjs} +7 -2
- package/dist/chunk-7NJUPRN6.cjs.map +1 -0
- package/dist/{chunk-ICOMPUMX.js → chunk-BSOHVQ7X.js} +6 -1
- package/dist/chunk-BSOHVQ7X.js.map +1 -0
- package/dist/{chunk-3H5TSZMU.cjs → chunk-EEMZBQ7O.cjs} +25 -25
- package/dist/{chunk-3H5TSZMU.cjs.map → chunk-EEMZBQ7O.cjs.map} +1 -1
- package/dist/{chunk-NDSCF6LB.cjs → chunk-IXRPEMZP.cjs} +15 -15
- package/dist/{chunk-NDSCF6LB.cjs.map → chunk-IXRPEMZP.cjs.map} +1 -1
- package/dist/{chunk-EMRSIP4Q.js → chunk-MCCYGAPE.js} +2 -2
- package/dist/{chunk-P7JHUX4X.js → chunk-N5MCKGAB.js} +2 -2
- package/dist/{chunk-N3OPJQYI.cjs → chunk-Q32JUFIK.cjs} +886 -812
- package/dist/chunk-Q32JUFIK.cjs.map +1 -0
- package/dist/{chunk-XOKXFW77.js → chunk-UQCB2CP7.js} +35 -6
- package/dist/chunk-UQCB2CP7.js.map +1 -0
- package/dist/{chunk-JIJEVSCC.cjs → chunk-V5OZQF4B.cjs} +69 -40
- package/dist/chunk-V5OZQF4B.cjs.map +1 -0
- package/dist/{chunk-RM7ZT3UU.js → chunk-VFMMVGA4.js} +3286 -3212
- package/dist/chunk-VFMMVGA4.js.map +1 -0
- package/dist/{chunk-AA77CC2Y.cjs → chunk-XDGDCO3I.cjs} +12 -12
- package/dist/{chunk-AA77CC2Y.cjs.map → chunk-XDGDCO3I.cjs.map} +1 -1
- package/dist/components/chat/entity-cards/index.d.ts +2 -0
- package/dist/components/chat/entity-cards/index.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/what-i-shipped-card.d.ts +43 -0
- package/dist/components/chat/entity-cards/what-i-shipped-card.d.ts.map +1 -0
- package/dist/components/chat/index.cjs +7 -3
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +6 -2
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/features/status-filter-component.d.ts +6 -1
- package/dist/components/features/status-filter-component.d.ts.map +1 -1
- package/dist/components/form.d.ts +3 -1
- package/dist/components/form.d.ts.map +1 -1
- package/dist/components/index.cjs +88 -98
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +10 -20
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +22 -22
- package/dist/components/onboarding-guides/index.js +2 -2
- package/dist/components/related-content/index.cjs +4 -4
- package/dist/components/related-content/index.js +3 -3
- package/dist/components/related-content/related-content-section.d.ts +13 -1
- package/dist/components/related-content/related-content-section.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +62 -62
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/index.cjs +7 -3
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +6 -2
- package/dist/index.cjs +9 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +8 -2
- package/dist/types/marketing.d.ts +1 -1
- package/dist/types/marketing.d.ts.map +1 -1
- package/dist/types/video-processing.d.ts +2 -1
- package/dist/types/video-processing.d.ts.map +1 -1
- package/dist/utils/content-ref-groups.d.ts.map +1 -1
- package/dist/utils/format.d.ts +8 -0
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/index.cjs +7 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +7 -2
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/chat-container.tsx +1 -1
- package/src/components/chat/entity-cards/index.ts +2 -0
- package/src/components/chat/entity-cards/what-i-shipped-card.tsx +109 -0
- package/src/components/features/status-filter-component.tsx +9 -1
- package/src/components/related-content/related-content-section.tsx +70 -4
- package/src/types/marketing.ts +1 -0
- package/src/types/video-processing.ts +2 -0
- package/src/utils/content-ref-groups.ts +1 -0
- package/src/utils/format.ts +15 -0
- package/src/utils/index.ts +1 -0
- package/dist/chunk-CICK3FUU.cjs.map +0 -1
- package/dist/chunk-ICOMPUMX.js.map +0 -1
- package/dist/chunk-JIJEVSCC.cjs.map +0 -1
- package/dist/chunk-N3OPJQYI.cjs.map +0 -1
- package/dist/chunk-RM7ZT3UU.js.map +0 -1
- package/dist/chunk-XOKXFW77.js.map +0 -1
- /package/dist/{chunk-37OJ53QH.js.map → chunk-4SNHFZQT.js.map} +0 -0
- /package/dist/{chunk-EMRSIP4Q.js.map → chunk-MCCYGAPE.js.map} +0 -0
- /package/dist/{chunk-P7JHUX4X.js.map → chunk-N5MCKGAB.js.map} +0 -0
package/package.json
CHANGED
|
@@ -17,6 +17,8 @@ export {
|
|
|
17
17
|
} from './entity-author-card'
|
|
18
18
|
export { BlogImagePlaceholder } from './blog-image-placeholder'
|
|
19
19
|
export { AdminContentCard } from './admin-content-card'
|
|
20
|
+
export { WhatIShippedCard, WhatIShippedCardSkeleton } from './what-i-shipped-card'
|
|
21
|
+
export type { WhatIShippedCardData, WhatIShippedCardProps } from './what-i-shipped-card'
|
|
20
22
|
|
|
21
23
|
// Moved-into-subdir flat cards
|
|
22
24
|
export { BlockCard, type BlockCardProps } from './block-card'
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { AdminContentCard } from './admin-content-card'
|
|
3
|
+
import { formatEntryMonthUTC } from '../../../utils/format'
|
|
4
|
+
import { getProxiedImageUrl } from '../../../utils/image-proxy-stub'
|
|
5
|
+
|
|
6
|
+
/** Minimal row shape the card renders. Both the hub dashboard entry and the
|
|
7
|
+
* related-content hydrated row satisfy it structurally. */
|
|
8
|
+
export interface WhatIShippedCardData {
|
|
9
|
+
title?: string | null
|
|
10
|
+
summary?: string | null
|
|
11
|
+
status?: string | null
|
|
12
|
+
featured_image?: string | null
|
|
13
|
+
main_video_thumbnail?: string | null
|
|
14
|
+
entry_month?: string | null
|
|
15
|
+
author?: { full_name?: string | null; avatar_url?: string | null } | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WhatIShippedCardProps {
|
|
19
|
+
entry: WhatIShippedCardData
|
|
20
|
+
/** OG fallback cover. Caller computes it (hub: `useOgPlaceholder`; related
|
|
21
|
+
* rail: `extras.buildOgPlaceholderUrl`). */
|
|
22
|
+
placeholderUrl?: string | null
|
|
23
|
+
/** Owner action row (dashboard). Omit for a read-only card. */
|
|
24
|
+
actions?: React.ReactNode
|
|
25
|
+
/** When provided, the WHOLE card becomes a link (related-rail click-through).
|
|
26
|
+
* Don't combine with `actions` (nested interactive). */
|
|
27
|
+
anchorProps?: React.AnchorHTMLAttributes<HTMLAnchorElement>
|
|
28
|
+
className?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const STATUS_BADGE_CLASS: Record<string, string> = {
|
|
32
|
+
published: 'bg-ods-success-secondary text-ods-success',
|
|
33
|
+
draft: 'bg-ods-warning-secondary text-ods-warning',
|
|
34
|
+
archived: 'bg-ods-border text-ods-text-secondary',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* THE single "What I Shipped" card. Wraps `AdminContentCard` with the canonical
|
|
39
|
+
* mapping (cover = featured_image || main_video_thumbnail, OG placeholder
|
|
40
|
+
* fallback, title, 140-char summary, status badge, author avatar+name +
|
|
41
|
+
* reporting month). Used by BOTH the people-hub dashboard (with owner `actions`)
|
|
42
|
+
* and the related-content rail (with `anchorProps` for click-through), so the
|
|
43
|
+
* card is byte-identical everywhere — one component, one mapping, no drift.
|
|
44
|
+
*/
|
|
45
|
+
export function WhatIShippedCard({ entry, placeholderUrl, actions, anchorProps, className }: WhatIShippedCardProps) {
|
|
46
|
+
const month = formatEntryMonthUTC(entry.entry_month, 'short')
|
|
47
|
+
const card = (
|
|
48
|
+
<AdminContentCard
|
|
49
|
+
imageUrl={entry.featured_image || entry.main_video_thumbnail}
|
|
50
|
+
placeholderUrl={placeholderUrl}
|
|
51
|
+
title={entry.title || 'Untitled entry'}
|
|
52
|
+
summary={(entry.summary ?? '').slice(0, 140) || 'No description'}
|
|
53
|
+
badges={
|
|
54
|
+
entry.status ? (
|
|
55
|
+
<span
|
|
56
|
+
className={`px-2 py-1 rounded text-xs font-medium ${
|
|
57
|
+
STATUS_BADGE_CLASS[entry.status] ?? 'bg-ods-card border border-ods-border text-ods-text-secondary'
|
|
58
|
+
}`}
|
|
59
|
+
>
|
|
60
|
+
{entry.status}
|
|
61
|
+
</span>
|
|
62
|
+
) : null
|
|
63
|
+
}
|
|
64
|
+
meta={
|
|
65
|
+
<>
|
|
66
|
+
<span className="flex items-center gap-2 min-w-0">
|
|
67
|
+
{entry.author?.avatar_url ? (
|
|
68
|
+
<img src={getProxiedImageUrl(entry.author.avatar_url) ?? entry.author.avatar_url} alt="" className="h-5 w-5 rounded-full object-cover shrink-0" />
|
|
69
|
+
) : null}
|
|
70
|
+
<span className="truncate">{entry.author?.full_name ?? ''}</span>
|
|
71
|
+
</span>
|
|
72
|
+
{month ? <span>{month}</span> : null}
|
|
73
|
+
</>
|
|
74
|
+
}
|
|
75
|
+
actions={actions}
|
|
76
|
+
className={className}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
return anchorProps ? (
|
|
80
|
+
<a {...anchorProps} className="block h-full">
|
|
81
|
+
{card}
|
|
82
|
+
</a>
|
|
83
|
+
) : (
|
|
84
|
+
card
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Loading skeleton matching WhatIShippedCard's AdminContentCard shape (3:2 cover
|
|
89
|
+
* + title / summary / meta lines). Used by the related-content rail while a
|
|
90
|
+
* group hydrates so there's no shape jump when the real card lands. */
|
|
91
|
+
export function WhatIShippedCardSkeleton({ className }: { className?: string }) {
|
|
92
|
+
// Same convention as BlogCardSkeleton et al.: animate-pulse on the container,
|
|
93
|
+
// `bg-ods-bg` placeholder blocks, flex-grow body with an `mt-auto` avatar+name
|
|
94
|
+
// row. Shape mirrors WhatIShippedCard's AdminContentCard (rounded-2xl, 3:2 cover).
|
|
95
|
+
return (
|
|
96
|
+
<div className={`group bg-ods-card border border-ods-border rounded-2xl overflow-hidden h-full flex flex-col animate-pulse ${className ?? ''}`}>
|
|
97
|
+
<div className="aspect-[3/2] bg-ods-bg" />
|
|
98
|
+
<div className="p-4 flex flex-col flex-grow space-y-3">
|
|
99
|
+
<div className="h-5 w-3/4 bg-ods-bg rounded" />
|
|
100
|
+
<div className="h-3 w-full bg-ods-bg/60 rounded" />
|
|
101
|
+
<div className="h-3 w-4/5 bg-ods-bg/60 rounded" />
|
|
102
|
+
<div className="mt-auto flex items-center gap-2">
|
|
103
|
+
<div className="h-8 w-8 rounded-full bg-ods-bg" />
|
|
104
|
+
<div className="h-3 w-24 bg-ods-bg/60 rounded" />
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -16,6 +16,11 @@ export interface StatusFilterComponentProps {
|
|
|
16
16
|
showCount?: boolean;
|
|
17
17
|
count?: number;
|
|
18
18
|
className?: string;
|
|
19
|
+
/** Status values (incl. `'all'`) to render but DISABLE — the buttons stay
|
|
20
|
+
* visible (so the full set is always shown) but are non-clickable. Lets a
|
|
21
|
+
* dashboard expose every status while gating which ones a given viewer may
|
|
22
|
+
* actually select (e.g. non-management can only pick `published`). */
|
|
23
|
+
disabledValues?: string[];
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
/**
|
|
@@ -29,7 +34,8 @@ export function StatusFilterComponent({
|
|
|
29
34
|
statusOptions,
|
|
30
35
|
showCount = false,
|
|
31
36
|
count = 0,
|
|
32
|
-
className = ''
|
|
37
|
+
className = '',
|
|
38
|
+
disabledValues = []
|
|
33
39
|
}: StatusFilterComponentProps) {
|
|
34
40
|
// Filter out 'all' from options since we render it separately
|
|
35
41
|
const filteredOptions = statusOptions.filter(option => option.value !== 'all');
|
|
@@ -49,6 +55,7 @@ export function StatusFilterComponent({
|
|
|
49
55
|
variant={selectedStatus === 'all' ? "accent" : "outline"}
|
|
50
56
|
size="small-legacy"
|
|
51
57
|
onClick={() => onStatusChange('all')}
|
|
58
|
+
disabled={disabledValues.includes('all')}
|
|
52
59
|
className="text-h3"
|
|
53
60
|
>
|
|
54
61
|
All
|
|
@@ -62,6 +69,7 @@ export function StatusFilterComponent({
|
|
|
62
69
|
variant={selectedStatus === option.value ? "accent" : "outline"}
|
|
63
70
|
size="small-legacy"
|
|
64
71
|
onClick={() => onStatusChange(option.value)}
|
|
72
|
+
disabled={disabledValues.includes(option.value)}
|
|
65
73
|
className="text-h3"
|
|
66
74
|
>
|
|
67
75
|
{option.label}
|
|
@@ -64,6 +64,7 @@ import { decideNewTab } from '../chat/utils/decide-new-tab';
|
|
|
64
64
|
// guarantee that matters here is the RUNTIME one: nothing on this path ever
|
|
65
65
|
// instantiates a QueryClient, so embedders need NO QueryClientProvider.
|
|
66
66
|
import { BlogCard, BlogCardSkeleton } from '../chat/entity-cards/blog-card';
|
|
67
|
+
import { WhatIShippedCard, WhatIShippedCardSkeleton } from '../chat/entity-cards/what-i-shipped-card';
|
|
67
68
|
import { CaseStudyCard, CaseStudyCardSkeleton } from '../chat/entity-cards/case-study-card';
|
|
68
69
|
import { CustomerInterviewCard, CustomerInterviewCardSkeleton } from '../chat/entity-cards/customer-interview-card';
|
|
69
70
|
import { ProductReleaseCard, ProductReleaseCardSkeleton } from '../chat/entity-cards/product-release-card';
|
|
@@ -163,6 +164,9 @@ function renderSkeletonForType(
|
|
|
163
164
|
// the step-numbered 'default' variant is for the guide detail page's
|
|
164
165
|
// "More in section" rail, not this full-width row.
|
|
165
166
|
return <OnboardingGuideCardSkeleton size={size === 'sm' ? 'sm' : 'catalog'} />;
|
|
167
|
+
case 'what_i_shipped':
|
|
168
|
+
// Matches the WhatIShippedCard (AdminContentCard 3:2) shape.
|
|
169
|
+
return <WhatIShippedCardSkeleton />;
|
|
166
170
|
case 'marketing_campaign':
|
|
167
171
|
return adminCampaignCard ? <adminCampaignCard.Skeleton size={legacySize} /> : null;
|
|
168
172
|
case 'roadmap_item':
|
|
@@ -273,6 +277,25 @@ function CardForType({
|
|
|
273
277
|
// Catalog variant (see skeleton note) — full-width rich card with a
|
|
274
278
|
// line-clamped description instead of the step-numbered rail card.
|
|
275
279
|
return <OnboardingGuideCard guide={item} size={size === 'sm' ? 'sm' : 'catalog'} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;
|
|
280
|
+
case 'what_i_shipped':
|
|
281
|
+
// THE single What I Shipped card — same lib component the people-hub
|
|
282
|
+
// dashboard renders, so the card is identical in the rail and the
|
|
283
|
+
// dashboard. `anchorProps` makes the whole card a click-through link
|
|
284
|
+
// (rail is read-only — no owner actions).
|
|
285
|
+
return (
|
|
286
|
+
<WhatIShippedCard
|
|
287
|
+
entry={item}
|
|
288
|
+
placeholderUrl={placeholderUrl}
|
|
289
|
+
// Only pass anchorProps when there's a REAL href — a fallback object
|
|
290
|
+
// with `href: undefined` is still truthy and would make WhatIShippedCard
|
|
291
|
+
// wrap the card in a dead <a> (no URL). Mirrors the ProductReleaseCard
|
|
292
|
+
// `linkProps ?? undefined` pattern above.
|
|
293
|
+
anchorProps={
|
|
294
|
+
linkProps ??
|
|
295
|
+
(href ? ({ href, ...anchorAttrs } as React.AnchorHTMLAttributes<HTMLAnchorElement>) : undefined)
|
|
296
|
+
}
|
|
297
|
+
/>
|
|
298
|
+
);
|
|
276
299
|
case 'marketing_campaign':
|
|
277
300
|
return adminCampaignCard ? <adminCampaignCard.Card campaign={item} /> : null;
|
|
278
301
|
case 'roadmap_item':
|
|
@@ -586,6 +609,18 @@ export interface RelatedContentSectionProps {
|
|
|
586
609
|
/** Renderer pair for the admin-only `marketing_campaign` type. Absent →
|
|
587
610
|
* the type renders nothing. */
|
|
588
611
|
adminCampaignCard?: AdminCampaignCardSlot;
|
|
612
|
+
/** When true, render the section shell (title + an empty-state line) even
|
|
613
|
+
* with ZERO refs, instead of returning null. Default false (the original
|
|
614
|
+
* behavior — empty rail = no shell). Opt-in per host page (e.g. people-hub's
|
|
615
|
+
* "What I Shipped", where the section should always be present). */
|
|
616
|
+
showWhenEmpty?: boolean;
|
|
617
|
+
/** Empty-state copy shown under the title when `showWhenEmpty` and no refs.
|
|
618
|
+
* Default: "No related content yet." */
|
|
619
|
+
emptyStateText?: string;
|
|
620
|
+
/** Custom empty-state node (e.g. a hub `<EmptyState/>`) rendered under the
|
|
621
|
+
* title when `showWhenEmpty` and there are no refs — overrides
|
|
622
|
+
* `emptyStateText`. Lets a host match its canonical empty state. */
|
|
623
|
+
emptyState?: React.ReactNode;
|
|
589
624
|
}
|
|
590
625
|
|
|
591
626
|
export function RelatedContentSection({
|
|
@@ -606,6 +641,9 @@ export function RelatedContentSection({
|
|
|
606
641
|
buildListUrl,
|
|
607
642
|
LinkProvider = DefaultLinkPropsProvider,
|
|
608
643
|
adminCampaignCard,
|
|
644
|
+
showWhenEmpty = false,
|
|
645
|
+
emptyStateText = 'No related content yet.',
|
|
646
|
+
emptyState,
|
|
609
647
|
}: RelatedContentSectionProps) {
|
|
610
648
|
// ── Hooks above EVERY early return (the original `if (!contentRefs.length)
|
|
611
649
|
// return null` guard moved below them). ──
|
|
@@ -660,7 +698,7 @@ export function RelatedContentSection({
|
|
|
660
698
|
() => (!suggestionsDisabled && initialItems ? { refs: initialItems } : undefined),
|
|
661
699
|
[initialItems, suggestionsDisabled],
|
|
662
700
|
);
|
|
663
|
-
const { data } = useSelfFetch<RelatedContentResponse>(suggestUrl, { initialData });
|
|
701
|
+
const { data, isLoading } = useSelfFetch<RelatedContentResponse>(suggestUrl, { initialData });
|
|
664
702
|
|
|
665
703
|
// Default group fetcher: the lib's byte-parity-tested builder, prefixed for
|
|
666
704
|
// embeds. Memoized so group-fetch URLs stay value-stable across renders.
|
|
@@ -677,9 +715,37 @@ export function RelatedContentSection({
|
|
|
677
715
|
// IS the mechanism in controlled mode (original behavior).
|
|
678
716
|
const exclude = new Set(excludeTypes || []);
|
|
679
717
|
const visibleRefs = exclude.size > 0 ? refs.filter((r) => !exclude.has(r.type)) : refs;
|
|
680
|
-
// Zero refs (still loading in suggestion mode, or genuinely empty)
|
|
681
|
-
// empty shell.
|
|
682
|
-
|
|
718
|
+
// Zero refs (still loading in suggestion mode, or genuinely empty). Default:
|
|
719
|
+
// no empty shell. Opt-in (`showWhenEmpty`): render the title + an empty-state
|
|
720
|
+
// line so the section is always present (e.g. people-hub "What I Shipped").
|
|
721
|
+
if (!visibleRefs.length) {
|
|
722
|
+
if (!showWhenEmpty) return null; // non-showWhenEmpty consumers stay blank (unchanged)
|
|
723
|
+
// Client-fetch loading (author/suggestion mode, no SSR initialItems): render a
|
|
724
|
+
// SKELETON grid — reserves height + matches the rest of the app's loading, so
|
|
725
|
+
// there's no blank-then-pop jump (the prior `return null` collapsed the tab to
|
|
726
|
+
// zero height during the fetch). SSR controlled mode has isLoading=false → it
|
|
727
|
+
// skips straight to the empty state below. Skeleton type = the requested rail
|
|
728
|
+
// type (author mode passes a single `includeTypes`).
|
|
729
|
+
if (isLoading) {
|
|
730
|
+
const skeletonType = includeTypes?.[0] ?? entityType ?? 'blog_post_existing';
|
|
731
|
+
return (
|
|
732
|
+
<div className="space-y-8">
|
|
733
|
+
<h2 className="text-2xl font-bold text-ods-text-primary">{title}</h2>
|
|
734
|
+
<div className={gridClassFor(columns)}>
|
|
735
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
736
|
+
<div key={i}>{renderSkeletonForType(skeletonType, 'default', adminCampaignCard)}</div>
|
|
737
|
+
))}
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
return (
|
|
743
|
+
<div className="space-y-8">
|
|
744
|
+
<h2 className="text-2xl font-bold text-ods-text-primary">{title}</h2>
|
|
745
|
+
{emptyState ?? <p className="text-ods-text-secondary">{emptyStateText}</p>}
|
|
746
|
+
</div>
|
|
747
|
+
);
|
|
748
|
+
}
|
|
683
749
|
|
|
684
750
|
const grouped: Record<string, ContentRef[]> = {};
|
|
685
751
|
for (const ref of visibleRefs) {
|
package/src/types/marketing.ts
CHANGED
|
@@ -346,6 +346,7 @@ export type ContentSourceType =
|
|
|
346
346
|
| 'webinar' // Webinars
|
|
347
347
|
| 'investor_update' // Investor updates
|
|
348
348
|
| 'onboarding_guide' // Onboarding guides (lives on openframe platform)
|
|
349
|
+
| 'what_i_shipped' // What I Shipped employee check-ins (lives on people-hub)
|
|
349
350
|
| 'from_scratch';
|
|
350
351
|
|
|
351
352
|
export type URLInjectionPreference = 'none' | 'in_post' | 'as_comment';
|
|
@@ -95,6 +95,7 @@ export interface VideoProcessingFields {
|
|
|
95
95
|
* - marketing_campaign: Marketing campaign content generation (autonomous workflow)
|
|
96
96
|
* - case_study: Case study generation from customer interviews
|
|
97
97
|
* - onboarding_guide: Step-by-step onboarding guides with optional video walkthrough
|
|
98
|
+
* - what_i_shipped: Monthly employee "What I Shipped" entries (people-hub) with transcription/bites/highlight
|
|
98
99
|
*/
|
|
99
100
|
export type VideoProcessingEntityType =
|
|
100
101
|
| 'customer_interview'
|
|
@@ -105,4 +106,5 @@ export type VideoProcessingEntityType =
|
|
|
105
106
|
| 'podcast_episode'
|
|
106
107
|
| 'investor_update'
|
|
107
108
|
| 'onboarding_guide'
|
|
109
|
+
| 'what_i_shipped'
|
|
108
110
|
// Fri May 15 14:58:59 EDT 2026
|
|
@@ -53,6 +53,7 @@ export const CONTENT_REF_GROUPS: Record<string, ContentRefGroupConfig> = {
|
|
|
53
53
|
blog_post_existing: { label: 'Blog Posts', order: 7, layout: 'grid', gridSize: 'default' },
|
|
54
54
|
customer_interview: { label: 'Customer Interviews', order: 8, layout: 'grid', gridSize: 'default' },
|
|
55
55
|
onboarding_guide: { label: 'Onboarding Guides', order: 9, layout: 'list', gridSize: 'default' },
|
|
56
|
+
what_i_shipped: { label: 'What I Shipped', order: 10, layout: 'grid', gridSize: 'default' },
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/** Human-readable label for a content_ref `type`. Returns null when the type
|
package/src/utils/format.ts
CHANGED
|
@@ -311,6 +311,21 @@ export function formatDateUTC(
|
|
|
311
311
|
})
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Format a reporting-month value (`entry_month`) as "Mon YYYY" / "Month YYYY",
|
|
316
|
+
* always anchored to UTC. THE single home for the "What I Shipped" month label —
|
|
317
|
+
* both the lib card (`'short'`) and the hub detail page (`'long'`) call this, so
|
|
318
|
+
* the React #418 UTC-pin convention lives in exactly one place. Returns `null`
|
|
319
|
+
* for empty input (callers omit the label entirely).
|
|
320
|
+
*/
|
|
321
|
+
export function formatEntryMonthUTC(
|
|
322
|
+
entryMonth: string | null | undefined,
|
|
323
|
+
style: 'short' | 'long' = 'short',
|
|
324
|
+
): string | null {
|
|
325
|
+
if (!entryMonth) return null
|
|
326
|
+
return new Date(entryMonth).toLocaleDateString('en-US', { month: style, year: 'numeric', timeZone: 'UTC' })
|
|
327
|
+
}
|
|
328
|
+
|
|
314
329
|
/**
|
|
315
330
|
* Format a date string as `MM/DD/YYYY` for legal-document display
|
|
316
331
|
* (privacy policy, terms of service). Locale-stable: always en-US.
|