Block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "create-block/content-testimonials",
"version": "0.1.0",
"title": "Content Testimonials",
"category": "widgets",
"icon": "smiley",
"description": "Example block scaffolded with Create Block tool.",
"example": {},
"supports": {
"html": false
},
"attributes": {
"section_style": {
"type": "string",
"default": ""
},
"section_class": {
"type": "string",
"default": ""
},
"section_id": {
"type": "string",
"default": ""
},
"section_image": {
"type": "string",
"default": null
},
"section_image_class": {
"type": "string"
},
"section_image_style": {
"type": "string"
},
"section_block": {
"type": "string",
"default": ""
},
"container_style": {
"type": "string",
"default": ""
},
"container_class": {
"type": "string",
"default": "container"
},
"container_id": {
"type": "string",
"default": ""
},
"row_style": {
"type": "string",
"default": ""
},
"row_class": {
"type": "string",
"default": "row justify-content-center testimonial-carousel owl-carousel owl-theme"
},
"row_id": {
"type": "string",
"default": ""
},
"col_style": {
"type": "string",
"default": "padding-bottom: 25px;"
},
"col_class": {
"type": "string",
"default": "col-lg-9 text-center"
},
"col_id": {
"type": "string",
"default": ""
},
"col_data_aos": {
"type": "string",
"default": "fade-up"
},
"col_data_aos_delay": {
"type": "string",
"default": ""
},
"col_data_aos_offset": {
"type": "string",
"default": ""
},
"testimonial_style": {
"type": "string",
"default": ""
},
"testimonial_class": {
"type": "string",
"default": "col-lg-10 testimonial-carousel owl-carousel owl-theme"
},
"testimonial_id": {
"type": "string",
"default": ""
},
"testimonial_data_aos": {
"type": "string",
"default": "fade-up"
},
"testimonial_data_aos_delay": {
"type": "string",
"default": ""
},
"testimonial_data_aos_offset": {
"type": "string",
"default": ""
},
"testimonials": {
"type": "array",
"items": {
"type": "object"
},
"default": [
{
"col_class": "",
"col_style": "",
"col_id": "",
"data_aos":"",
"data_aos_delay": "",
"name": "",
"content": ""
}
]
}
},
"textdomain": "content-testimonials",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"viewScript": "file:./view.js"
}
Edit.js
/**
* Retrieves the translation of text.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
*/
import { __ } from '@wordpress/i18n';
/**
* 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 {
InspectorControls,
useBlockProps,
InnerBlocks,
MediaUpload,
MediaUploadCheck,
RichText,
} from '@wordpress/block-editor';
import {
Button,
PanelBody,
__experimentalInputControl as InputControl,
TextControl,
} from '@wordpress/components';
import { useState, useEffect } from '@wordpress/element';
// import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
/**
* Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
* Those files can contain any CSS code that gets applied to the editor.
*
* @see https://www.npmjs.com/package/@wordpress/scripts#using-css
*/
import './editor.scss';
/**
* The edit function describes the structure of your block in the context of the
* editor. This represents what the editor will render when the block is used.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
*
* @return {Element} Element to render.
*/
export default function Edit( { attributes, setAttributes } ) {
const {
section_style,
section_class,
section_id,
section_image,
section_image_class,
section_image_style,
section_block,
container_style,
container_class,
container_id,
row_style,
row_class,
row_id,
col_style,
col_class,
col_id,
col_data_aos,
col_data_aos_delay,
col_data_aos_offset,
testimonial_style,
testimonial_class,
testimonial_id,
testimonial_data_aos,
testimonial_data_aos_delay,
testimonial_data_aos_offset,
testimonials,
} = attributes;
const [ value, setValue ] = useState( '' );
const addTestimonial = () => {
setAttributes( {
testimonials: [
...testimonials,
{
col_class: '',
col_style: '',
col_id: '',
data_aos: 'fade-up',
data_aos_delay: '',
img: '',
alt:'',
img_style:'',
img_class:'',
title: '',
content: '',
code_block: ''
},
],
} );
};
// const updateTestimonial = ( testimonialIndex, field, value ) => {
// setAttributes( {
// testimonials: testimonials.map( ( testimonial, index ) => {
// if ( index === testimonialIndex ) {
// return {
// ...testimonial,
// [ field ]: value,
// };
// }
// return testimonial;
// } ),
// } );
// };
const updateTestimonial = (testimonialIndex, field, value) => {
setAttributes({
testimonials: testimonials.map((testimonial, index) => {
if (index === testimonialIndex) {
// Check if value is an object (for handling multiple fields like img and alt)
if (typeof value === 'object' && value !== null) {
return {
...testimonial,
// Spread the value object to update multiple fields
...value,
};
}
// Default case: single value for a single field
return {
...testimonial,
[field]: value,
};
}
return testimonial;
}),
});
};
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Section' ) } initialOpen={ false }>
<InputControl
label="Section Style"
value={ section_style }
onChange={ ( nextValue ) =>
setAttributes( { section_style: nextValue } )
}
/>
<InputControl
label="Section Class"
value={ section_class }
onChange={ ( nextValue ) =>
setAttributes( { section_class: nextValue } )
}
/>
<InputControl
label="Section ID"
value={ section_id }
onChange={ ( nextValue ) =>
setAttributes( { section_id: nextValue } )
}
/>
</PanelBody>
<PanelBody
title={ __( 'Background Image' ) }
initialOpen={ false }
>
<MediaUploadCheck>
<MediaUpload
onSelect={ ( media ) =>
setAttributes( { section_image: media.url } )
}
type="image"
allowedTypes={ [ 'image' ] }
value={ section_image }
render={ ( { open } ) => (
<div>
{ section_image && (
<Button
isLink
isDestructive
onClick={ () =>
setAttributes( {
section_image: '',
} )
}
>
{ __( 'Remove Section Image' ) }
</Button>
) }
<Button
onClick={ open }
icon="upload"
className="editor-media-placeholder__button is-button is-default is-large"
>
{ __( 'Select Section Image' ) }
</Button>
</div>
) }
/>
</MediaUploadCheck>
<InputControl
label="Background Image Class"
value={ section_image_class }
onChange={ ( nextValue ) =>
setAttributes( { section_image_class: nextValue } )
}
/>
<InputControl
label="Background Image Style"
value={ section_image_style }
onChange={ ( nextValue ) =>
setAttributes( { section_image_style: nextValue } )
}
/>
</PanelBody>
<PanelBody title={ __( 'Code Block' ) } initialOpen={ false }>
<label style={ { lineHeight: '2' } }>Code Block</label>
<textarea
id="sectionStyleTextarea"
value={ attributes.section_block }
onChange={ ( event ) =>
setAttributes( {
section_block: event.target.value,
} )
}
placeholder="Enter section block here"
style={ { width: '100%', height: '100px' } }
/>
</PanelBody>
<PanelBody title={ __( 'Container' ) } initialOpen={ false }>
<InputControl
label="Container Section Style"
value={ container_style }
onChange={ ( nextValue ) =>
setAttributes( { container_style: nextValue } )
}
/>
<InputControl
label="Container Section Class"
value={ container_class }
onChange={ ( nextValue ) =>
setAttributes( { container_class: nextValue } )
}
/>
<InputControl
label="Container Section ID"
value={ container_id }
onChange={ ( nextValue ) =>
setAttributes( { container_id: nextValue } )
}
/>
</PanelBody>
<PanelBody title={ __( 'Row' ) } initialOpen={ false }>
<InputControl
label="Row Style"
value={ row_style }
onChange={ ( nextValue ) =>
setAttributes( { row_style: nextValue } )
}
/>
<InputControl
label="Row Class"
value={ row_class }
onChange={ ( nextValue ) =>
setAttributes( { row_class: nextValue } )
}
/>
<InputControl
label="Row ID"
value={ row_id }
onChange={ ( nextValue ) =>
setAttributes( { row_id: nextValue } )
}
/>
</PanelBody>
<PanelBody
title={ __( 'Testimonial Settings' ) }
initialOpen={ false }
>
<InputControl
label="Testimonial Style"
value={ testimonial_style }
onChange={ ( nextValue ) =>
setAttributes( { testimonial_style: nextValue } )
}
/>
<InputControl
label="Testimonial Class"
value={ testimonial_class }
onChange={ ( nextValue ) =>
setAttributes( { testimonial_class: nextValue } )
}
/>
<InputControl
label="Testimonial ID"
value={ testimonial_id }
onChange={ ( nextValue ) =>
setAttributes( { testimonial_id: nextValue } )
}
/>
<InputControl
label="Testimonial Data AOS"
value={ testimonial_data_aos }
onChange={ ( nextValue ) =>
setAttributes( { testimonial_data_aos: nextValue } )
}
/>
<InputControl
label="Testimonial Data AOS Delay"
value={ testimonial_data_aos_delay }
onChange={ ( nextValue ) =>
setAttributes( { testimonial_data_aos_delay: nextValue } )
}
/>
<InputControl
label="Testimonial Data AOS Offset"
value={ testimonial_data_aos_offset }
onChange={ ( nextValue ) =>
setAttributes( { testimonial_data_aos_offset: nextValue } )
}
/>
<button onClick={ () => addTestimonial() }>
Add New Testimonial
</button>
</PanelBody>
<PanelBody title={ __( 'Column' ) } initialOpen={ false }>
<InputControl
label="Column Style"
value={ col_style }
onChange={ ( nextValue ) =>
setAttributes( { col_style: nextValue } )
}
/>
<InputControl
label="Column Class"
value={ col_class }
onChange={ ( nextValue ) =>
setAttributes( { col_class: nextValue } )
}
/>
<InputControl
label="Column ID"
value={ col_id }
onChange={ ( nextValue ) =>
setAttributes( { col_id: nextValue } )
}
/>
<InputControl
label="Column Data AOS"
value={ col_data_aos }
onChange={ ( nextValue ) =>
setAttributes( { col_data_aos: nextValue } )
}
/>
<InputControl
label="Column Data AOS Delay"
value={ col_data_aos_delay }
onChange={ ( nextValue ) =>
setAttributes( { col_data_aos_delay: nextValue } )
}
/>
<InputControl
label="Column Data AOS Offset"
value={ col_data_aos_offset }
onChange={ ( nextValue ) =>
setAttributes( { col_data_aos_offset: nextValue } )
}
/>
</PanelBody>
</InspectorControls>
<section { ...useBlockProps() }>
<img src={ section_image } alt="" />
<p style={{fontSize:'24px'}}>Inner Blocks Below</p>
<InnerBlocks />
{console.log('hello')}
<p style={{fontSize:'24px'}}>Testimonials Below</p>
<div className="column-wrapper">
{ testimonials.map( ( testimonial, index ) => {
return (
<div
className={ `column ${ testimonial.col_class }` }
style={ {
padding: '25px',
borderBottom: '1px solid',
marginBottom: '25px',
} }
>
<div style={{display:'flex'}}>
<div style={{paddingRight:'25px'}}>
<p style={ { marginBottom: '0px' } }>
Testimonial Class
</p>
<input
type="text"
value={ testimonial.col_class }
onChange={ ( content ) =>
updateTestimonial(
index,
'col_class',
content.target.value
)
}
/>
</div>
<div style={{paddingRight:'25px'}}>
<p style={ { marginBottom: '0px' } }>
Testimonial Style
</p>
<input
type="text"
value={ testimonial.col_style }
onChange={ ( content ) =>
updateTestimonial(
index,
'col_style',
content.target.value
)
}
/>
</div>
<div>
<p style={ { marginBottom: '0px' } }>
Testimonial ID
</p>
<input
type="text"
value={ testimonial.col_id }
onChange={ ( content ) =>
updateTestimonial(
index,
'col_id',
content.target.value
)
}
/>
</div>
</div>
<div style={{display:'flex'}}>
<div style={{paddingRight:'25px'}}>
<p style={ { marginBottom: '0px' } }>
Data AOS
</p>
<input
type="text"
value={ testimonial.data_aos }
onChange={ ( content ) =>
updateTestimonial(
index,
'data_aos',
content.target.value
)
}
/>
</div>
<div style={{paddingRight:'25px'}}>
<p style={ { marginBottom: '0px' } }>
Data AOS Delay
</p>
<input
type="text"
value={ testimonial.data_aos_delay }
onChange={ ( content ) =>
updateTestimonial(
index,
'data_aos_delay',
content.target.value
)
}
/>
</div>
</div>
<div style={{display:'flex',justifyContent:'space-between',marginTop:'25px'}}>
<div style={{width:'49%'}}>
<img
src={testimonial.img}
style={{width:'200px',height:'auto'}}
/>
<MediaUploadCheck>
<MediaUpload
onSelect={(media) =>
updateTestimonial(index, 'img', { img: media.url, alt: media.alt })
}
type="image"
allowedTypes={['image']}
value={testimonial.img}
render={({ open }) => (
<div>
{testimonial.img && (
<p className={``} style={{fontSize:'80%',lineHeight:'1.2'}}>{__('Alt Text:')} {testimonial.alt}</p>
)}
{testimonial.img && (
<Button
isLink
isDestructive
onClick={() => updateTestimonial(index, 'img', '')}
>
{__('Remove Col Image')}
</Button>
)}
<Button
onClick={open}
icon="upload"
className="editor-media-placeholder__button is-button is-default is-large"
>
{__('Select Col Image')}
</Button>
</div>
)}
/>
</MediaUploadCheck>
<div style={{display:'flex'}}>
<div style={{paddingRight:'25px',width:'49%'}}>
<p style={ { marginBottom: '0px' } }>Img Class</p>
<input
type="text"
style={{}}
value={ testimonial.img_class }
onChange={ ( content ) =>
updateTestimonial(
index,
'img_class',
content.target.value
)
}
/>
</div>
<div style={{width:'49%'}}>
<p style={ { marginBottom: '0px' } }>Img Style</p>
<input
type="text"
style={{}}
value={ testimonial.img_style }
onChange={ ( content ) =>
updateTestimonial(
index,
'img_style',
content.target.value
)
}
/>
</div>
</div> {/** end of display flex */}
</div>
<div style={{width:'49%'}}>
<p>Code Block</p>
<textarea
value={ testimonial.code_block }
onChange={ ( content ) =>
updateTestimonial( index, 'code_block', content.target.value )
}
style={{width:'100%',height:'200px'}}
placeholder={ __( 'Code Block' ) }
/>
</div>
</div>
<div
style={ {
display: 'flex',
paddingTop: '25px',
} }
>
<div style={ { } }>
<p style={{marginBottom:'0px'}}><strong>Testimonial Content</strong></p>
<RichText
value={ testimonial.content }
onChange={ ( content ) =>
updateTestimonial(
index,
'content',
content
)
}
placeholder={ __( '' ) }
/>
<p style={{marginBottom:'0px'}}><strong>Testimonial Name</strong></p>
<RichText
value={ testimonial.title }
onChange={ ( content ) =>
updateTestimonial(
index,
'title',
content
)
}
placeholder={ __( '' ) }
/>
</div>
</div>
<Button
style={{border:'1px solid'}}
onClick={() => {
const newTestimonials = [...testimonials]; // Create a copy of the columns array
const newColumn = { // Define a new column object
col_class: '',
col_style: '',
col_id: '',
data_aos: 'fade-up',
data_aos_delay: '',
img: '',
alt:'',
img_style:'',
img_class:'',
title: '',
content: '',
code_block: ''
};
newTestimonials.splice(index, 0, newColumn); // Insert the new column at the current index
setAttributes({ testimonials: newTestimonials }); // Update the columns attribute with the new array
}}
>
{__('Add Testimonial Above')}
</Button>
<Button
style={{border:'1px solid'}}
onClick={() => {
const newTestimonials = [...testimonials]; // Create a copy of the columns array
const newColumn = { // Define a new column object
col_class: '',
col_style: '',
col_id: '',
data_aos: 'fade-up',
data_aos_delay: '',
img: '',
alt:'',
img_style:'',
img_class:'',
title: '',
content: '',
code_block: ''
};
newTestimonials.splice(index + 1, 0, newColumn); // Insert the new column at the current index
setAttributes({ testimonials: newTestimonials }); // Update the columns attribute with the new array
}}
>
{__('Add Testimonial Below')}
</Button>
{/* Duplicate Button */}
<Button
style={{ border: '1px solid', marginTop: '10px' }}
onClick={() => {
const newTestimonials = [...testimonials];
const duplicateFeature = { ...testimonial }; // Copy the testimonial object
newTestimonials.splice(index + 1, 0, duplicateFeature); // Insert the copy after the current testimonial
setAttributes({ testimonials: newTestimonials });
}}
>
{__('Duplicate Feature')}
</Button>
<Button
style={{border:'1px solid'}}
isDestructive
onClick={() => {
const newTestimonials = [...testimonials];
newTestimonials.splice(index, 1);
setAttributes({ testimonials: newTestimonials });
}}
>
{__('Remove Testimonial')}
</Button>
{/* Move Up Button */}
<Button
style={{ border: '1px solid', marginTop: '10px' }}
onClick={() => {
if (index === 0) return; // Prevent moving the first item up
const newTestimonials = [...testimonials];
const temp = newTestimonials[index - 1];
newTestimonials[index - 1] = newTestimonials[index];
newTestimonials[index] = temp;
setAttributes({ testimonials: newTestimonials });
}}
disabled={index === 0} // Disable if it's the first item
>
{__('Move Up')}
</Button>
{/* Move Down Button */}
<Button
style={{ border: '1px solid', marginTop: '10px' }}
onClick={() => {
if (index === testimonials.length - 1) return; // Prevent moving the last item down
const newTestimonials = [...testimonials];
const temp = newTestimonials[index + 1];
newTestimonials[index + 1] = newTestimonials[index];
newTestimonials[index] = temp;
setAttributes({ testimonials: newTestimonials });
}}
disabled={index === testimonials.length - 1} // Disable if it's the last item
>
{__('Move Down')}
</Button>
</div>
);
} ) }
</div>
</section>
</>
);
}
Save.js
/**
* 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, InnerBlocks, RichText } from ‘@wordpress/block-editor’;
import { RawHTML } from ‘@wordpress/element’;
/**
* 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 blockProps = useBlockProps.save();
return (
<div { …blockProps }>
<section
className={ `position-relative ${ attributes.section_class }` }
style={ `padding:50px 0;${ attributes.section_style }` }
id={ attributes.section_id }
>
{/* <div className=”column-wrapper”> */}
{ attributes.section_image && (
<img
src={ attributes.section_image }
alt=””
className={ `w-100 h-100 position-absolute bg-img ${ attributes.section_image_class }` }
style={ `top:0;left:0;object-fit:cover;pointer-events:none;${ attributes.section_image_style }` }
/>
) }
<RawHTML>{ attributes.section_block }</RawHTML>
<div
className={ attributes.container_class }
style={ attributes.container_style }
id={ attributes.container_id }
>
<div
className={ attributes.row_class }
style={ attributes.row_style }
id={ attributes.row_id }
>
<div className={attributes.col_class} style={attributes.col_style} id={attributes.col_id} data-aos={attributes.col_data_aos} data-aos-delay={attributes.col_data_aos_delay} data-aos-offset={attributes.col_data_aos_delay_offset}>
<InnerBlocks.Content />
</div>
<div className={attributes.testimonial_class} style={attributes.testimonial_style} id={attributes.testimonial_id} data-aos={attributes.testimonial_data_aos} data-aos-delay={attributes.testimonial_data_aos_delay} data-aos-offset={attributes.testimonial_data_aos_delay_offset}>
{
attributes.testimonials.map((testimonial, index) => {
return (
<div className={`position-relative text-center ${testimonial.col_class}`} style={`${testimonial.col_style}`}>
<div className={“} data-aos={testimonial.data_aos} data-aos-delay={testimonial.data_aos_delay}>
<div className={“}>
<RawHTML>{testimonial.code_block}</RawHTML>
{ testimonial.img && (
<div className={”} style={{display: ‘inline-block’}}>
<img
src={ testimonial.img }
alt={ testimonial.alt }
className={testimonial.img_class}
style={`border-radius: 50%;height: 100px;width: 100px;object-fit: cover;object-position: top;${testimonial.img_style}`}
/>
</div>
)}
<div className={`position-relative`} style={{paddingLeft:’25px’}}>
<img src=”https://resources.latinowebstudio.com/wp-content/uploads/2024/10/Quotes-Gray.png” className={`position-absolute`} style=”object-fit:contain;top:5px;left:0px;width:auto;” width=”auto” height=”15px” alt=”quotes for testimonials” />
<p style={{ margin: ‘0px’ }} className={`text-left`}><RichText.Content value={testimonial.content} /></p>
<img src=”https://resources.latinowebstudio.com/wp-content/uploads/2024/10/Five-Stars.png” style=”width:75px;margin:5px auto;” alt=”happy customers” />
</div>
</div>
<p className={`bold`} style={{ cursor: ‘pointer’ }}><RichText.Content value={testimonial.title} /></p>
</div>
</div>
);
})
}
</div> {/* end of testimonial */}
</div>
</div>
</section>
</div>
);
}
Style.scss
/**
* 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-content-testimonials {
@media only screen and (min-width:991px) {
button.owl-prev,
button.owl-next {
position: absolute;
top: 20%;
}
button.owl-prev {
left:-4%;
}
button.owl-next {
right:-4%;
}
}
.owl-nav img {
width: 35px;
}
p {
color: #acacac;
}
}
// Import CSS files from owl-carousel directory
// @import 'owl-carousel/owl.carousel.min.css';
// .testimonial-carousel {
// background:red;
// }
// @import './owl-carousel/owl-carousel.css';
// @import 'owl-carousel/owl-carousel';
// @import 'owl-carousel/owl.carousel.min.css';
// @import 'owl-carousel/owl.theme.default.min.css';
// .testimonial-carousel {
// background:red;
// }
Have any questions or comments? Write them below!