@flamingo-stack/openframe-frontend-core 0.0.404-snapshot.20260711053526 → 0.0.404
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-F3YLM5PO.js → chunk-3I4IZYHM.js} +3 -3
- package/dist/{chunk-QYTGHN2T.js → chunk-5Z5GXXRU.js} +4 -4
- package/dist/{chunk-DHIYBC5Z.cjs → chunk-6MO5HF6P.cjs} +32 -32
- package/dist/{chunk-DHIYBC5Z.cjs.map → chunk-6MO5HF6P.cjs.map} +1 -1
- package/dist/{chunk-UIAQXIA6.js → chunk-A636Y7GU.js} +5 -5
- package/dist/{chunk-BK7QCB5J.js → chunk-AMPQKNAX.js} +6 -6
- package/dist/{chunk-ACJ4CM6H.cjs → chunk-CRUPO3MR.cjs} +93 -93
- package/dist/{chunk-ACJ4CM6H.cjs.map → chunk-CRUPO3MR.cjs.map} +1 -1
- package/dist/{chunk-E6YHDT3D.js → chunk-DH6HTYOV.js} +2 -6
- package/dist/chunk-DH6HTYOV.js.map +1 -0
- package/dist/{chunk-VFWEFR3Q.cjs → chunk-FFWIUNOT.cjs} +40 -40
- package/dist/{chunk-VFWEFR3Q.cjs.map → chunk-FFWIUNOT.cjs.map} +1 -1
- package/dist/{chunk-FFYEDIAG.cjs → chunk-G3QYCV63.cjs} +441 -499
- package/dist/chunk-G3QYCV63.cjs.map +1 -0
- package/dist/{chunk-ELLTTMPE.cjs → chunk-G4YLKA7O.cjs} +5 -5
- package/dist/{chunk-ELLTTMPE.cjs.map → chunk-G4YLKA7O.cjs.map} +1 -1
- package/dist/{chunk-BX3R44MR.cjs → chunk-GA5AN24T.cjs} +2 -2
- package/dist/chunk-GA5AN24T.cjs.map +1 -0
- package/dist/{chunk-GT7AJXKH.js → chunk-GYFUGMNM.js} +2 -2
- package/dist/{chunk-5XZEY34X.js → chunk-HBO2DMX4.js} +3 -3
- package/dist/{chunk-WJRPMULP.js → chunk-HGKWTKW7.js} +2 -2
- package/dist/chunk-HGKWTKW7.js.map +1 -0
- package/dist/{chunk-CCIBZ7TO.cjs → chunk-J4XR6YYZ.cjs} +6 -10
- package/dist/chunk-J4XR6YYZ.cjs.map +1 -0
- package/dist/{chunk-V4MOB3AD.cjs → chunk-KVB7SHXY.cjs} +12 -12
- package/dist/{chunk-V4MOB3AD.cjs.map → chunk-KVB7SHXY.cjs.map} +1 -1
- package/dist/{chunk-6OQ7YOX5.cjs → chunk-M7LIGEAX.cjs} +33 -33
- package/dist/{chunk-6OQ7YOX5.cjs.map → chunk-M7LIGEAX.cjs.map} +1 -1
- package/dist/{chunk-PNJ5LMVK.js → chunk-MTBFE3JF.js} +2 -16
- package/dist/chunk-MTBFE3JF.js.map +1 -0
- package/dist/{chunk-3XCNFTFV.js → chunk-NOWXUQWF.js} +3 -3
- package/dist/{chunk-WEZIVLCO.cjs → chunk-OJ4HFM3I.cjs} +18 -18
- package/dist/{chunk-WEZIVLCO.cjs.map → chunk-OJ4HFM3I.cjs.map} +1 -1
- package/dist/{chunk-M6BGJVDY.cjs → chunk-PFQQ5H6G.cjs} +19 -19
- package/dist/{chunk-M6BGJVDY.cjs.map → chunk-PFQQ5H6G.cjs.map} +1 -1
- package/dist/{chunk-QAFRLO5N.cjs → chunk-Q3AO2QWK.cjs} +4 -18
- package/dist/chunk-Q3AO2QWK.cjs.map +1 -0
- package/dist/{chunk-LREN3G7R.cjs → chunk-QXAHXMPD.cjs} +12 -12
- package/dist/{chunk-LREN3G7R.cjs.map → chunk-QXAHXMPD.cjs.map} +1 -1
- package/dist/{chunk-RC35ITSR.js → chunk-R6SMQ2CO.js} +6 -6
- package/dist/{chunk-RIGB7LP7.js → chunk-RNKXG55J.js} +4 -4
- package/dist/{chunk-FOJRR647.cjs → chunk-S4ZHBKGS.cjs} +73 -73
- package/dist/{chunk-FOJRR647.cjs.map → chunk-S4ZHBKGS.cjs.map} +1 -1
- package/dist/{chunk-BS6O4HLJ.cjs → chunk-TBXE5S6P.cjs} +40 -40
- package/dist/{chunk-BS6O4HLJ.cjs.map → chunk-TBXE5S6P.cjs.map} +1 -1
- package/dist/{chunk-ICJRSNBM.js → chunk-TTUQSOBL.js} +3 -61
- package/dist/chunk-TTUQSOBL.js.map +1 -0
- package/dist/{chunk-UN2BJ7XW.js → chunk-YORONSXG.js} +3 -3
- package/dist/{chunk-Q2KKEE6W.js → chunk-YXOWUHGQ.js} +6 -6
- package/dist/components/announcement-bar.d.ts +1 -27
- package/dist/components/announcement-bar.d.ts.map +1 -1
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/index.cjs +4 -4
- package/dist/components/chat/index.js +3 -3
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +299 -274
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +135 -110
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/file-manager/index.cjs +50 -50
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +4 -4
- package/dist/components/ui/index.js +3 -3
- package/dist/hooks/api-hooks.d.ts +4 -0
- package/dist/hooks/api-hooks.d.ts.map +1 -1
- package/dist/hooks/index.cjs +2 -2
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/use-self-fetch.d.ts +0 -7
- package/dist/hooks/use-self-fetch.d.ts.map +1 -1
- package/dist/index.cjs +4 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -19
- package/dist/types/announcement.cjs +0 -5
- package/dist/types/announcement.cjs.map +1 -1
- package/dist/types/announcement.d.ts +7 -37
- package/dist/types/announcement.d.ts.map +1 -1
- package/dist/types/announcement.js +1 -5
- package/dist/types/announcement.js.map +1 -1
- package/dist/types/index.cjs +0 -5
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +1 -5
- package/dist/types/index.js.map +1 -1
- package/dist/utils/announcement-storage.d.ts +7 -39
- package/dist/utils/announcement-storage.d.ts.map +1 -1
- package/dist/utils/app-config.d.ts.map +1 -1
- package/dist/utils/color-analysis.d.ts +0 -13
- package/dist/utils/color-analysis.d.ts.map +1 -1
- package/dist/utils/index.cjs +1 -58
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -52
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/.announcement-bar.md +30 -32
- package/src/components/announcement-bar.tsx +182 -227
- package/src/components/ui/button/button.tsx +1 -1
- package/src/hooks/.api-hooks.md +4 -1
- package/src/hooks/api-hooks.ts +7 -0
- package/src/hooks/use-self-fetch.ts +1 -29
- package/src/stories/AnnouncementBar.stories.tsx +152 -188
- package/src/types/.announcement.md +2 -2
- package/src/types/announcement.ts +7 -37
- package/src/utils/.announcement-storage.md +24 -19
- package/src/utils/announcement-storage.ts +28 -74
- package/src/utils/app-config.ts +1 -10
- package/src/utils/color-analysis.ts +1 -16
- package/src/utils/index.ts +0 -3
- package/dist/chunk-BX3R44MR.cjs.map +0 -1
- package/dist/chunk-CCIBZ7TO.cjs.map +0 -1
- package/dist/chunk-E6YHDT3D.js.map +0 -1
- package/dist/chunk-FFYEDIAG.cjs.map +0 -1
- package/dist/chunk-ICJRSNBM.js.map +0 -1
- package/dist/chunk-PNJ5LMVK.js.map +0 -1
- package/dist/chunk-QAFRLO5N.cjs.map +0 -1
- package/dist/chunk-WJRPMULP.js.map +0 -1
- /package/dist/{chunk-F3YLM5PO.js.map → chunk-3I4IZYHM.js.map} +0 -0
- /package/dist/{chunk-QYTGHN2T.js.map → chunk-5Z5GXXRU.js.map} +0 -0
- /package/dist/{chunk-UIAQXIA6.js.map → chunk-A636Y7GU.js.map} +0 -0
- /package/dist/{chunk-BK7QCB5J.js.map → chunk-AMPQKNAX.js.map} +0 -0
- /package/dist/{chunk-GT7AJXKH.js.map → chunk-GYFUGMNM.js.map} +0 -0
- /package/dist/{chunk-5XZEY34X.js.map → chunk-HBO2DMX4.js.map} +0 -0
- /package/dist/{chunk-3XCNFTFV.js.map → chunk-NOWXUQWF.js.map} +0 -0
- /package/dist/{chunk-RC35ITSR.js.map → chunk-R6SMQ2CO.js.map} +0 -0
- /package/dist/{chunk-RIGB7LP7.js.map → chunk-RNKXG55J.js.map} +0 -0
- /package/dist/{chunk-UN2BJ7XW.js.map → chunk-YORONSXG.js.map} +0 -0
- /package/dist/{chunk-Q2KKEE6W.js.map → chunk-YXOWUHGQ.js.map} +0 -0
|
@@ -31,30 +31,19 @@ export interface UseSelfFetchResult<T> {
|
|
|
31
31
|
* - a `cancelled` guard ensures an in-flight response from a STALE `url`
|
|
32
32
|
* (e.g. a fast pagination / section toggle) can't overwrite a newer one.
|
|
33
33
|
* - `reload()` bumps an internal key to retry after an error.
|
|
34
|
-
* - `revalidateOnVisibleAfterMs` (opt-in) → re-fetch when the tab becomes
|
|
35
|
-
* visible AND the held data is older than the threshold. Event-driven
|
|
36
|
-
* freshness with ZERO background requests — the replacement for
|
|
37
|
-
* setInterval polling. Data age counts from the last completed fetch, or
|
|
38
|
-
* from mount when `initialData` seeded it (an SSR seed is milliseconds
|
|
39
|
-
* old — without that, the first refocus would always refire).
|
|
40
34
|
*
|
|
41
35
|
* Re-fetches whenever `url` changes, so callers fold all query params INTO the
|
|
42
36
|
* url string (the url IS the cache key).
|
|
43
37
|
*/
|
|
44
38
|
export function useSelfFetch<T>(
|
|
45
39
|
url: string | null,
|
|
46
|
-
options?: { initialData?: T
|
|
40
|
+
options?: { initialData?: T },
|
|
47
41
|
): UseSelfFetchResult<T> {
|
|
48
42
|
const initialData = options?.initialData
|
|
49
|
-
const revalidateOnVisibleAfterMs = options?.revalidateOnVisibleAfterMs
|
|
50
43
|
const [data, setData] = useState<T | null>(initialData ?? null)
|
|
51
44
|
const [isLoading, setIsLoading] = useState(initialData === undefined && url !== null)
|
|
52
45
|
const [error, setError] = useState(false)
|
|
53
46
|
const [reloadKey, setReloadKey] = useState(0)
|
|
54
|
-
// When the held data was obtained: seeded at mount for SSR-hydrated data,
|
|
55
|
-
// stamped after each completed fetch. Drives the visibility revalidation age
|
|
56
|
-
// check. 0 = nothing held yet (first visible event may fetch).
|
|
57
|
-
const dataAtRef = useRef<number>(initialData !== undefined ? Date.now() : 0)
|
|
58
47
|
// The url whose data we currently hold — seeded from SSR `initialData`, then set after
|
|
59
48
|
// each completed fetch. The effect skips the fetch when this equals `url` (so server-
|
|
60
49
|
// rendered data isn't re-fetched on mount). A VALUE compare (not a one-shot flag) so the
|
|
@@ -89,7 +78,6 @@ export function useSelfFetch<T>(
|
|
|
89
78
|
if (!cancelled) {
|
|
90
79
|
setData(json)
|
|
91
80
|
dataUrlRef.current = url // remember the url we now hold data for
|
|
92
|
-
dataAtRef.current = Date.now()
|
|
93
81
|
}
|
|
94
82
|
} catch (err) {
|
|
95
83
|
// AbortError on cleanup (unmount / stale-url change / React StrictMode's dev
|
|
@@ -112,22 +100,6 @@ export function useSelfFetch<T>(
|
|
|
112
100
|
// `url` folds in every query param; `reloadKey` drives retry.
|
|
113
101
|
}, [url, reloadKey])
|
|
114
102
|
|
|
115
|
-
// Opt-in visibility-driven revalidation. Registered only when the option is
|
|
116
|
-
// set and fetching is enabled; SSR-safe (effects don't run on the server)
|
|
117
|
-
// and StrictMode-safe (idempotent listener, cleaned up per mount cycle).
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
if (!revalidateOnVisibleAfterMs || url === null) return
|
|
120
|
-
const onVisible = () => {
|
|
121
|
-
if (document.visibilityState !== 'visible') return
|
|
122
|
-
if (Date.now() - dataAtRef.current < revalidateOnVisibleAfterMs) return
|
|
123
|
-
// Same mechanics as reload(): clear the held-url skip, bump the key.
|
|
124
|
-
dataUrlRef.current = null
|
|
125
|
-
setReloadKey((k) => k + 1)
|
|
126
|
-
}
|
|
127
|
-
document.addEventListener('visibilitychange', onVisible)
|
|
128
|
-
return () => document.removeEventListener('visibilitychange', onVisible)
|
|
129
|
-
}, [url, revalidateOnVisibleAfterMs])
|
|
130
|
-
|
|
131
103
|
return {
|
|
132
104
|
data,
|
|
133
105
|
setData,
|
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
|
2
2
|
import type { Announcement } from '../types/announcement'
|
|
3
3
|
import { AnnouncementBar } from '../components/announcement-bar'
|
|
4
|
-
import { announcementDismissCookieName } from '../utils/announcement-storage'
|
|
5
|
-
import { getAppType } from '../utils/app-config'
|
|
6
|
-
import { EndpointsRuntimeContext, type EndpointsRuntime } from '../contexts/endpoints-runtime-context'
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* mounted the bar never fetches. The decorator clears any dismissal state left
|
|
12
|
-
* by a previous interaction so every story starts visible.
|
|
6
|
+
* Mock fetch so the component receives announcement data without a real API.
|
|
7
|
+
* Also seeds localStorage so the bar isn't marked as dismissed.
|
|
13
8
|
*/
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
function mockAnnouncement(announcement: Announcement | null) {
|
|
10
|
+
// Clear any previous dismiss flags
|
|
11
|
+
if (announcement) {
|
|
12
|
+
Object.keys(localStorage).forEach((key) => {
|
|
13
|
+
if (key.includes(`announcement-${announcement.id}-dismissed`)) {
|
|
14
|
+
localStorage.removeItem(key)
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const originalFetch = window.fetch
|
|
20
|
+
window.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
21
|
+
const url = typeof input === 'string' ? input : input.toString()
|
|
22
|
+
if (url.includes('/api/announcements/active')) {
|
|
23
|
+
return new Response(JSON.stringify({ announcement }), {
|
|
24
|
+
status: 200,
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
})
|
|
20
27
|
}
|
|
21
|
-
|
|
28
|
+
return originalFetch(input, init)
|
|
29
|
+
}) as typeof window.fetch
|
|
30
|
+
|
|
31
|
+
return () => {
|
|
32
|
+
window.fetch = originalFetch
|
|
33
|
+
}
|
|
22
34
|
}
|
|
23
35
|
|
|
24
36
|
const now = new Date().toISOString()
|
|
@@ -29,6 +41,7 @@ const baseAnnouncement: Announcement = {
|
|
|
29
41
|
description: 'Check out our latest updates and improvements to the platform.',
|
|
30
42
|
background_color: '#FFD951',
|
|
31
43
|
icon_name: 'rocket',
|
|
44
|
+
platform_id: 'openframe',
|
|
32
45
|
is_active: true,
|
|
33
46
|
created_at: now,
|
|
34
47
|
updated_at: now,
|
|
@@ -42,16 +55,10 @@ const meta = {
|
|
|
42
55
|
docs: {
|
|
43
56
|
description: {
|
|
44
57
|
component:
|
|
45
|
-
'A top-of-page announcement bar
|
|
58
|
+
'A top-of-page announcement bar that fetches the active announcement from the API. Supports SVG/PNG icons, CTA buttons, dismiss functionality, and responsive mobile layout.',
|
|
46
59
|
},
|
|
47
60
|
},
|
|
48
61
|
},
|
|
49
|
-
decorators: [
|
|
50
|
-
(Story) => {
|
|
51
|
-
clearDismissals()
|
|
52
|
-
return <Story />
|
|
53
|
-
},
|
|
54
|
-
],
|
|
55
62
|
tags: ['autodocs'],
|
|
56
63
|
} satisfies Meta<typeof AnnouncementBar>
|
|
57
64
|
|
|
@@ -62,232 +69,189 @@ type Story = StoryObj<typeof meta>
|
|
|
62
69
|
* Default announcement with an icon and description.
|
|
63
70
|
*/
|
|
64
71
|
export const Default: Story = {
|
|
65
|
-
|
|
72
|
+
decorators: [
|
|
73
|
+
(Story) => {
|
|
74
|
+
const cleanup = mockAnnouncement(baseAnnouncement)
|
|
75
|
+
return (
|
|
76
|
+
<>
|
|
77
|
+
<Story />
|
|
78
|
+
{/* cleanup on unmount is handled by storybook re-renders */}
|
|
79
|
+
</>
|
|
80
|
+
)
|
|
81
|
+
},
|
|
82
|
+
],
|
|
66
83
|
}
|
|
67
84
|
|
|
68
85
|
/**
|
|
69
86
|
* Announcement with a CTA button.
|
|
70
87
|
*/
|
|
71
88
|
export const WithCTA: Story = {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
decorators: [
|
|
90
|
+
(Story) => {
|
|
91
|
+
mockAnnouncement({
|
|
92
|
+
...baseAnnouncement,
|
|
93
|
+
id: 'story-cta',
|
|
94
|
+
title: 'OpenFrame v2.0 is here!',
|
|
95
|
+
description: 'Explore the new dashboard, improved device management, and more.',
|
|
96
|
+
background_color: '#FFD951',
|
|
97
|
+
icon_name: 'star',
|
|
98
|
+
cta_enabled: true,
|
|
99
|
+
cta_text: 'Learn More',
|
|
100
|
+
cta_url: 'https://example.com',
|
|
101
|
+
cta_target: '_blank',
|
|
102
|
+
cta_icon_name: 'rocket',
|
|
103
|
+
cta_show_icon: true,
|
|
104
|
+
cta_button_background_color: '#1A1A1A',
|
|
105
|
+
cta_button_text_color: '#FFFFFF',
|
|
106
|
+
})
|
|
107
|
+
return <Story />
|
|
88
108
|
},
|
|
89
|
-
|
|
109
|
+
],
|
|
90
110
|
}
|
|
91
111
|
|
|
92
112
|
/**
|
|
93
113
|
* Announcement with a CTA button that opens in the same tab.
|
|
94
114
|
*/
|
|
95
115
|
export const WithCTASameTab: Story = {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Dark background — text flips to the light shade via pickReadableTextColor.
|
|
114
|
-
*/
|
|
115
|
-
export const DarkBackground: Story = {
|
|
116
|
-
args: {
|
|
117
|
-
initialAnnouncement: {
|
|
118
|
-
...baseAnnouncement,
|
|
119
|
-
id: 'story-dark',
|
|
120
|
-
title: 'Contrast-aware text',
|
|
121
|
-
description: 'On a dark admin-chosen background the foreground goes light automatically.',
|
|
122
|
-
background_color: '#1F2937',
|
|
123
|
-
icon_name: 'megaphone',
|
|
116
|
+
decorators: [
|
|
117
|
+
(Story) => {
|
|
118
|
+
mockAnnouncement({
|
|
119
|
+
...baseAnnouncement,
|
|
120
|
+
id: 'story-cta-same',
|
|
121
|
+
title: 'Scheduled Maintenance',
|
|
122
|
+
description: 'We will perform maintenance on Saturday from 2:00 AM to 4:00 AM UTC.',
|
|
123
|
+
background_color: '#FFE082',
|
|
124
|
+
icon_name: 'info',
|
|
125
|
+
cta_enabled: true,
|
|
126
|
+
cta_text: 'View Status',
|
|
127
|
+
cta_url: '/status',
|
|
128
|
+
cta_target: '_self',
|
|
129
|
+
})
|
|
130
|
+
return <Story />
|
|
124
131
|
},
|
|
125
|
-
|
|
132
|
+
],
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
/**
|
|
129
136
|
* Announcement with a blue background.
|
|
130
137
|
*/
|
|
131
138
|
export const BlueBackground: Story = {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
decorators: [
|
|
140
|
+
(Story) => {
|
|
141
|
+
mockAnnouncement({
|
|
142
|
+
...baseAnnouncement,
|
|
143
|
+
id: 'story-blue',
|
|
144
|
+
title: 'Join our Community',
|
|
145
|
+
description: 'Connect with other MSPs and share best practices.',
|
|
146
|
+
background_color: '#60A5FA',
|
|
147
|
+
icon_name: 'megaphone',
|
|
148
|
+
})
|
|
149
|
+
return <Story />
|
|
140
150
|
},
|
|
141
|
-
|
|
151
|
+
],
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
/**
|
|
145
155
|
* Announcement with a green background.
|
|
146
156
|
*/
|
|
147
157
|
export const GreenBackground: Story = {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
decorators: [
|
|
159
|
+
(Story) => {
|
|
160
|
+
mockAnnouncement({
|
|
161
|
+
...baseAnnouncement,
|
|
162
|
+
id: 'story-green',
|
|
163
|
+
title: 'All Systems Operational',
|
|
164
|
+
description: 'Everything is running smoothly. No issues detected.',
|
|
165
|
+
background_color: '#86EFAC',
|
|
166
|
+
icon_name: 'bell',
|
|
167
|
+
})
|
|
168
|
+
return <Story />
|
|
156
169
|
},
|
|
157
|
-
|
|
170
|
+
],
|
|
158
171
|
}
|
|
159
172
|
|
|
160
173
|
/**
|
|
161
174
|
* Announcement with OpenFrame logo icon.
|
|
162
175
|
*/
|
|
163
176
|
export const WithOpenFrameLogo: Story = {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
decorators: [
|
|
178
|
+
(Story) => {
|
|
179
|
+
mockAnnouncement({
|
|
180
|
+
...baseAnnouncement,
|
|
181
|
+
id: 'story-logo',
|
|
182
|
+
title: 'Welcome to OpenFrame',
|
|
183
|
+
description: 'Your open-source IT infrastructure management platform.',
|
|
184
|
+
background_color: '#FFD951',
|
|
185
|
+
icon_name: 'openframe-logo',
|
|
186
|
+
cta_enabled: true,
|
|
187
|
+
cta_text: 'Get Started',
|
|
188
|
+
cta_url: '/getting-started',
|
|
189
|
+
cta_target: '_self',
|
|
190
|
+
cta_button_background_color: '#000000',
|
|
191
|
+
cta_button_text_color: '#FFD951',
|
|
192
|
+
})
|
|
193
|
+
return <Story />
|
|
178
194
|
},
|
|
179
|
-
|
|
195
|
+
],
|
|
180
196
|
}
|
|
181
197
|
|
|
182
198
|
/**
|
|
183
199
|
* Announcement with a PNG icon.
|
|
184
200
|
*/
|
|
185
201
|
export const WithPNGIcon: Story = {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
202
|
+
decorators: [
|
|
203
|
+
(Story) => {
|
|
204
|
+
mockAnnouncement({
|
|
205
|
+
...baseAnnouncement,
|
|
206
|
+
id: 'story-png',
|
|
207
|
+
title: 'Partner Program Launch',
|
|
208
|
+
description: 'Become an OpenFrame certified partner and grow your business.',
|
|
209
|
+
background_color: '#C4B5FD',
|
|
210
|
+
icon_url: 'https://placehold.co/32x32/1a1a1a/white?text=OF',
|
|
211
|
+
})
|
|
212
|
+
return <Story />
|
|
194
213
|
},
|
|
195
|
-
|
|
214
|
+
],
|
|
196
215
|
}
|
|
197
216
|
|
|
198
217
|
/**
|
|
199
218
|
* Long title and description to test truncation.
|
|
200
219
|
*/
|
|
201
220
|
export const LongContent: Story = {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* previewMode — the admin live-preview path: render-only, inert dismiss,
|
|
221
|
-
* no fetch and no storage side effects.
|
|
222
|
-
*/
|
|
223
|
-
export const PreviewMode: Story = {
|
|
224
|
-
args: {
|
|
225
|
-
initialAnnouncement: {
|
|
226
|
-
...baseAnnouncement,
|
|
227
|
-
id: 'story-preview',
|
|
228
|
-
title: 'Admin preview',
|
|
229
|
-
description: 'Exactly the live bar, but inert: dismiss does nothing.',
|
|
221
|
+
decorators: [
|
|
222
|
+
(Story) => {
|
|
223
|
+
mockAnnouncement({
|
|
224
|
+
...baseAnnouncement,
|
|
225
|
+
id: 'story-long',
|
|
226
|
+
title: 'This is a very long announcement title that should be truncated on smaller screens to prevent layout issues',
|
|
227
|
+
description:
|
|
228
|
+
'This is an extremely long description that goes into great detail about the announcement. It should be truncated on the desktop view and hidden on mobile view to keep the bar compact and readable.',
|
|
229
|
+
background_color: '#FCA5A5',
|
|
230
|
+
icon_name: 'info',
|
|
231
|
+
cta_enabled: true,
|
|
232
|
+
cta_text: 'Read Full Announcement',
|
|
233
|
+
cta_url: '/announcements/long',
|
|
234
|
+
cta_target: '_self',
|
|
235
|
+
})
|
|
236
|
+
return <Story />
|
|
230
237
|
},
|
|
231
|
-
|
|
232
|
-
},
|
|
238
|
+
],
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
/**
|
|
236
242
|
* No active announcement — the bar renders nothing.
|
|
237
243
|
*/
|
|
238
244
|
export const NoAnnouncement: Story = {
|
|
239
|
-
args: { initialAnnouncement: null },
|
|
240
|
-
decorators: [
|
|
241
|
-
(Story) => (
|
|
242
|
-
<div>
|
|
243
|
-
<Story />
|
|
244
|
-
<p className="p-4 text-sm text-gray-500">
|
|
245
|
-
No announcement is active — the bar renders nothing above this text.
|
|
246
|
-
</p>
|
|
247
|
-
</div>
|
|
248
|
-
),
|
|
249
|
-
],
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Client-only mode (no SSR) — the react-embedding-example structure: the
|
|
254
|
-
* embedder mounts an EndpointsRuntime provider (announcementsUrl is a fixed
|
|
255
|
-
* suffix under its one content base) and drops in a PROP-LESS bar. No
|
|
256
|
-
* platform knob anywhere on the client: the proxied server resolves its own
|
|
257
|
-
* platform via currentPlatform(). The decorator mocks the fetch.
|
|
258
|
-
*/
|
|
259
|
-
const embedEndpoints: EndpointsRuntime = {
|
|
260
|
-
announcementsUrl: '/content/api/announcements/active',
|
|
261
|
-
accessCode: { validateUrl: '/content/api/validate-access-code', consumeUrl: '/content/api/consume-access-code' },
|
|
262
|
-
contactUrl: '/content/api/contact',
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export const ClientOnlyEmbed: Story = {
|
|
266
|
-
args: {},
|
|
267
245
|
decorators: [
|
|
268
246
|
(Story) => {
|
|
269
|
-
|
|
270
|
-
window.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
271
|
-
const requestUrl = typeof input === 'string' ? input : input.toString()
|
|
272
|
-
if (requestUrl.includes('/content/api/announcements/active')) {
|
|
273
|
-
return new Response(
|
|
274
|
-
JSON.stringify({
|
|
275
|
-
announcement: {
|
|
276
|
-
...baseAnnouncement,
|
|
277
|
-
id: 'story-embed',
|
|
278
|
-
title: 'Client-only embed',
|
|
279
|
-
description: 'Fetched on mount through the EndpointsRuntime provider, no SSR.',
|
|
280
|
-
},
|
|
281
|
-
}),
|
|
282
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
283
|
-
)
|
|
284
|
-
}
|
|
285
|
-
return originalFetch(input, init)
|
|
286
|
-
}) as typeof window.fetch
|
|
247
|
+
mockAnnouncement(null)
|
|
287
248
|
return (
|
|
288
|
-
<
|
|
249
|
+
<div>
|
|
289
250
|
<Story />
|
|
290
|
-
|
|
251
|
+
<p className="p-4 text-sm text-gray-500">
|
|
252
|
+
No announcement is active — the bar renders nothing above this text.
|
|
253
|
+
</p>
|
|
254
|
+
</div>
|
|
291
255
|
)
|
|
292
256
|
},
|
|
293
257
|
],
|
|
@@ -4,7 +4,7 @@ TypeScript type definitions for the announcement system, providing comprehensive
|
|
|
4
4
|
## Key Components
|
|
5
5
|
|
|
6
6
|
### Core Database Models
|
|
7
|
-
- **`Announcement`** - Main announcement entity
|
|
7
|
+
- **`Announcement`** - Main announcement entity with title, description, platform targeting, and visual customization
|
|
8
8
|
- **`AnnouncementAsset`** - File attachments linked to announcements (images, documents, etc.)
|
|
9
9
|
|
|
10
10
|
### API Types
|
|
@@ -30,7 +30,7 @@ const newAnnouncement: CreateAnnouncementData = {
|
|
|
30
30
|
background_color: "#FFC008",
|
|
31
31
|
icon_type: "svg",
|
|
32
32
|
icon_svg_name: "rocket",
|
|
33
|
-
|
|
33
|
+
platform_id: "openframe-platform-id",
|
|
34
34
|
is_active: true,
|
|
35
35
|
cta_enabled: true,
|
|
36
36
|
cta_text: "Learn More",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Announcement system TypeScript types
|
|
2
|
-
import type { LegacyPlatform, PlatformFilter } from './platform';
|
|
3
|
-
import type { EntityPlatformAssoc } from './entity-platform';
|
|
2
|
+
import type { LegacyPlatform, PlatformFilter, PlatformRecord } from './platform';
|
|
4
3
|
|
|
4
|
+
// For backward compatibility, maintain the old Platform type temporarily
|
|
5
5
|
// Database Models
|
|
6
6
|
export interface Announcement {
|
|
7
7
|
id: string;
|
|
@@ -12,9 +12,8 @@ export interface Announcement {
|
|
|
12
12
|
icon_url?: string;
|
|
13
13
|
/** Extra props for the main bar/icon */
|
|
14
14
|
icon_props?: Record<string, any>;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
announcement_platforms?: EntityPlatformAssoc[];
|
|
15
|
+
platform_id: string; // Foreign key to platforms table
|
|
16
|
+
platform?: PlatformRecord; // Joined platform data
|
|
18
17
|
is_active: boolean;
|
|
19
18
|
// CTA (Call-To-Action) fields
|
|
20
19
|
cta_enabled?: boolean;
|
|
@@ -110,25 +109,10 @@ export interface AnnouncementListResponse {
|
|
|
110
109
|
};
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
/**
|
|
114
|
-
* Wire shape of GET /api/announcements/active — the single typed home for the
|
|
115
|
-
* `{ announcement }` envelope (hub route return + AnnouncementBar's
|
|
116
|
-
* useSelfFetch generic). `null` when no active announcement exists.
|
|
117
|
-
*/
|
|
118
112
|
export interface AnnouncementResponse {
|
|
119
|
-
announcement: Announcement
|
|
113
|
+
announcement: Announcement;
|
|
120
114
|
}
|
|
121
115
|
|
|
122
|
-
/**
|
|
123
|
-
* Default CTA button colors — one home for the pair the DAL create/update
|
|
124
|
-
* defaults and the admin form both reference. Announcement colors are
|
|
125
|
-
* admin-chosen data (not ODS-token surfaces) by design.
|
|
126
|
-
*/
|
|
127
|
-
export const ANNOUNCEMENT_CTA_DEFAULTS = {
|
|
128
|
-
background: '#1A1A1A',
|
|
129
|
-
text: '#FAFAFA',
|
|
130
|
-
} as const;
|
|
131
|
-
|
|
132
116
|
// Form Data Types
|
|
133
117
|
export interface AnnouncementFormData {
|
|
134
118
|
title: string;
|
|
@@ -230,22 +214,8 @@ export interface AnnouncementStats {
|
|
|
230
214
|
|
|
231
215
|
// Component Props Types
|
|
232
216
|
export interface AnnouncementBarProps {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
* announcement, or dismissed via cookie) — seeds the fetch hook and skips
|
|
236
|
-
* the mount fetch. Absent (`undefined`) = client-only mode: the bar
|
|
237
|
-
* self-fetches on mount from the EndpointsRuntime provider's
|
|
238
|
-
* `announcementsUrl` (see react-embedding-example — no per-component URL
|
|
239
|
-
* or platform knobs; the server resolves its own platform via
|
|
240
|
-
* currentPlatform()).
|
|
241
|
-
*/
|
|
242
|
-
initialAnnouncement?: Announcement | null;
|
|
243
|
-
/**
|
|
244
|
-
* Render-only mode for the admin live preview: no fetch/revalidation, inert
|
|
245
|
-
* dismiss button, and no storage side effects (never writes dismissal
|
|
246
|
-
* cookies or touches localStorage).
|
|
247
|
-
*/
|
|
248
|
-
previewMode?: boolean;
|
|
217
|
+
announcement?: Announcement;
|
|
218
|
+
onDismiss?: (announcementId: string) => void;
|
|
249
219
|
className?: string;
|
|
250
220
|
}
|
|
251
221
|
|
|
@@ -1,31 +1,36 @@
|
|
|
1
|
-
<!-- source-hash:
|
|
2
|
-
|
|
1
|
+
<!-- source-hash: eb9e1e114c43007debee28971d977a02 -->
|
|
2
|
+
A utility module for managing announcement data persistence in browser localStorage with safe server-side rendering support.
|
|
3
3
|
|
|
4
4
|
## Key Components
|
|
5
5
|
|
|
6
|
-
- **`
|
|
7
|
-
- **`
|
|
8
|
-
- **`
|
|
9
|
-
- **`
|
|
10
|
-
- **`clearLegacyAnnouncementCache(platform)`** - One-time cleanup of the pre-refactor localStorage "instant paint" cache blob
|
|
6
|
+
- **`AnnouncementStorageOptions`** - Interface defining storage configuration with key and optional default value
|
|
7
|
+
- **`getStoredAnnouncement(key)`** - Retrieves and parses announcement data from localStorage
|
|
8
|
+
- **`setStoredAnnouncement(key, value)`** - Stores announcement data as JSON in localStorage
|
|
9
|
+
- **`clearStoredAnnouncement(key?)`** - Removes announcement data from localStorage (defaults to 'announcement' key)
|
|
11
10
|
|
|
12
11
|
## Usage Example
|
|
13
12
|
|
|
14
13
|
```typescript
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
isDismissedCookieValue,
|
|
14
|
+
import {
|
|
15
|
+
getStoredAnnouncement,
|
|
16
|
+
setStoredAnnouncement,
|
|
17
|
+
clearStoredAnnouncement
|
|
20
18
|
} from './announcement-storage';
|
|
21
19
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
// Store announcement dismissal state
|
|
21
|
+
setStoredAnnouncement('banner-dismissed', {
|
|
22
|
+
dismissed: true,
|
|
23
|
+
timestamp: Date.now()
|
|
24
|
+
});
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
const
|
|
28
|
-
|
|
26
|
+
// Check if announcement was dismissed
|
|
27
|
+
const dismissalState = getStoredAnnouncement('banner-dismissed');
|
|
28
|
+
if (dismissalState?.dismissed) {
|
|
29
|
+
console.log('Banner was dismissed');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Clear announcement data
|
|
33
|
+
clearStoredAnnouncement('banner-dismissed');
|
|
29
34
|
```
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
All functions include browser environment checks and error handling to prevent issues during server-side rendering or when localStorage is unavailable.
|