Initial
This commit is contained in:
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
# Ignore node_modules
|
||||
node_modules/
|
||||
|
||||
**/node_modules/
|
||||
# Ignore logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Ignore build files
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Ignore .env files (for sensitive data like API keys)
|
||||
.env
|
||||
18
blocks/bar-graph/.editorconfig
Normal file
18
blocks/bar-graph/.editorconfig
Normal file
@ -0,0 +1,18 @@
|
||||
# This file is for unifying the coding style for different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
# WordPress Coding Standards
|
||||
# https://make.wordpress.org/core/handbook/coding-standards/
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = tab
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
30
blocks/bar-graph/.gitignore
vendored
Normal file
30
blocks/bar-graph/.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Output of `npm pack`
|
||||
*.tgz
|
||||
|
||||
# Output of `wp-scripts plugin-zip`
|
||||
*.zip
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
74
blocks/bar-graph/lcp-bar-graph.php
Normal file
74
blocks/bar-graph/lcp-bar-graph.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/**
|
||||
* Bar Graph Block Registration
|
||||
*
|
||||
* @package LCP-Data-Blocks
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the block using the metadata loaded from the `block.json` file.
|
||||
*/
|
||||
function lcp_bar_graph_block_init() {
|
||||
// Register the block using the metadata loaded from the `block.json` file.
|
||||
register_block_type(
|
||||
__DIR__ . '/build',
|
||||
array(
|
||||
'render_callback' => 'lcp_render_bar_graph_block'
|
||||
)
|
||||
);
|
||||
}
|
||||
add_action('init', 'lcp_bar_graph_block_init');
|
||||
|
||||
/**
|
||||
* Register D3.js as a dependency
|
||||
*/
|
||||
function lcp_register_d3() {
|
||||
wp_register_script(
|
||||
'd3',
|
||||
'https://d3js.org/d3.v7.min.js',
|
||||
array(),
|
||||
'7.0.0',
|
||||
true
|
||||
);
|
||||
}
|
||||
add_action('wp_enqueue_scripts', 'lcp_register_d3');
|
||||
add_action('enqueue_block_editor_assets', 'lcp_register_d3');
|
||||
|
||||
/**
|
||||
* Renders the block on the server.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
* @param string $content Block default content.
|
||||
* @return string Returns the block content.
|
||||
*/
|
||||
function lcp_render_bar_graph_block($attributes, $content) {
|
||||
if (!is_array($attributes)) {
|
||||
$attributes = array();
|
||||
}
|
||||
|
||||
// Get attributes with defaults
|
||||
$chart_height = $attributes['chartHeight'] ?? '400px';
|
||||
$background_color = $attributes['backgroundColor'] ?? '#ffffff';
|
||||
$bar_color = $attributes['barColor'] ?? '#0073aa';
|
||||
$bar_opacity = $attributes['barOpacity'] ?? 1;
|
||||
$show_grid_y = $attributes['showGridY'] ?? true;
|
||||
$grid_color = $attributes['gridColor'] ?? '#e0e0e0';
|
||||
|
||||
// Enqueue D3.js
|
||||
wp_enqueue_script('d3');
|
||||
|
||||
// Return the block's container
|
||||
return sprintf(
|
||||
'<div class="wp-block-lcp-bar-graph"><div class="lcp-bar-graph" style="height: %s; background-color: %s;" data-bar-color="%s" data-bar-opacity="%f" data-show-grid="%s" data-grid-color="%s"></div></div>',
|
||||
esc_attr($chart_height),
|
||||
esc_attr($background_color),
|
||||
esc_attr($bar_color),
|
||||
esc_attr($bar_opacity),
|
||||
esc_attr($show_grid_y ? 'true' : 'false'),
|
||||
esc_attr($grid_color)
|
||||
);
|
||||
}
|
||||
20608
blocks/bar-graph/package-lock.json
generated
Normal file
20608
blocks/bar-graph/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
blocks/bar-graph/package.json
Normal file
23
blocks/bar-graph/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "lcp-bar-graph",
|
||||
"version": "0.1.0",
|
||||
"description": "Bar Graph block for LCP Data Blocks plugin",
|
||||
"author": "CoosGuide",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"main": "build/index.js",
|
||||
"scripts": {
|
||||
"build": "wp-scripts build",
|
||||
"format": "wp-scripts format",
|
||||
"lint:css": "wp-scripts lint-style",
|
||||
"lint:js": "wp-scripts lint-js",
|
||||
"packages-update": "wp-scripts packages-update",
|
||||
"plugin-zip": "wp-scripts plugin-zip",
|
||||
"start": "wp-scripts start"
|
||||
},
|
||||
"dependencies": {
|
||||
"d3": "^7.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wordpress/scripts": "^30.8.1"
|
||||
}
|
||||
}
|
||||
55
blocks/bar-graph/readme.txt
Normal file
55
blocks/bar-graph/readme.txt
Normal file
@ -0,0 +1,55 @@
|
||||
=== Todo List ===
|
||||
Contributors: The WordPress Contributors
|
||||
Tags: block
|
||||
Tested up to: 6.7
|
||||
Stable tag: 0.1.0
|
||||
License: GPL-2.0-or-later
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
Example block scaffolded with Create Block tool.
|
||||
|
||||
== Description ==
|
||||
|
||||
This is the long description. No limit, and you can use Markdown (as well as in the following sections).
|
||||
|
||||
For backwards compatibility, if this section is missing, the full length of the short description will be used, and
|
||||
Markdown parsed.
|
||||
|
||||
== Installation ==
|
||||
|
||||
This section describes how to install the plugin and get it working.
|
||||
|
||||
e.g.
|
||||
|
||||
1. Upload the plugin files to the `/wp-content/plugins/todo-list` directory, or install the plugin through the WordPress plugins screen directly.
|
||||
1. Activate the plugin through the 'Plugins' screen in WordPress
|
||||
|
||||
|
||||
== Frequently Asked Questions ==
|
||||
|
||||
= A question that someone might have =
|
||||
|
||||
An answer to that question.
|
||||
|
||||
= What about foo bar? =
|
||||
|
||||
Answer to foo bar dilemma.
|
||||
|
||||
== Screenshots ==
|
||||
|
||||
1. This screen shot description corresponds to screenshot-1.(png|jpg|jpeg|gif). Note that the screenshot is taken from
|
||||
the /assets directory or the directory that contains the stable readme.txt (tags or trunk). Screenshots in the /assets
|
||||
directory take precedence. For example, `/assets/screenshot-1.png` would win over `/tags/4.3/screenshot-1.png`
|
||||
(or jpg, jpeg, gif).
|
||||
2. This is the second screen shot
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 0.1.0 =
|
||||
* Release
|
||||
|
||||
== Arbitrary section ==
|
||||
|
||||
You may provide arbitrary sections, in the same format as the ones above. This may be of use for extremely complicated
|
||||
plugins where more information needs to be conveyed that doesn't fit into the categories of "description" or
|
||||
"installation." Arbitrary sections will be shown below the built-in sections outlined above.
|
||||
104
blocks/bar-graph/src/block.json
Normal file
104
blocks/bar-graph/src/block.json
Normal file
@ -0,0 +1,104 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2,
|
||||
"name": "lcp/bar-graph",
|
||||
"version": "0.1.0",
|
||||
"title": "LCP Bar Graph",
|
||||
"category": "widgets",
|
||||
"icon": "chart-bar",
|
||||
"description": "Display data in a bar graph format.",
|
||||
"supports": {
|
||||
"html": false
|
||||
},
|
||||
"textdomain": "lcp",
|
||||
"editorScript": "file:./index.js",
|
||||
"editorStyle": "file:./index.css",
|
||||
"style": "file:./style-index.css",
|
||||
"viewScript": "file:./view.js",
|
||||
"attributes": {
|
||||
"showBarValues": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"chartHeight": {
|
||||
"type": "string",
|
||||
"default": "400px"
|
||||
},
|
||||
"chartWidth": {
|
||||
"type": "string",
|
||||
"default": "100%"
|
||||
},
|
||||
"chartTitle": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"displayChartTitle": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"allowDownload": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"downloadMaxWidth": {
|
||||
"type": "string",
|
||||
"default": "2000px"
|
||||
},
|
||||
"showSorting": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"showFiltering": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"chartData": {
|
||||
"type": "object",
|
||||
"default": {}
|
||||
},
|
||||
"dataSource": {
|
||||
"type": "string",
|
||||
"default": "manual_json"
|
||||
},
|
||||
"barColor": {
|
||||
"type": "string",
|
||||
"default": "#0073aa"
|
||||
},
|
||||
"barOpacity": {
|
||||
"type": "number",
|
||||
"default": 1
|
||||
},
|
||||
"backgroundColor": {
|
||||
"type": "string",
|
||||
"default": "#ffffff"
|
||||
},
|
||||
"showGridX": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"showGridY": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"gridColor": {
|
||||
"type": "string",
|
||||
"default": "#e0e0e0"
|
||||
},
|
||||
"gridWidth": {
|
||||
"type": "number",
|
||||
"default": 1
|
||||
},
|
||||
"xAxisLabel": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"yAxisLabel": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"gridOpacity": {
|
||||
"type": "number",
|
||||
"default": 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
184
blocks/bar-graph/src/components/BarGraph.js
Normal file
184
blocks/bar-graph/src/components/BarGraph.js
Normal file
@ -0,0 +1,184 @@
|
||||
import { useEffect, useRef } from '@wordpress/element';
|
||||
import * as d3 from 'd3';
|
||||
|
||||
const BarGraph = ({
|
||||
data,
|
||||
width = '100%',
|
||||
height = '400px',
|
||||
backgroundColor,
|
||||
title,
|
||||
showGridX,
|
||||
showGridY,
|
||||
gridColor,
|
||||
gridWidth,
|
||||
showBarValues,
|
||||
xAxisLabel,
|
||||
yAxisLabel
|
||||
}) => {
|
||||
const svgRef = useRef();
|
||||
const containerRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || !svgRef.current || !containerRef.current) return;
|
||||
|
||||
// Clear previous content
|
||||
d3.select(svgRef.current).selectAll("*").remove();
|
||||
|
||||
// Convert data to simple array
|
||||
const chartData = [];
|
||||
for (const [dataset, items] of Object.entries(data)) {
|
||||
for (const item of items) {
|
||||
chartData.push({
|
||||
dataset,
|
||||
label: item.label,
|
||||
value: parseInt(item.value, 10),
|
||||
color: item.color || '#FF6384'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get container dimensions
|
||||
const containerWidth = containerRef.current.clientWidth;
|
||||
const containerHeight = containerRef.current.clientHeight;
|
||||
|
||||
// Basic dimensions with extra margin for axis labels
|
||||
const margin = {
|
||||
top: title ? 40 : 20,
|
||||
right: 20,
|
||||
bottom: xAxisLabel ? 50 : 30,
|
||||
left: yAxisLabel ? 60 : 40
|
||||
};
|
||||
const innerWidth = containerWidth - margin.left - margin.right;
|
||||
const innerHeight = containerHeight - margin.top - margin.bottom;
|
||||
|
||||
// Create SVG
|
||||
const svg = d3.select(svgRef.current)
|
||||
.attr('width', containerWidth)
|
||||
.attr('height', containerHeight);
|
||||
|
||||
// Add title if present
|
||||
if (title) {
|
||||
svg.append('text')
|
||||
.attr('x', containerWidth / 2)
|
||||
.attr('y', 20)
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('font-size', '16px')
|
||||
.text(title);
|
||||
}
|
||||
|
||||
// Create main group
|
||||
const g = svg.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
// Create scales
|
||||
const xScale = d3.scaleBand()
|
||||
.domain(chartData.map(d => d.label))
|
||||
.range([0, innerWidth])
|
||||
.padding(0.1);
|
||||
|
||||
const yScale = d3.scaleLinear()
|
||||
.domain([0, d3.max(chartData, d => d.value)])
|
||||
.range([innerHeight, 0]);
|
||||
|
||||
// Add X grid
|
||||
if (showGridX) {
|
||||
g.append('g')
|
||||
.attr('class', 'grid x-grid')
|
||||
.attr('transform', `translate(0,${innerHeight})`)
|
||||
.call(d3.axisBottom(xScale)
|
||||
.tickSize(-innerHeight)
|
||||
.tickFormat(''))
|
||||
.call(g => g.selectAll('.tick line')
|
||||
.attr('stroke', gridColor)
|
||||
.attr('stroke-width', gridWidth)
|
||||
.attr('opacity', 0.5));
|
||||
}
|
||||
|
||||
// Add Y grid
|
||||
if (showGridY) {
|
||||
g.append('g')
|
||||
.attr('class', 'grid y-grid')
|
||||
.call(d3.axisLeft(yScale)
|
||||
.tickSize(-innerWidth)
|
||||
.tickFormat(''))
|
||||
.call(g => g.selectAll('.tick line')
|
||||
.attr('stroke', gridColor)
|
||||
.attr('stroke-width', gridWidth)
|
||||
.attr('opacity', 0.5));
|
||||
}
|
||||
|
||||
// Add bars
|
||||
const bars = g.selectAll('rect')
|
||||
.data(chartData)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('x', d => xScale(d.label))
|
||||
.attr('y', d => yScale(d.value))
|
||||
.attr('width', xScale.bandwidth())
|
||||
.attr('height', d => innerHeight - yScale(d.value))
|
||||
.attr('fill', d => d.color);
|
||||
|
||||
// Add bar values
|
||||
if (showBarValues) {
|
||||
g.selectAll('.bar-value')
|
||||
.data(chartData)
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('class', 'bar-value')
|
||||
.attr('x', d => xScale(d.label) + xScale.bandwidth() / 2)
|
||||
.attr('y', d => yScale(d.value) - 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.text(d => d.value);
|
||||
}
|
||||
|
||||
// Add X axis
|
||||
g.append('g')
|
||||
.attr('transform', `translate(0,${innerHeight})`)
|
||||
.call(d3.axisBottom(xScale));
|
||||
|
||||
// Add Y axis
|
||||
g.append('g')
|
||||
.call(d3.axisLeft(yScale));
|
||||
|
||||
// Add X axis label
|
||||
if (xAxisLabel) {
|
||||
g.append('text')
|
||||
.attr('class', 'x-axis-label')
|
||||
.attr('x', innerWidth / 2)
|
||||
.attr('y', innerHeight + 40)
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('font-size', '12px')
|
||||
.text(xAxisLabel);
|
||||
}
|
||||
|
||||
// Add Y axis label
|
||||
if (yAxisLabel) {
|
||||
g.append('text')
|
||||
.attr('class', 'y-axis-label')
|
||||
.attr('transform', 'rotate(-90)')
|
||||
.attr('x', -innerHeight / 2)
|
||||
.attr('y', -45)
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('font-size', '12px')
|
||||
.text(yAxisLabel);
|
||||
}
|
||||
|
||||
}, [data, width, height, title, showGridX, showGridY, gridColor, gridWidth, showBarValues, xAxisLabel, yAxisLabel]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="bar-graph"
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
backgroundColor,
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<svg ref={svgRef}></svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BarGraph;
|
||||
341
blocks/bar-graph/src/edit.js
Normal file
341
blocks/bar-graph/src/edit.js
Normal file
@ -0,0 +1,341 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
|
||||
import {
|
||||
PanelBody,
|
||||
Panel,
|
||||
ColorPicker,
|
||||
RangeControl,
|
||||
ToggleControl,
|
||||
TabPanel,
|
||||
TextControl,
|
||||
Button
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './editor.scss';
|
||||
import LCPDataSelector from '../../../components/LCPDataSelector';
|
||||
import BarGraph from './components/BarGraph';
|
||||
import LCPDimensionControl from '../../../components/LCPDimensionControl';
|
||||
|
||||
export default function Edit({ attributes, setAttributes }) {
|
||||
const {
|
||||
chartHeight,
|
||||
chartWidth,
|
||||
barColor,
|
||||
barOpacity,
|
||||
backgroundColor,
|
||||
showGridY,
|
||||
gridColor,
|
||||
chartData,
|
||||
dataSource,
|
||||
displayChartTitle,
|
||||
chartTitle,
|
||||
allowDownload,
|
||||
downloadMaxWidth,
|
||||
showSorting,
|
||||
showFiltering,
|
||||
showBarValues,
|
||||
showGridX,
|
||||
gridWidth,
|
||||
xAxisLabel,
|
||||
yAxisLabel
|
||||
} = attributes;
|
||||
|
||||
const blockProps = useBlockProps();
|
||||
|
||||
const handleDataChange = (newData) => {
|
||||
console.log('New data:', newData); // Debug log
|
||||
setAttributes({ chartData: newData });
|
||||
};
|
||||
|
||||
const handleDataSourceChange = (newSource) => {
|
||||
setAttributes({
|
||||
dataSource: newSource,
|
||||
chartData: {} // Reset data when source changes
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
// Get the SVG element
|
||||
const svg = document.querySelector('.bar-graph svg');
|
||||
if (!svg) return;
|
||||
|
||||
// Create a canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
||||
const DOMURL = window.URL || window.webkitURL || window;
|
||||
const url = DOMURL.createObjectURL(svgBlob);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
// Set canvas dimensions
|
||||
canvas.width = svg.width.baseVal.value;
|
||||
canvas.height = svg.height.baseVal.value;
|
||||
|
||||
// Draw background
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = backgroundColor;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw the image
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// Convert to blob and download
|
||||
canvas.toBlob((blob) => {
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = 'bar-graph.png';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
DOMURL.revokeObjectURL(url);
|
||||
}, 'image/png');
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
};
|
||||
|
||||
// Debug log to check chartData
|
||||
console.log('Chart data:', chartData);
|
||||
|
||||
return (
|
||||
<div {...blockProps}>
|
||||
<InspectorControls>
|
||||
<TabPanel
|
||||
className="lcp-tab-panel"
|
||||
activeClass="active-tab"
|
||||
tabs={[
|
||||
{
|
||||
name: 'data',
|
||||
title: __('Data Settings', 'lcp'),
|
||||
className: 'tab-data',
|
||||
},
|
||||
{
|
||||
name: 'appearance',
|
||||
title: __('Appearance', 'lcp'),
|
||||
className: 'tab-appearance',
|
||||
},
|
||||
{
|
||||
name: 'tools',
|
||||
title: __('Tools Settings', 'lcp'),
|
||||
className: 'tab-tools',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(tab) => {
|
||||
if (tab.name === 'data') {
|
||||
return (
|
||||
<Panel>
|
||||
<PanelBody title="Data Settings">
|
||||
<LCPDataSelector
|
||||
value={chartData}
|
||||
onChange={handleDataChange}
|
||||
dataSource={dataSource}
|
||||
onDataSourceChange={handleDataSourceChange}
|
||||
/>
|
||||
</PanelBody>
|
||||
<PanelBody title="Chart Settings">
|
||||
<ToggleControl
|
||||
label={__('Display Chart Title', 'lcp')}
|
||||
checked={displayChartTitle}
|
||||
onChange={(value) => setAttributes({ displayChartTitle: value })}
|
||||
/>
|
||||
{displayChartTitle && (
|
||||
<TextControl
|
||||
label={__('Chart Title', 'lcp')}
|
||||
value={chartTitle}
|
||||
onChange={(value) => setAttributes({ chartTitle: value })}
|
||||
/>
|
||||
)}
|
||||
<LCPDimensionControl
|
||||
label={__('Chart Width', 'lcp')}
|
||||
value={chartWidth}
|
||||
onChange={(value) => setAttributes({ chartWidth: value })}
|
||||
/>
|
||||
<LCPDimensionControl
|
||||
label={__('Chart Height', 'lcp')}
|
||||
value={chartHeight}
|
||||
onChange={(value) => setAttributes({ chartHeight: value })}
|
||||
/>
|
||||
</PanelBody>
|
||||
<PanelBody title="Appearance">
|
||||
<div className="components-base-control">
|
||||
<label className="components-base-control__label">
|
||||
{__('Bar Color', 'lcp')}
|
||||
</label>
|
||||
<ColorPicker
|
||||
color={barColor}
|
||||
onChangeComplete={(color) => setAttributes({ barColor: color.hex })}
|
||||
/>
|
||||
</div>
|
||||
<RangeControl
|
||||
label={__('Bar Opacity', 'lcp')}
|
||||
value={barOpacity}
|
||||
onChange={(value) => setAttributes({ barOpacity: value })}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.1}
|
||||
/>
|
||||
<div className="components-base-control">
|
||||
<label className="components-base-control__label">
|
||||
{__('Background Color', 'lcp')}
|
||||
</label>
|
||||
<ColorPicker
|
||||
color={backgroundColor}
|
||||
onChangeComplete={(color) => setAttributes({ backgroundColor: color.hex })}
|
||||
/>
|
||||
</div>
|
||||
<ToggleControl
|
||||
label={__('Show Bar Values', 'lcp')}
|
||||
checked={showBarValues}
|
||||
onChange={(value) => setAttributes({ showBarValues: value })}
|
||||
/>
|
||||
</PanelBody>
|
||||
<PanelBody title="Grid Settings">
|
||||
<TextControl
|
||||
label={__('X-Axis Label', 'lcp')}
|
||||
value={xAxisLabel}
|
||||
onChange={(value) => setAttributes({ xAxisLabel: value })}
|
||||
/>
|
||||
<TextControl
|
||||
label={__('Y-Axis Label', 'lcp')}
|
||||
value={yAxisLabel}
|
||||
onChange={(value) => setAttributes({ yAxisLabel: value })}
|
||||
/>
|
||||
<ToggleControl
|
||||
label={__('Show X-Axis Grid', 'lcp')}
|
||||
checked={showGridX}
|
||||
onChange={(value) => setAttributes({ showGridX: value })}
|
||||
/>
|
||||
<ToggleControl
|
||||
label={__('Show Y-Axis Grid', 'lcp')}
|
||||
checked={showGridY}
|
||||
onChange={(value) => setAttributes({ showGridY: value })}
|
||||
/>
|
||||
{(showGridX || showGridY) && (
|
||||
<>
|
||||
<div className="components-base-control">
|
||||
<label className="components-base-control__label">
|
||||
{__('Grid Color', 'lcp')}
|
||||
</label>
|
||||
<ColorPicker
|
||||
color={gridColor}
|
||||
onChangeComplete={(color) => setAttributes({ gridColor: color.hex })}
|
||||
/>
|
||||
</div>
|
||||
<RangeControl
|
||||
label={__('Grid Width', 'lcp')}
|
||||
value={gridWidth}
|
||||
onChange={(value) => setAttributes({ gridWidth: value })}
|
||||
min={1}
|
||||
max={5}
|
||||
step={1}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</PanelBody>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
if (tab.name === 'appearance') {
|
||||
return (
|
||||
<PanelBody>
|
||||
|
||||
</PanelBody>
|
||||
);
|
||||
}
|
||||
if (tab.name === 'tools') {
|
||||
return (
|
||||
<PanelBody>
|
||||
<ToggleControl
|
||||
label={__('Allow Download', 'lcp')}
|
||||
checked={allowDownload}
|
||||
onChange={(value) => setAttributes({ allowDownload: value })}
|
||||
/>
|
||||
{allowDownload && (
|
||||
<TextControl
|
||||
type="number"
|
||||
label={__('Max Download Width (px)', 'lcp')}
|
||||
value={parseInt(downloadMaxWidth)}
|
||||
onChange={(value) => setAttributes({ downloadMaxWidth: `${value}px` })}
|
||||
min={100}
|
||||
max={5000}
|
||||
/>
|
||||
)}
|
||||
<ToggleControl
|
||||
label={__('Show Sorting Options', 'lcp')}
|
||||
checked={showSorting}
|
||||
onChange={(value) => setAttributes({ showSorting: value })}
|
||||
/>
|
||||
<ToggleControl
|
||||
label={__('Show Filtering Options', 'lcp')}
|
||||
checked={showFiltering}
|
||||
onChange={(value) => setAttributes({ showFiltering: value })}
|
||||
/>
|
||||
</PanelBody>
|
||||
);
|
||||
}
|
||||
}}
|
||||
</TabPanel>
|
||||
</InspectorControls>
|
||||
|
||||
{allowDownload && (
|
||||
<div className="lcp-bar-graph-toolbar">
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={handleDownload}
|
||||
icon="download"
|
||||
>
|
||||
{__('Download Graph', 'lcp')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{chartData && Object.keys(chartData).length > 0 ? (
|
||||
<BarGraph
|
||||
data={chartData}
|
||||
height={chartHeight}
|
||||
width={chartWidth}
|
||||
backgroundColor={backgroundColor}
|
||||
defaultBarColor={barColor}
|
||||
barOpacity={barOpacity}
|
||||
showGridX={showGridX}
|
||||
showGridY={showGridY}
|
||||
gridColor={gridColor}
|
||||
gridWidth={gridWidth}
|
||||
title={displayChartTitle ? chartTitle : ''}
|
||||
showSorting={showSorting}
|
||||
showFiltering={showFiltering}
|
||||
showBarValues={showBarValues}
|
||||
xAxisLabel={xAxisLabel}
|
||||
yAxisLabel={yAxisLabel}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="lcp-bar-graph-placeholder"
|
||||
style={{
|
||||
height: chartHeight,
|
||||
width: chartWidth,
|
||||
backgroundColor: backgroundColor,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '2px dashed #ccc',
|
||||
borderRadius: '4px',
|
||||
color: '#666',
|
||||
}}
|
||||
>
|
||||
{__('Please add data using the Data Settings panel', 'lcp')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
68
blocks/bar-graph/src/editor.scss
Normal file
68
blocks/bar-graph/src/editor.scss
Normal file
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* The following styles get applied inside the editor only.
|
||||
*
|
||||
* Replace them with your own styles or remove the file completely.
|
||||
*/
|
||||
|
||||
.wp-block-create-block-todo-list {
|
||||
border: 1px dotted #f00;
|
||||
}
|
||||
|
||||
.wp-block-lcp-bar-graph {
|
||||
margin: 1em 0;
|
||||
|
||||
.lcp-bar-graph-container {
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
|
||||
svg {
|
||||
overflow: visible;
|
||||
|
||||
.bar {
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.grid line {
|
||||
stroke-opacity: 0.1;
|
||||
}
|
||||
|
||||
.domain,
|
||||
.tick line {
|
||||
stroke: #666;
|
||||
}
|
||||
|
||||
.tick text {
|
||||
fill: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lcp-bar-graph-placeholder {
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
// Data Selector Styles
|
||||
.lcp-data-selector {
|
||||
.components-base-control {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.lcp-data-selector-error {
|
||||
margin-top: 0.5em;
|
||||
padding: 0.5em;
|
||||
border-left: 4px solid #cc1818;
|
||||
background-color: #f8d7da;
|
||||
}
|
||||
}
|
||||
39
blocks/bar-graph/src/index.js
Normal file
39
blocks/bar-graph/src/index.js
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Registers a new block provided a unique name and an object defining its behavior.
|
||||
*
|
||||
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
|
||||
*/
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
|
||||
* All files containing `style` keyword are bundled together. The code used
|
||||
* gets applied both to the front of your site and to the editor.
|
||||
*
|
||||
* @see https://www.npmjs.com/package/@wordpress/scripts#using-css
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Edit from './edit';
|
||||
import save from './save';
|
||||
import metadata from './block.json';
|
||||
|
||||
/**
|
||||
* Every block starts by registering a new block type definition.
|
||||
*
|
||||
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
|
||||
*/
|
||||
registerBlockType( metadata.name, {
|
||||
/**
|
||||
* @see ./edit.js
|
||||
*/
|
||||
edit: Edit,
|
||||
|
||||
/**
|
||||
* @see ./save.js
|
||||
*/
|
||||
save,
|
||||
} );
|
||||
35
blocks/bar-graph/src/save.js
Normal file
35
blocks/bar-graph/src/save.js
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* React hook that is used to mark the block wrapper element.
|
||||
* It provides all the necessary props like the class name.
|
||||
*
|
||||
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
|
||||
*/
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
|
||||
/**
|
||||
* The save function defines the way in which the different attributes should
|
||||
* be combined into the final markup, which is then serialized by the block
|
||||
* editor into `post_content`.
|
||||
*
|
||||
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save
|
||||
*
|
||||
* @return {Element} Element to render.
|
||||
*/
|
||||
export default function save({ attributes }) {
|
||||
const {
|
||||
chartHeight,
|
||||
backgroundColor,
|
||||
} = attributes;
|
||||
|
||||
return (
|
||||
<div { ...useBlockProps.save() }>
|
||||
<div
|
||||
className="lcp-bar-graph"
|
||||
style={{
|
||||
height: chartHeight,
|
||||
backgroundColor: backgroundColor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
blocks/bar-graph/src/style.scss
Normal file
12
blocks/bar-graph/src/style.scss
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* The following styles get applied both on the front of your site
|
||||
* and in the editor.
|
||||
*
|
||||
* Replace them with your own styles or remove the file completely.
|
||||
*/
|
||||
|
||||
.wp-block-create-block-todo-list {
|
||||
background-color: #21759b;
|
||||
color: #fff;
|
||||
padding: 2px;
|
||||
}
|
||||
25
blocks/bar-graph/src/view.js
Normal file
25
blocks/bar-graph/src/view.js
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Use this file for JavaScript code that you want to run in the front-end
|
||||
* on posts/pages that contain this block.
|
||||
*
|
||||
* When this file is defined as the value of the `viewScript` property
|
||||
* in `block.json` it will be enqueued on the front end of the site.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* "viewScript": "file:./view.js"
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* If you're not making any changes to this file because your project doesn't need any
|
||||
* JavaScript running in the front-end, then you should delete this file and remove
|
||||
* the `viewScript` property from `block.json`.
|
||||
*
|
||||
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#view-script
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log( 'Hello World! (from create-block-todo-list block)' );
|
||||
/* eslint-enable no-console */
|
||||
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": {}
|
||||
}
|
||||
223
lcp-data-blocks.php
Normal file
223
lcp-data-blocks.php
Normal file
@ -0,0 +1,223 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: LCP Data Blocks
|
||||
* Plugin URI: https://coosguide.com
|
||||
* Description: A plugin for displaying various charts and graphs in Local Content Pro
|
||||
* Version: 1.0.0
|
||||
* Author: CoosGuide
|
||||
* Author URI: https://coosguide.com
|
||||
* License: GPL v2 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
* Text Domain: lcp
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
// Include block files
|
||||
require_once plugin_dir_path(__FILE__) . 'blocks/bar-graph/lcp-bar-graph.php';
|
||||
|
||||
/**
|
||||
* Add custom category for LCP blocks
|
||||
*/
|
||||
function lcp_block_categories($categories) {
|
||||
return array_merge(
|
||||
$categories,
|
||||
array(
|
||||
array(
|
||||
'slug' => 'lcp-blocks',
|
||||
'title' => __('LCP Blocks', 'lcp'),
|
||||
'icon' => 'chart-bar',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
add_filter('block_categories_all', 'lcp_block_categories', 10, 1);
|
||||
|
||||
// Register the Data Collection Custom Post Type
|
||||
function lcp_register_data_collection_post_type() {
|
||||
$labels = array(
|
||||
'name' => _x('Data Collections', 'Post type general name', 'lcp-data'),
|
||||
'singular_name' => _x('Data Collection', 'Post type singular name', 'lcp-data'),
|
||||
'menu_name' => _x('Data Collections', 'Admin Menu text', 'lcp-data'),
|
||||
'name_admin_bar' => _x('Data Collection', 'Add New on Toolbar', 'lcp-data'),
|
||||
'add_new' => __('Add New', 'lcp-data'),
|
||||
'add_new_item' => __('Add New Data Collection', 'lcp-data'),
|
||||
'new_item' => __('New Data Collection', 'lcp-data'),
|
||||
'edit_item' => __('Edit Data Collection', 'lcp-data'),
|
||||
'view_item' => __('View Data Collection', 'lcp-data'),
|
||||
'all_items' => __('All Data Collections', 'lcp-data'),
|
||||
'search_items' => __('Search Data Collections', 'lcp-data'),
|
||||
'parent_item_colon' => __('Parent Data Collections:', 'lcp-data'),
|
||||
'not_found' => __('No data collections found.', 'lcp-data'),
|
||||
'not_found_in_trash' => __('No data collections found in Trash.', 'lcp-data'),
|
||||
'featured_image' => _x('Data Collection Cover Image', 'Overrides the "Featured Image" phrase', 'lcp-data'),
|
||||
'set_featured_image' => _x('Set cover image', 'Overrides the "Set featured image" phrase', 'lcp-data'),
|
||||
'remove_featured_image'=> _x('Remove cover image', 'Overrides the "Remove featured image" phrase', 'lcp-data'),
|
||||
'use_featured_image' => _x('Use as cover image', 'Overrides the "Use as featured image" phrase', 'lcp-data'),
|
||||
'archives' => _x('Data Collection archives', 'The post type archive label used in nav menus', 'lcp-data'),
|
||||
'insert_into_item' => _x('Insert into data collection', 'Overrides the "Insert into post" phrase', 'lcp-data'),
|
||||
'uploaded_to_this_item'=> _x('Uploaded to this data collection', 'Overrides the "Uploaded to this post" phrase', 'lcp-data'),
|
||||
'filter_items_list' => _x('Filter data collections list', 'Screen reader text for the filter links', 'lcp-data'),
|
||||
'items_list_navigation'=> _x('Data Collections list navigation', 'Screen reader text for the pagination', 'lcp-data'),
|
||||
'items_list' => _x('Data Collections list', 'Screen reader text for the items list', 'lcp-data'),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => true,
|
||||
'query_var' => true,
|
||||
'rewrite' => array('slug' => 'data-collection'),
|
||||
'capability_type' => 'post',
|
||||
'has_archive' => true,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'menu_icon' => 'dashicons-chart-area',
|
||||
'supports' => array('title', 'editor', 'author', 'thumbnail', 'excerpt', 'custom-fields'),
|
||||
'show_in_rest' => true, // Enable Gutenberg editor
|
||||
);
|
||||
|
||||
register_post_type('lcp-data-collection', $args);
|
||||
}
|
||||
add_action('init', 'lcp_register_data_collection_post_type');
|
||||
|
||||
// Register meta field for datasets
|
||||
function lcp_register_meta_fields() {
|
||||
register_post_meta('lcp-data-collection', 'lcp_datasets', array(
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'array',
|
||||
'auth_callback' => function() {
|
||||
return current_user_can('edit_posts');
|
||||
},
|
||||
'sanitize_callback' => function($meta_value) {
|
||||
if (!is_array($meta_value)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$sanitized = array();
|
||||
foreach ($meta_value as $key => $dataset) {
|
||||
$sanitized[$key] = array(
|
||||
'dataset_name' => sanitize_text_field($dataset['dataset_name']),
|
||||
'dataset_source' => sanitize_text_field($dataset['dataset_source']),
|
||||
'dataset_json' => json_decode(wp_unslash($dataset['dataset_json']), true)
|
||||
);
|
||||
}
|
||||
return $sanitized;
|
||||
}
|
||||
));
|
||||
}
|
||||
add_action('init', 'lcp_register_meta_fields');
|
||||
|
||||
// Add meta box for datasets
|
||||
function lcp_add_datasets_meta_box() {
|
||||
add_meta_box(
|
||||
'lcp_datasets_meta_box',
|
||||
'Datasets',
|
||||
'lcp_render_datasets_meta_box',
|
||||
'lcp-data-collection',
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
}
|
||||
add_action('add_meta_boxes', 'lcp_add_datasets_meta_box');
|
||||
|
||||
// Render the meta box
|
||||
function lcp_render_datasets_meta_box($post) {
|
||||
// Get existing datasets
|
||||
$datasets = get_post_meta($post->ID, 'lcp_datasets', true);
|
||||
|
||||
// Ensure it's an array
|
||||
if (!is_array($datasets)) {
|
||||
$datasets = array();
|
||||
}
|
||||
|
||||
// Convert PHP array to JSON for the React component
|
||||
$datasets_json = json_encode($datasets);
|
||||
|
||||
// Add nonce for verification
|
||||
wp_nonce_field('lcp_datasets_meta_box', 'lcp_datasets_meta_box_nonce');
|
||||
|
||||
// Add hidden input for data
|
||||
?>
|
||||
<input type="hidden" id="lcp_datasets_data" name="lcp_datasets_data" value="<?php echo esc_attr($datasets_json); ?>" />
|
||||
<div id="lcp-datasets-repeater" data-datasets="<?php echo esc_attr($datasets_json); ?>"></div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// Save the meta box data
|
||||
function lcp_save_datasets_meta($post_id) {
|
||||
// Check if our nonce is set and verify it
|
||||
if (!isset($_POST['lcp_datasets_meta_box_nonce']) ||
|
||||
!wp_verify_nonce($_POST['lcp_datasets_meta_box_nonce'], 'lcp_datasets_meta_box')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is an autosave, don't do anything
|
||||
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the user's permissions
|
||||
if (!current_user_can('edit_post', $post_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the data
|
||||
if (isset($_POST['lcp_datasets_data'])) {
|
||||
$datasets = json_decode(wp_unslash($_POST['lcp_datasets_data']), true);
|
||||
if (is_array($datasets)) {
|
||||
update_post_meta($post_id, 'lcp_datasets', $datasets);
|
||||
}
|
||||
}
|
||||
}
|
||||
add_action('save_post_lcp-data-collection', 'lcp_save_datasets_meta');
|
||||
|
||||
// Enqueue scripts and styles for the admin
|
||||
function lcp_enqueue_admin_scripts($hook) {
|
||||
global $post;
|
||||
|
||||
// Only enqueue on our custom post type edit screen
|
||||
if ($hook == 'post-new.php' || $hook == 'post.php') {
|
||||
if (is_object($post) && $post->post_type == 'lcp-data-collection') {
|
||||
// Enqueue React and dependencies
|
||||
wp_enqueue_script('wp-element');
|
||||
wp_enqueue_script('wp-components');
|
||||
wp_enqueue_script('wp-data');
|
||||
|
||||
// Enqueue our custom scripts
|
||||
wp_enqueue_script(
|
||||
'lcp-form-repeater',
|
||||
plugins_url('build/index.js', __FILE__),
|
||||
array('wp-element', 'wp-components', 'wp-data'),
|
||||
filemtime(plugin_dir_path(__FILE__) . 'build/index.js'),
|
||||
true
|
||||
);
|
||||
|
||||
// Pass data to JavaScript
|
||||
wp_localize_script('lcp-form-repeater', 'lcpDataSettings', array(
|
||||
'root' => esc_url_raw(rest_url()),
|
||||
'nonce' => wp_create_nonce('wp_rest'),
|
||||
'postId' => $post->ID
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
add_action('admin_enqueue_scripts', 'lcp_enqueue_admin_scripts');
|
||||
|
||||
// Flush rewrite rules on plugin activation
|
||||
function lcp_data_activate() {
|
||||
lcp_register_data_collection_post_type();
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
register_activation_hook(__FILE__, 'lcp_data_activate');
|
||||
|
||||
// Flush rewrite rules on plugin deactivation
|
||||
function lcp_data_deactivate() {
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
register_deactivation_hook(__FILE__, 'lcp_data_deactivate');
|
||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "lcp-data-blocks",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
Reference in New Issue
Block a user