@cdc/core 4.25.10 → 4.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_stories/Gallery.Charts.stories.tsx +307 -0
- package/_stories/Gallery.DataBite.stories.tsx +72 -0
- package/_stories/Gallery.Maps.stories.tsx +230 -0
- package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
- package/_stories/PageART.stories.tsx +192 -0
- package/_stories/PageBRFSS.stories.tsx +289 -0
- package/_stories/PageCancerRegistries.stories.tsx +199 -0
- package/_stories/PageEasternEquineEncephalitis.stories.tsx +202 -0
- package/_stories/PageExcessiveAlcoholUse.stories.tsx +196 -0
- package/_stories/PageMaternalMortality.stories.tsx +192 -0
- package/_stories/PageOralHealth.stories.tsx +196 -0
- package/_stories/PageRespiratory.stories.tsx +332 -0
- package/_stories/PageSmokingTobacco.stories.tsx +195 -0
- package/_stories/PageStateDiabetesProfiles.stories.tsx +196 -0
- package/_stories/PageWastewater.stories.tsx +463 -0
- package/_stories/StoryRenderingTests.stories.tsx +164 -0
- package/assets/icon-magnifying-glass.svg +5 -0
- package/assets/icon-warming-stripes.svg +13 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +7 -1
- package/components/AdvancedEditor/EmbedEditor.tsx +281 -0
- package/components/ComboBox/ComboBox.tsx +345 -0
- package/components/ComboBox/combobox.styles.css +185 -0
- package/components/ComboBox/index.ts +1 -0
- package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
- package/components/CustomColorsEditor/index.ts +1 -0
- package/components/DataTable/DataTable.tsx +132 -58
- package/components/DataTable/DataTableStandAlone.tsx +8 -3
- package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
- package/components/DataTable/data-table.css +217 -210
- package/components/DataTable/helpers/mapCellMatrix.tsx +28 -9
- package/components/DataTable/helpers/standardizeState.js +2 -2
- package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
- package/components/EditorPanel/ColumnsEditor.tsx +37 -19
- package/components/EditorPanel/DataTableEditor.tsx +54 -28
- package/components/EditorPanel/EditorPanel.styles.css +439 -0
- package/components/EditorPanel/EditorPanel.tsx +144 -0
- package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
- package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
- package/components/EditorPanel/FootnotesEditor.tsx +44 -37
- package/components/EditorPanel/Inputs.tsx +44 -8
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +246 -175
- package/components/EditorPanel/components/MarkupVariablesEditor.tsx +61 -22
- package/components/EditorPanel/sections/VisualSection.tsx +169 -0
- package/components/Filters/Filters.tsx +57 -10
- package/components/Filters/components/Dropdown.tsx +6 -1
- package/components/Filters/helpers/getNestedOptions.ts +2 -1
- package/components/Filters/helpers/handleSorting.ts +1 -1
- package/components/Footnotes/Footnotes.tsx +35 -25
- package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
- package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
- package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
- package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
- package/components/HeaderThemeSelector/index.ts +2 -0
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +82 -0
- package/components/Layout/components/Visualization/index.tsx +16 -1
- package/components/Layout/components/Visualization/visualizations.scss +7 -0
- package/components/Layout/styles/editor.scss +2 -1
- package/components/Legend/Legend.Gradient.tsx +1 -1
- package/components/Loader/Loader.tsx +1 -1
- package/components/MediaControls.tsx +63 -34
- package/components/PaletteConversionModal.tsx +7 -4
- package/components/PaletteSelector/PaletteSelector.css +49 -6
- package/components/Table/components/Cell.tsx +23 -2
- package/components/Table/components/Row.tsx +5 -3
- package/components/_stories/Filters.stories.tsx +20 -1
- package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
- package/components/_stories/Footnotes.stories.tsx +768 -3
- package/components/_stories/Inputs.stories.tsx +2 -2
- package/components/_stories/styles.scss +0 -1
- package/components/ui/Accordion.jsx +1 -1
- package/components/ui/Icon.tsx +3 -1
- package/components/ui/Title/index.tsx +30 -2
- package/components/ui/Title/title.styles.css +42 -0
- package/components/ui/accordion.styles.css +57 -0
- package/data/chartColorPalettes.ts +1 -1
- package/dist/cove-main.css +75 -6
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +8 -1
- package/helpers/addValuesToFilters.ts +11 -1
- package/helpers/constants.ts +37 -0
- package/helpers/cove/number.ts +33 -12
- package/helpers/coveUpdateWorker.ts +20 -11
- package/helpers/embedCodeGenerator.ts +109 -0
- package/helpers/fetchRemoteData.ts +3 -15
- package/helpers/getUniqueValues.ts +19 -0
- package/helpers/hashObj.ts +25 -0
- package/helpers/isRightAlignedTableValue.js +5 -0
- package/helpers/markupProcessor.ts +27 -12
- package/helpers/mergeCustomOrderValues.ts +37 -0
- package/helpers/metrics/helpers.ts +1 -0
- package/helpers/parseCsvWithQuotes.ts +65 -0
- package/helpers/pivotData.ts +2 -2
- package/helpers/prepareScreenshot.ts +268 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/testing.ts +17 -4
- package/helpers/tests/prepareScreenshot.test.ts +414 -0
- package/helpers/tests/queryStringUtils.test.ts +381 -0
- package/helpers/tests/testStandaloneBuild.ts +23 -5
- package/helpers/useDataVizClasses.ts +0 -1
- package/helpers/ver/4.25.11.ts +13 -0
- package/helpers/ver/4.26.1.ts +80 -0
- package/helpers/viewports.ts +2 -0
- package/hooks/useDataColumns.ts +63 -0
- package/hooks/useFilterManagement.ts +94 -0
- package/hooks/useLegendSeparators.ts +26 -0
- package/hooks/useListManagement.ts +192 -0
- package/package.json +6 -4
- package/styles/_button-section.scss +0 -3
- package/styles/_common-components.css +73 -0
- package/styles/_global.scss +25 -5
- package/styles/base.scss +0 -50
- package/styles/cove-main.scss +3 -1
- package/styles/filters.scss +10 -3
- package/styles/v2/base/index.scss +0 -1
- package/styles/v2/components/editor.scss +14 -6
- package/styles/v2/utils/_breakpoints.scss +1 -1
- package/styles/v2/utils/index.scss +0 -1
- package/styles/waiting.scss +1 -1
- package/types/Axis.ts +1 -0
- package/types/ForecastingSeriesKey.ts +1 -0
- package/types/MarkupInclude.ts +5 -3
- package/types/MarkupVariable.ts +1 -1
- package/types/Series.ts +3 -0
- package/types/Table.ts +1 -0
- package/types/Visualization.ts +1 -0
- package/types/VizFilter.ts +2 -0
- package/LICENSE +0 -201
- package/styles/_mixins.scss +0 -13
- package/styles/_typography.scss +0 -0
- package/styles/v2/base/_typography.scss +0 -0
- package/styles/v2/components/guidance-block.scss +0 -74
- package/styles/v2/utils/_functions.scss +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { within, expect } from 'storybook/test'
|
|
3
|
+
import { performAndAssert } from '@cdc/core/helpers/testing'
|
|
4
|
+
|
|
5
|
+
const ChartRenderingValidator = () => (
|
|
6
|
+
<div data-testid='chart-rendering-validator'>
|
|
7
|
+
<h2>Simple COVE Visualization Tests</h2>
|
|
8
|
+
<p>This test validates all stories load and render.</p>
|
|
9
|
+
</div>
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof ChartRenderingValidator> = {
|
|
13
|
+
title: 'Testing/Story Rendering Tests',
|
|
14
|
+
component: ChartRenderingValidator,
|
|
15
|
+
parameters: {
|
|
16
|
+
layout: 'fullscreen'
|
|
17
|
+
},
|
|
18
|
+
tags: ['!dev', '!autodocs']
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
22
|
+
type Story = StoryObj<typeof ChartRenderingValidator>
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fetch all stories from Storybook's JSON API and filter for visualization stories
|
|
26
|
+
* @returns Promise that resolves to an array of story URLs to test
|
|
27
|
+
*/
|
|
28
|
+
const getVisualizationStoryUrls = async (): Promise<string[]> => {
|
|
29
|
+
let response
|
|
30
|
+
try {
|
|
31
|
+
response = await fetch('http://localhost:6006/index.json')
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Error fetching visualization story URLs:', error)
|
|
34
|
+
return []
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const data = await response.json()
|
|
38
|
+
|
|
39
|
+
const storyUrls: string[] = []
|
|
40
|
+
|
|
41
|
+
Object.values(data.entries).forEach((story: any) => {
|
|
42
|
+
if (story.type === 'story') {
|
|
43
|
+
const isVisualizationStory =
|
|
44
|
+
story.title.includes('Components/Templates/') &&
|
|
45
|
+
!story.name.toLowerCase().includes('test') &&
|
|
46
|
+
!story.title.includes('Guide')
|
|
47
|
+
|
|
48
|
+
if (isVisualizationStory) {
|
|
49
|
+
const iframeUrl = `http://localhost:6006/iframe.html?id=${story.id}`
|
|
50
|
+
storyUrls.push(iframeUrl)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
return storyUrls
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Convert iframe URL to Storybook story URL for better debugging
|
|
59
|
+
* @param iframeUrl - The iframe URL (e.g., 'http://localhost:6006/iframe.html?id=components-templates-chart--multiple-lines')
|
|
60
|
+
* @returns The Storybook story URL (e.g., 'http://localhost:6006/?path=/story/components-templates-chart--multiple-lines')
|
|
61
|
+
*/
|
|
62
|
+
const iframeUrlToStoryUrl = (iframeUrl: string): string => {
|
|
63
|
+
const url = new URL(iframeUrl)
|
|
64
|
+
const storyId = url.searchParams.get('id')
|
|
65
|
+
return `http://localhost:6006/?path=/story/${storyId}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Test a single Storybook iframe URL for successful visualization rendering
|
|
70
|
+
* @param iframeUrl - The complete iframe URL to test (e.g., 'http://localhost:6006/iframe.html?id=...')
|
|
71
|
+
* @returns Promise that resolves with test results
|
|
72
|
+
*/
|
|
73
|
+
const testIframeVisualization = async (iframeUrl: string) => {
|
|
74
|
+
iframeUrl = iframeUrl
|
|
75
|
+
|
|
76
|
+
const iframe = document.createElement('iframe')
|
|
77
|
+
iframe.style.width = '1200px'
|
|
78
|
+
iframe.style.height = '800px'
|
|
79
|
+
iframe.src = iframeUrl
|
|
80
|
+
document.body.appendChild(iframe)
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await performAndAssert(
|
|
84
|
+
'Wait for iframe to load',
|
|
85
|
+
() => {
|
|
86
|
+
try {
|
|
87
|
+
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document
|
|
88
|
+
return {
|
|
89
|
+
loaded: !!iframeDoc && iframeDoc.readyState !== 'loading',
|
|
90
|
+
readyState: iframeDoc?.readyState || 'unknown'
|
|
91
|
+
}
|
|
92
|
+
} catch (error: any) {
|
|
93
|
+
return { loaded: false, readyState: 'error', error: error.message }
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
async () => {},
|
|
97
|
+
(before, after) => {
|
|
98
|
+
return after.loaded
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
await performAndAssert(
|
|
103
|
+
'Wait for SVG elements to render in iframe',
|
|
104
|
+
() => {
|
|
105
|
+
try {
|
|
106
|
+
const iframeDoc = iframe?.contentDocument || iframe?.contentWindow?.document
|
|
107
|
+
if (!iframeDoc) return { svgCount: 0, hasCoveModule: false, error: 'No document access' }
|
|
108
|
+
|
|
109
|
+
const svgCount = iframeDoc.querySelectorAll('svg').length
|
|
110
|
+
const hasCoveModule = !!iframeDoc.querySelector('.cdc-open-viz-module')
|
|
111
|
+
const isDataBite = !!iframeDoc.querySelector('.bite-content')
|
|
112
|
+
const isDataTable = !!iframeDoc.querySelector('.data-table')
|
|
113
|
+
|
|
114
|
+
return { svgCount, hasCoveModule, isDataBite, isDataTable, error: null }
|
|
115
|
+
} catch (error: any) {
|
|
116
|
+
return { svgCount: 0, hasCoveModule: false, isDataBite: false, isDataTable: false, error: error.message }
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
async () => {},
|
|
120
|
+
(before, after) => {
|
|
121
|
+
return (after.svgCount > 0 && after.hasCoveModule) || after.isDataBite || after.isDataTable
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
} finally {
|
|
125
|
+
if (iframe.parentNode) {
|
|
126
|
+
document.body.removeChild(iframe)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const StoryRenderingTests: Story = {
|
|
132
|
+
play: async ({ canvasElement }) => {
|
|
133
|
+
const canvas = within(canvasElement)
|
|
134
|
+
expect(canvas.getByTestId('chart-rendering-validator')).toBeInTheDocument()
|
|
135
|
+
|
|
136
|
+
const storyUrls = await getVisualizationStoryUrls()
|
|
137
|
+
|
|
138
|
+
if (storyUrls.length === 0) {
|
|
139
|
+
console.warn('No visualization stories found to test')
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const results: { iframeUrl: string; storyUrl: string; success: boolean; error?: string }[] = []
|
|
144
|
+
|
|
145
|
+
for (const [i, iframeUrl] of storyUrls.entries()) {
|
|
146
|
+
const storyUrl = iframeUrlToStoryUrl(iframeUrl)
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await testIframeVisualization(iframeUrl)
|
|
150
|
+
results.push({ iframeUrl, storyUrl, success: true })
|
|
151
|
+
} catch (error: any) {
|
|
152
|
+
if (i > 0) {
|
|
153
|
+
results.push({ iframeUrl, storyUrl, success: false, error: error.message })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const failed = results.filter(r => !r.success).length
|
|
159
|
+
|
|
160
|
+
if (failed > 0) {
|
|
161
|
+
throw new Error(`${failed} out of ${storyUrls.length} visualization stories failed to render`)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 512 512" fill="currentColor">
|
|
2
|
+
<!-- Outlined/Regular style magnifying glass -->
|
|
3
|
+
<path d="M208 48a160 160 0 1 1 0 320 160 160 0 1 1 0-320zm0 368c48.8 0 93.7-16.8 129.1-44.9L471 505c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9L371.1 337.1C399.2 301.7 416 256.8 416 208C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208z"/>
|
|
4
|
+
</svg>
|
|
5
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
2
|
+
<rect x="32" y="64" width="32" height="384" fill="#000000"/>
|
|
3
|
+
<rect x="72" y="64" width="32" height="384" fill="#1a1a1a"/>
|
|
4
|
+
<rect x="112" y="64" width="32" height="384" fill="#333333"/>
|
|
5
|
+
<rect x="152" y="64" width="32" height="384" fill="#666666"/>
|
|
6
|
+
<rect x="192" y="64" width="32" height="384" fill="#999999"/>
|
|
7
|
+
<rect x="232" y="64" width="32" height="384" fill="#b3b3b3"/>
|
|
8
|
+
<rect x="272" y="64" width="32" height="384" fill="#cccccc"/>
|
|
9
|
+
<rect x="312" y="64" width="32" height="384" fill="#e6e6e6"/>
|
|
10
|
+
<rect x="352" y="64" width="32" height="384" fill="#f2f2f2"/>
|
|
11
|
+
<rect x="392" y="64" width="32" height="384" fill="#fafafa"/>
|
|
12
|
+
<rect x="432" y="64" width="32" height="384" fill="#ffffff"/>
|
|
13
|
+
</svg>
|
|
@@ -6,6 +6,7 @@ import { FilterFunction, JsonEditor, UpdateFunction } from 'json-edit-react'
|
|
|
6
6
|
import './advanced-editor-styles.css'
|
|
7
7
|
import _ from 'lodash'
|
|
8
8
|
import Tooltip from '../ui/Tooltip'
|
|
9
|
+
import EmbedEditor from './EmbedEditor'
|
|
9
10
|
|
|
10
11
|
export const AdvancedEditor = ({
|
|
11
12
|
loadConfig,
|
|
@@ -59,7 +60,9 @@ export const AdvancedEditor = ({
|
|
|
59
60
|
chart: ['Charts', 'https://www.cdc.gov/cove/index.html', <ChartIcon />],
|
|
60
61
|
dashboard: ['Dashboard', 'https://www.cdc.gov/cove/index.html', <ChartIcon />],
|
|
61
62
|
map: ['Maps', 'https://www.cdc.gov/cove/index.html', <MapIcon />],
|
|
62
|
-
'markup-include': ['Markup Include', 'https://www.cdc.gov/cove/index.html', <MarkupIncludeIcon />]
|
|
63
|
+
'markup-include': ['Markup Include', 'https://www.cdc.gov/cove/index.html', <MarkupIncludeIcon />],
|
|
64
|
+
'data-bite': ['Data Bite', 'https://www.cdc.gov/cove/index.html', <ChartIcon />],
|
|
65
|
+
'waffle-chart': ['Waffle Chart', 'https://www.cdc.gov/cove/index.html', <ChartIcon />]
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
if (!config.type) return <></>
|
|
@@ -113,6 +116,9 @@ export const AdvancedEditor = ({
|
|
|
113
116
|
</React.Fragment>
|
|
114
117
|
)}
|
|
115
118
|
</div>
|
|
119
|
+
|
|
120
|
+
{/* Share with Partners Section */}
|
|
121
|
+
<EmbedEditor config={config} />
|
|
116
122
|
</>
|
|
117
123
|
)
|
|
118
124
|
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react'
|
|
2
|
+
import { generateEmbedCode } from '../../helpers/embedCodeGenerator'
|
|
3
|
+
|
|
4
|
+
type EmbedEditorProps = {
|
|
5
|
+
config?: any // Current visualization config
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* EmbedEditor - Provides "Share with Partners" functionality
|
|
10
|
+
* Generates embed codes for iframe embedding of visualizations
|
|
11
|
+
*/
|
|
12
|
+
export const EmbedEditor: React.FC<EmbedEditorProps> = ({ config }) => {
|
|
13
|
+
const [configUrl, setConfigUrl] = useState<string | null>(null)
|
|
14
|
+
const [showEmbedModal, setShowEmbedModal] = useState(false)
|
|
15
|
+
const [embedCode, setEmbedCode] = useState('')
|
|
16
|
+
const [embedCodeCopied, setEmbedCodeCopied] = useState(false)
|
|
17
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
18
|
+
|
|
19
|
+
// Check if all filters have setByQueryParameter
|
|
20
|
+
const filtersAreValid = useMemo(() => {
|
|
21
|
+
if (!config) return true
|
|
22
|
+
|
|
23
|
+
// Check regular filters
|
|
24
|
+
const filters = config.filters || []
|
|
25
|
+
// Check dashboard shared filters
|
|
26
|
+
const sharedFilters = config.dashboard?.sharedFilters || []
|
|
27
|
+
|
|
28
|
+
const allFilters = [...filters, ...sharedFilters]
|
|
29
|
+
|
|
30
|
+
// If no filters, valid
|
|
31
|
+
if (allFilters.length === 0) return true
|
|
32
|
+
|
|
33
|
+
// All filters must have setByQueryParameter
|
|
34
|
+
return allFilters.every((filter: any) => !!filter.setByQueryParameter)
|
|
35
|
+
}, [config])
|
|
36
|
+
|
|
37
|
+
// Detect configUrl from WCMS permalink or use dev fallback
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
// Try to get config URL from WCMS permalink element
|
|
40
|
+
const permalinkElement = document.querySelector('#sample-permalink') as HTMLAnchorElement
|
|
41
|
+
|
|
42
|
+
if (permalinkElement?.href) {
|
|
43
|
+
try {
|
|
44
|
+
// Parse the URL and extract just the pathname (strip host)
|
|
45
|
+
const url = new URL(permalinkElement.href)
|
|
46
|
+
const pathname = url.pathname
|
|
47
|
+
setConfigUrl(pathname)
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.warn('[EmbedEditor] Failed to parse permalink URL:', err)
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Check if we're in development mode
|
|
53
|
+
const isDevelopment =
|
|
54
|
+
process.env.NODE_ENV === 'development' ||
|
|
55
|
+
window.location.hostname === 'localhost' ||
|
|
56
|
+
window.location.hostname === '127.0.0.1'
|
|
57
|
+
|
|
58
|
+
if (isDevelopment) {
|
|
59
|
+
// Use fallback only in development
|
|
60
|
+
const fallbackUrl = '/examples/line-chart-states.json'
|
|
61
|
+
setConfigUrl(fallbackUrl)
|
|
62
|
+
} else {
|
|
63
|
+
// In production without permalink, don't show embed section
|
|
64
|
+
console.warn('[EmbedEditor] No permalink found and not in development mode')
|
|
65
|
+
setConfigUrl(null)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
// Handle showing embed code modal
|
|
71
|
+
const handleShowEmbedCode = () => {
|
|
72
|
+
if (!configUrl) {
|
|
73
|
+
alert('This visualization must be published before generating embed code.')
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const code = generateEmbedCode({ configUrl })
|
|
78
|
+
setEmbedCode(code)
|
|
79
|
+
setShowEmbedModal(true)
|
|
80
|
+
setEmbedCodeCopied(false)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle copying embed code from modal
|
|
84
|
+
const handleCopyFromModal = async () => {
|
|
85
|
+
try {
|
|
86
|
+
await navigator.clipboard.writeText(embedCode)
|
|
87
|
+
setEmbedCodeCopied(true)
|
|
88
|
+
setTimeout(() => setEmbedCodeCopied(false), 3000)
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.error('Failed to copy embed code:', err)
|
|
91
|
+
alert('Failed to copy to clipboard. Please copy manually.')
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Handle closing modal
|
|
96
|
+
const handleCloseModal = () => {
|
|
97
|
+
setShowEmbedModal(false)
|
|
98
|
+
setEmbedCodeCopied(false)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Hide embed section until released
|
|
102
|
+
return null
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<>
|
|
106
|
+
{/* Collapsible Share with Partners Section */}
|
|
107
|
+
<div className='share-partners' style={{ padding: '0 1em 1em', textAlign: 'left' }}>
|
|
108
|
+
<span
|
|
109
|
+
className='advanced-toggle-link'
|
|
110
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
111
|
+
style={{ paddingTop: '1em', display: 'block', cursor: 'pointer', textDecoration: 'underline' }}
|
|
112
|
+
>
|
|
113
|
+
<span
|
|
114
|
+
style={{ textDecoration: 'none', display: 'inline-block', fontFamily: 'monospace', paddingRight: '5px' }}
|
|
115
|
+
>
|
|
116
|
+
{isExpanded ? `— ` : `+ `}
|
|
117
|
+
</span>
|
|
118
|
+
Share with Partners
|
|
119
|
+
</span>
|
|
120
|
+
|
|
121
|
+
{isExpanded && (
|
|
122
|
+
<div style={{ paddingTop: '1em' }}>
|
|
123
|
+
{!configUrl ? (
|
|
124
|
+
<div
|
|
125
|
+
style={{
|
|
126
|
+
padding: '0.75em',
|
|
127
|
+
background: '#fff3cd',
|
|
128
|
+
border: '1px solid #ffc107',
|
|
129
|
+
borderRadius: '4px',
|
|
130
|
+
marginBottom: '0.5em'
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
<p style={{ fontSize: '0.85em', margin: 0, color: '#856404' }}>
|
|
134
|
+
⚠️ An embed code cannot be generated until this visualization has been saved.
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
137
|
+
) : !filtersAreValid ? (
|
|
138
|
+
<div
|
|
139
|
+
style={{
|
|
140
|
+
padding: '0.75em',
|
|
141
|
+
background: '#fff3cd',
|
|
142
|
+
border: '1px solid #ffc107',
|
|
143
|
+
borderRadius: '4px',
|
|
144
|
+
marginBottom: '0.5em'
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
<p style={{ fontSize: '0.85em', margin: '0 0 0.5em 0', fontWeight: 'bold', color: '#856404' }}>
|
|
148
|
+
⚠️ Embed Code Not Available
|
|
149
|
+
</p>
|
|
150
|
+
<p style={{ fontSize: '0.85em', margin: 0, color: '#856404' }}>
|
|
151
|
+
To enable embedding, all filters must have the "Query String Parameter" field set. Some filters are
|
|
152
|
+
missing this field. After setting the field, make sure to save your visualization.
|
|
153
|
+
</p>
|
|
154
|
+
</div>
|
|
155
|
+
) : (
|
|
156
|
+
<>
|
|
157
|
+
<p style={{ fontSize: '0.85em', marginBottom: '1em', color: '#666' }}>
|
|
158
|
+
Generate embed codes for partners to add this visualization to their websites. Your visualization will
|
|
159
|
+
need to be published to Link (www.cdc.gov) before it can be embedded by a partner.
|
|
160
|
+
</p>
|
|
161
|
+
|
|
162
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5em' }}>
|
|
163
|
+
<button
|
|
164
|
+
className='btn btn-primary'
|
|
165
|
+
onClick={handleShowEmbedCode}
|
|
166
|
+
style={{ width: '100%', textAlign: 'left' }}
|
|
167
|
+
>
|
|
168
|
+
Get Embed Code
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{/* Embed Code Modal */}
|
|
178
|
+
{showEmbedModal && (
|
|
179
|
+
<div
|
|
180
|
+
className='modal-overlay'
|
|
181
|
+
style={{
|
|
182
|
+
position: 'fixed',
|
|
183
|
+
top: 0,
|
|
184
|
+
left: 0,
|
|
185
|
+
right: 0,
|
|
186
|
+
bottom: 0,
|
|
187
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
188
|
+
display: 'flex',
|
|
189
|
+
alignItems: 'center',
|
|
190
|
+
justifyContent: 'center',
|
|
191
|
+
zIndex: 9999
|
|
192
|
+
}}
|
|
193
|
+
onClick={handleCloseModal}
|
|
194
|
+
>
|
|
195
|
+
<div
|
|
196
|
+
className='modal-content'
|
|
197
|
+
style={{
|
|
198
|
+
backgroundColor: 'white',
|
|
199
|
+
borderRadius: '8px',
|
|
200
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
201
|
+
maxWidth: '600px',
|
|
202
|
+
width: '90%',
|
|
203
|
+
margin: '20px'
|
|
204
|
+
}}
|
|
205
|
+
onClick={e => e.stopPropagation()}
|
|
206
|
+
>
|
|
207
|
+
<div
|
|
208
|
+
className='modal-header'
|
|
209
|
+
style={{
|
|
210
|
+
padding: '15px 20px',
|
|
211
|
+
borderBottom: '1px solid #e0e0e0',
|
|
212
|
+
backgroundColor: '#005eaa',
|
|
213
|
+
display: 'flex',
|
|
214
|
+
justifyContent: 'space-between',
|
|
215
|
+
alignItems: 'center',
|
|
216
|
+
borderRadius: '8px 8px 0 0'
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
<h3 style={{ color: 'white', margin: 0 }}>Embed Code</h3>
|
|
220
|
+
<button
|
|
221
|
+
onClick={handleCloseModal}
|
|
222
|
+
style={{
|
|
223
|
+
background: 'transparent',
|
|
224
|
+
border: 'none',
|
|
225
|
+
color: 'white',
|
|
226
|
+
fontSize: '1.5em',
|
|
227
|
+
cursor: 'pointer',
|
|
228
|
+
padding: '0 5px',
|
|
229
|
+
lineHeight: 1
|
|
230
|
+
}}
|
|
231
|
+
aria-label='Close'
|
|
232
|
+
>
|
|
233
|
+
×
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div className='modal-body' style={{ padding: '20px' }}>
|
|
238
|
+
<p style={{ marginBottom: '10px', color: '#666' }}>Copy this code and paste it into your website:</p>
|
|
239
|
+
<textarea
|
|
240
|
+
readOnly
|
|
241
|
+
value={embedCode}
|
|
242
|
+
style={{
|
|
243
|
+
width: '100%',
|
|
244
|
+
height: '180px',
|
|
245
|
+
fontFamily: 'monospace',
|
|
246
|
+
fontSize: '0.85em',
|
|
247
|
+
padding: '10px',
|
|
248
|
+
border: '1px solid #ddd',
|
|
249
|
+
borderRadius: '4px',
|
|
250
|
+
resize: 'vertical',
|
|
251
|
+
boxSizing: 'border-box'
|
|
252
|
+
}}
|
|
253
|
+
onFocus={e => e.target.select()}
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<div
|
|
258
|
+
className='modal-footer'
|
|
259
|
+
style={{
|
|
260
|
+
padding: '15px 20px',
|
|
261
|
+
borderTop: '1px solid #e0e0e0',
|
|
262
|
+
display: 'flex',
|
|
263
|
+
justifyContent: 'flex-end',
|
|
264
|
+
gap: '10px'
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
<button className='btn btn-secondary' onClick={handleCloseModal}>
|
|
268
|
+
Close
|
|
269
|
+
</button>
|
|
270
|
+
<button className='btn btn-primary' onClick={handleCopyFromModal} style={{ minWidth: '120px' }}>
|
|
271
|
+
{embedCodeCopied ? '✓ Copied!' : 'Copy to Clipboard'}
|
|
272
|
+
</button>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
</>
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export default EmbedEditor
|