Files
lcp-visualizer/components/LCPDataSelector.js
2025-01-15 23:53:26 -08:00

266 lines
9.1 KiB
JavaScript

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { SelectControl, TextareaControl, Button, TextControl } from '@wordpress/components';
import { useState, useEffect } from '@wordpress/element';
import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import LCPDatasetBuilder from './LCPDatasetBuilder';
const DEFAULT_COLORS = [
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
'#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0', '#FF9F40'
];
/**
* LCPDataSelector Component
*
* @param {Object} props Component properties
* @param {Object} props.value Current data value
* @param {Function} props.onChange Callback function when data changes
* @param {string} props.dataSource Current data source type
* @param {Function} props.onDataSourceChange Callback function when data source changes
*/
const LCPDataSelector = ({ value, onChange, dataSource, onDataSourceChange }) => {
const [csvUrl, setCsvUrl] = useState('');
const [jsonData, setJsonData] = useState('');
const [error, setError] = useState('');
// Initialize component state with existing data
useEffect(() => {
if (value && Object.keys(value).length > 0) {
if (dataSource === 'manual_json') {
setJsonData(JSON.stringify(value, null, 2));
} else if (dataSource === 'csv_url') {
const storedUrl = value[Object.keys(value)[0]]?.[0]?.sourceUrl;
if (storedUrl) {
setCsvUrl(storedUrl);
}
}
}
}, []);
const dataSourceOptions = [
{ label: __('Manual JSON', 'lcp'), value: 'manual_json' },
{ label: __('CSV Upload', 'lcp'), value: 'csv_upload' },
{ label: __('CSV URL', 'lcp'), value: 'csv_url' },
{ label: __('Dataset Builder', 'lcp'), value: 'dataset_builder' },
];
const validateJsonData = (jsonString) => {
try {
const parsed = JSON.parse(jsonString);
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('Data must be an object with dataset names as keys');
}
// Validate each dataset
Object.entries(parsed).forEach(([datasetName, dataset]) => {
if (!Array.isArray(dataset)) {
throw new Error(`Dataset "${datasetName}" must be an array`);
}
dataset.forEach((item, index) => {
if (!item.label || !item.value) {
throw new Error(`Item at index ${index} in dataset "${datasetName}" is missing required fields (label or value)`);
}
// Add default color if not provided
if (!item.color) {
item.color = DEFAULT_COLORS[index % DEFAULT_COLORS.length];
}
});
});
setError('');
return parsed;
} catch (e) {
setError(e.message);
return null;
}
};
const handleJsonChange = (newJsonData) => {
setJsonData(newJsonData);
const validData = validateJsonData(newJsonData);
if (validData) {
onChange(validData);
}
};
const handleCsvUpload = (media) => {
const sourceUrl = media.url;
fetch(media.url)
.then(response => response.text())
.then(csvText => {
const data = parseCsvToJson(csvText, sourceUrl);
if (data) {
onChange(data);
}
})
.catch(err => {
setError('Error reading CSV file: ' + err.message);
});
};
const handleCsvUrlChange = (url) => {
setCsvUrl(url);
if (url) {
fetch(url)
.then(response => response.text())
.then(csvText => {
const data = parseCsvToJson(csvText, url);
if (data) {
onChange(data);
}
})
.catch(err => {
setError('Error fetching CSV file: ' + err.message);
});
}
};
const parseCsvToJson = (csvText, sourceUrl = '') => {
try {
const lines = csvText.split('\n');
if (lines.length < 2) {
throw new Error('CSV must have at least a header row and one data row');
}
const headers = lines[0].split(',').map(h => h.trim());
if (!headers.includes('label') || !headers.includes('value')) {
throw new Error('CSV must have "label" and "value" columns');
}
// Create a single dataset from CSV
const dataset = lines.slice(1)
.filter(line => line.trim())
.map((line, index) => {
const values = line.split(',').map(v => v.trim());
const item = {};
headers.forEach((header, i) => {
if (header === 'color' && !values[i]) {
item[header] = DEFAULT_COLORS[index % DEFAULT_COLORS.length];
} else {
item[header] = values[i];
}
});
if (sourceUrl) {
item.sourceUrl = sourceUrl;
}
return item;
});
// Create the new data structure
const data = {
'Dataset 1': dataset
};
setError('');
return data;
} catch (e) {
setError('Error parsing CSV: ' + e.message);
return null;
}
};
return (
<div className="lcp-data-selector">
<SelectControl
label={__('Data Source', 'lcp')}
value={dataSource}
options={dataSourceOptions}
onChange={onDataSourceChange}
/>
{dataSource === 'dataset_builder' && (
<LCPDatasetBuilder
value={value}
onChange={(newValue) => {
onChange(newValue);
}}
/>
)}
{dataSource === 'manual_json' && (
<TextareaControl
label={__('JSON Data', 'lcp')}
help={__('Enter datasets with arrays containing objects with label and value properties. Color is optional.', 'lcp')}
value={jsonData}
onChange={handleJsonChange}
placeholder={`{
"Dataset 1": [
{
"label": "Label 1",
"value": 100,
"color": "red"
},
{
"label": "Label 2",
"value": 200
}
],
"Dataset 2": [
{
"label": "Label 3",
"value": 300,
"color": "green"
}
]
}`}
/>
)}
{dataSource === 'csv_upload' && (
<MediaUploadCheck>
<MediaUpload
onSelect={handleCsvUpload}
allowedTypes={['text/csv']}
render={({ open }) => (
<div>
<Button onClick={open} isPrimary>
{__('Upload CSV File', 'lcp')}
</Button>
{value && Object.keys(value).length > 0 && value[Object.keys(value)[0]]?.[0]?.sourceUrl && (
<div className="current-file" style={{ marginTop: '10px' }}>
{__('Current file:', 'lcp')} {value[Object.keys(value)[0]][0].sourceUrl.split('/').pop()}
</div>
)}
</div>
)}
/>
</MediaUploadCheck>
)}
{dataSource === 'csv_url' && (
<TextControl
label={__('CSV URL', 'lcp')}
value={csvUrl}
onChange={handleCsvUrlChange}
help={__('Enter URL of a CSV file with label and value columns. Color column is optional.', 'lcp')}
/>
)}
{error && (
<div className="lcp-data-selector-error" style={{ color: 'red', marginTop: '10px' }}>
{error}
</div>
)}
{value && Object.keys(value).length > 0 && (
<div className="data-preview" style={{ marginTop: '15px' }}>
<h4>{__('Current Data Preview:', 'lcp')}</h4>
<div style={{ fontSize: '12px', color: '#666' }}>
{Object.entries(value).map(([datasetName, dataset]) => (
<div key={datasetName}>
{datasetName}: {dataset.length} {__('data points', 'lcp')}
</div>
))}
</div>
</div>
)}
</div>
);
};
export default LCPDataSelector;