Initial
This commit is contained in:
254
components/LCPDataSelector.js
Normal file
254
components/LCPDataSelector.js
Normal file
@ -0,0 +1,254 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
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' },
|
||||
];
|
||||
|
||||
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 === '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;
|
||||
49
components/LCPDimensionControl.js
Normal file
49
components/LCPDimensionControl.js
Normal file
@ -0,0 +1,49 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { SelectControl, TextControl } from '@wordpress/components';
|
||||
|
||||
const LCPDimensionControl = ({ label, value, onChange }) => {
|
||||
// Parse the current value into number and unit
|
||||
const parseValue = (val) => {
|
||||
const match = val?.match(/^(\d+)(.*)$/);
|
||||
return match ? {
|
||||
number: parseInt(match[1], 10),
|
||||
unit: match[2] || 'px'
|
||||
} : { number: 300, unit: 'px' };
|
||||
};
|
||||
|
||||
const { number, unit } = parseValue(value);
|
||||
|
||||
const units = [
|
||||
{ label: 'Pixels (px)', value: 'px' },
|
||||
{ label: 'Percentage (%)', value: '%' },
|
||||
{ label: 'Viewport Width (vw)', value: 'vw' },
|
||||
{ label: 'Viewport Height (vh)', value: 'vh' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="lcp-dimension-control">
|
||||
<label className="components-base-control__label">{label}</label>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start' }}>
|
||||
<TextControl
|
||||
type="number"
|
||||
value={number}
|
||||
onChange={(newNumber) => {
|
||||
onChange(`${newNumber}${unit}`);
|
||||
}}
|
||||
min={0}
|
||||
style={{ width: '80px' }}
|
||||
/>
|
||||
<SelectControl
|
||||
value={unit}
|
||||
options={units}
|
||||
onChange={(newUnit) => {
|
||||
onChange(`${number}${newUnit}`);
|
||||
}}
|
||||
style={{ minWidth: '100px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LCPDimensionControl;
|
||||
6
components/package-lock.json
generated
Normal file
6
components/package-lock.json
generated
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "components",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
Reference in New Issue
Block a user