AlkantarClanX12
| Current Path : /www/capitalgmcbuickregina_830/public/wp-content/themes/lbx-egypt/resources/scripts/ |
| Current File : /www/capitalgmcbuickregina_830/public/wp-content/themes/lbx-egypt/resources/scripts/editor.js |
import { domReady } from "@roots/sage/client";
import { registerBlockStyle, unregisterBlockStyle } from "@wordpress/blocks";
import { __ } from "@wordpress/i18n";
import { registerBlockType } from "@wordpress/blocks";
import { InnerBlocks } from "@wordpress/block-editor";
import { useSelect } from "@wordpress/data";
/**
* editor.main
*/
const main = (err) => {
if (err) {
// handle hmr errors
console.error(err);
}
unregisterBlockStyle("core/button", "outline");
registerBlockStyle("core/button", {
name: "outline",
label: "Outline",
});
/** Poet Components */
const { PlainText } = wp.editor;
const { SelectControl, PanelBody, TextControl } = wp.components;
const { InspectorControls, MediaUpload, MediaUploadCheck } = wp.blockEditor;
registerBlockType("sage/showroom", {
title: __("Showroom", "sage"),
category: "leadbox",
icon: "car",
attributes: {
accordions: {
type: "array",
default: [{ label: "New Showroom" }],
},
},
edit: () => {
const shortcode = `[showroom]`;
return (
<div className="leadbox-showroom-block">
<div className="block">
<h3>Showroom shortcode</h3>
<div>
This block will render on the frontend as a graphical showroom
generated by the following shortcode:
</div>
<pre>
<PlainText className="shortcode" value={shortcode} />
</pre>
<div>Configure this block on the right panel.</div>
</div>
</div>
);
},
save: () => <InnerBlocks.Content />,
});
registerBlockType("sage/showroom-multi-brands", {
title: __("Showroom Multi Brands", "sage"),
category: "leadbox",
icon: "car",
attributes: {
accordions: {
type: "array",
default: [{ label: "New Showroom" }],
},
},
edit: () => {
const shortcode = `[showroom-multi-brands]`;
return (
<div className="leadbox-showroom-block">
<div className="block">
<h3>Showroom Multi Brands shortcode</h3>
<div>
This block will render on the frontend as a graphical showroom
generated by the following shortcode:
</div>
<pre>
<PlainText className="shortcode" value={shortcode} />
</pre>
<div>Configure this block on the right panel.</div>
</div>
</div>
);
},
save: () => <InnerBlocks.Content />,
});
registerBlockType("sage/contact-us-card", {
title: __("Contact Us Card", "sage"),
category: "leadbox",
icon: "id",
attributes: {
version: {
type: "string",
default: "1",
},
imageUrl: {
type: "string",
default: `${sageThemeData.themeUri}/resources/images/300x150.png`,
},
imageId: {
type: "number",
default: null,
},
},
edit: (props) => {
const { attributes, setAttributes } = props;
const { version, imageUrl, imageId } = attributes;
const onSelectImage = (media) => {
setAttributes({
imageUrl: media.url,
imageId: media.id,
});
};
const onRemoveImage = () => {
setAttributes({
imageUrl: "",
imageId: null,
});
};
const onChangeVersion = (newVersion) => {
console.log(newVersion);
setAttributes({
version: newVersion,
});
};
const renderBlockContent = () => {
switch (version) {
case "1":
return (
<div className="contact-card-v1">
<img
src={imageUrl}
alt="Image"
className="contact-card-v1-image"
/>
<div className="contact-card-v1-content">
<h2 className="contact-card-v1-title">[DEALER NAME]</h2>
<div className="contact-card-v1-address">
<p>[Address Line 1], [Address Line 2],</p>
<p>[City], [Province]</p>
<p>[Postal code]</p>
</div>
<a href="javascript:void(0)" className="contact-card-v1-link">
View Inventory
</a>
</div>
</div>
);
case "2":
return (
<div className="contact-card-v2">
<h2 className="contact-card-v2-title">Contact Us</h2>
<div className="contact-card-v2-address">
<p>
<strong>[Address Line 1], [Address Line 2],</strong>
</p>
<p>
<strong>[City], [Province], [Postal code]</strong>
</p>
<p>
<strong>General:</strong> 000-000-0000
</p>
</div>
</div>
);
case "3":
return (
<div className="contact-card-v3">
<h2 className="contact-card-v3-title">Contact Information</h2>
<div className="contact-card-v3-address">
<p>
<svg
width="20"
height="21"
viewBox="0 0 24 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22.0001 17.92V20.92C22.0012 21.1985 21.9441 21.4742 21.8326 21.7294C21.721 21.9845 21.5574 22.2136 21.3521 22.4019C21.1469 22.5901 20.9046 22.7335 20.6408 22.8227C20.377 22.9119 20.0974 22.9451 19.8201 22.92C16.7429 22.5856 13.7871 21.5342 11.1901 19.85C8.77388 18.3147 6.72539 16.2662 5.19006 13.85C3.50003 11.2412 2.4483 8.271 2.12006 5.18001C2.09507 4.90347 2.12793 4.62477 2.21656 4.36163C2.30518 4.09849 2.44763 3.85669 2.63482 3.65163C2.82202 3.44656 3.04986 3.28271 3.30385 3.17053C3.55783 3.05834 3.8324 3.00027 4.11006 3.00001H7.11006C7.59536 2.99523 8.06585 3.16708 8.43382 3.48354C8.80179 3.79999 9.04213 4.23945 9.11005 4.72001C9.23668 5.68007 9.47151 6.62273 9.81006 7.53001C9.9446 7.88793 9.97372 8.27692 9.89396 8.65089C9.81421 9.02485 9.62892 9.36812 9.36005 9.64001L8.09006 10.91C9.51361 13.4136 11.5865 15.4865 14.0901 16.91L15.3601 15.64C15.6319 15.3711 15.9752 15.1859 16.3492 15.1061C16.7231 15.0263 17.1121 15.0555 17.4701 15.19C18.3773 15.5286 19.32 15.7634 20.2801 15.89C20.7658 15.9585 21.2095 16.2032 21.5266 16.5775C21.8437 16.9518 22.0122 17.4296 22.0001 17.92Z"
stroke="black"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<strong>Phone: </strong> 000-000-0000
</p>
<p>
<svg
width="20"
height="21"
viewBox="0 0 24 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22.0001 17.92V20.92C22.0012 21.1985 21.9441 21.4742 21.8326 21.7294C21.721 21.9845 21.5574 22.2136 21.3521 22.4019C21.1469 22.5901 20.9046 22.7335 20.6408 22.8227C20.377 22.9119 20.0974 22.9451 19.8201 22.92C16.7429 22.5856 13.7871 21.5342 11.1901 19.85C8.77388 18.3147 6.72539 16.2662 5.19006 13.85C3.50003 11.2412 2.4483 8.271 2.12006 5.18001C2.09507 4.90347 2.12793 4.62477 2.21656 4.36163C2.30518 4.09849 2.44763 3.85669 2.63482 3.65163C2.82202 3.44656 3.04986 3.28271 3.30385 3.17053C3.55783 3.05834 3.8324 3.00027 4.11006 3.00001H7.11006C7.59536 2.99523 8.06585 3.16708 8.43382 3.48354C8.80179 3.79999 9.04213 4.23945 9.11005 4.72001C9.23668 5.68007 9.47151 6.62273 9.81006 7.53001C9.9446 7.88793 9.97372 8.27692 9.89396 8.65089C9.81421 9.02485 9.62892 9.36812 9.36005 9.64001L8.09006 10.91C9.51361 13.4136 11.5865 15.4865 14.0901 16.91L15.3601 15.64C15.6319 15.3711 15.9752 15.1859 16.3492 15.1061C16.7231 15.0263 17.1121 15.0555 17.4701 15.19C18.3773 15.5286 19.32 15.7634 20.2801 15.89C20.7658 15.9585 21.2095 16.2032 21.5266 16.5775C21.8437 16.9518 22.0122 17.4296 22.0001 17.92Z"
stroke="black"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<strong>Phone 2: </strong> 000-000-0000
</p>
<p style={{ marginTop: "15px" }}>
{" "}
<svg
width="20"
height="21"
viewBox="0 0 24 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.7392 9.86957C17.7392 15.1928 11.8696 22 11.8696 22C11.8696 22 6 15.3197 6 9.86957C6 6.62761 8.62761 4 11.8696 4C15.1112 4 17.7392 6.62761 17.7392 9.86957Z"
stroke="#39393C"
stroke-width="1.7647"
stroke-miterlimit="10"
stroke-linejoin="round"
/>
<path
d="M11.8697 12.6087C13.1663 12.6087 14.2175 11.5575 14.2175 10.2609C14.2175 8.96421 13.1663 7.91306 11.8697 7.91306C10.573 7.91306 9.52185 8.96421 9.52185 10.2609C9.52185 11.5575 10.573 12.6087 11.8697 12.6087Z"
stroke="#39393C"
stroke-width="1.7647"
stroke-miterlimit="10"
stroke-linejoin="round"
/>
</svg>
<strong>Address:</strong>
</p>
<p>
[Address Line 1], [Address Line 2], [City], [Province],
[Postal code]
</p>
<p style={{ marginTop: "15px" }}>
<svg
width="20"
height="21"
viewBox="0 0 24 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.0055 24.9891C8.80121 24.9891 5.78973 23.7416 3.52447 21.4755C-0.178409 17.7735 -1.05829 12.1592 1.33633 7.50703C1.55309 7.08637 2.06885 6.92102 2.49037 7.13692C2.9119 7.35282 3.07725 7.86944 2.86049 8.29096C0.807713 12.2783 1.56165 17.0898 4.73592 20.2641C6.67732 22.2063 9.25872 23.2756 12.0055 23.2756C14.7514 23.2756 17.3336 22.2063 19.275 20.2641C21.2164 18.3218 22.2865 15.7404 22.2865 12.9945C22.2865 10.2478 21.2173 7.66639 19.275 5.72499C17.3328 3.78359 14.7522 2.7135 12.0055 2.7135C9.25872 2.7135 6.67732 3.78359 4.73592 5.72499C4.40093 6.05998 3.85946 6.05998 3.52447 5.72499C3.18948 5.39 3.18948 4.84853 3.52447 4.51354C5.78973 2.24829 8.80121 1 12.0055 1C15.2097 1 18.2221 2.24829 20.4865 4.51354C22.7526 6.77879 24 9.79028 24 12.9945C24 16.1979 22.7526 19.2103 20.4865 21.4755C18.2221 23.7416 15.2097 24.9891 12.0055 24.9891Z"
fill="black"
/>
<path
d="M16.2885 17.2783C16.1008 17.2783 15.9123 17.2175 15.7538 17.0907L11.4701 13.6636C11.267 13.5009 11.1488 13.255 11.1488 12.9945V6.1405C11.1488 5.66758 11.5326 5.28375 12.0056 5.28375C12.4785 5.28375 12.8623 5.66758 12.8623 6.1405V12.5833L16.8248 15.7524C17.194 16.0488 17.254 16.5877 16.9584 16.957C16.7888 17.1678 16.5403 17.2783 16.2885 17.2783Z"
fill="black"
/>
</svg>
<strong>Business Hours:</strong>
</p>
<div className="contact-card-v3-business-hours">
<div>
<span>Monday</span> <span>10:00 AM - 08:00 PM</span>
</div>
<div>
<span>Tuesday</span> <span>10:00 AM - 08:00 PM</span>
</div>
<div>
<span>Wednesday</span> <span>10:00 AM - 08:00 PM</span>
</div>
<div>
<span>Thursday</span> <span>10:00 AM - 08:00 PM</span>
</div>
<div>
<span>Friday</span> <span>10:00 AM - 08:00 PM</span>
</div>
<div>
<span>Saturday</span> <span>10:00 AM - 08:00 PM</span>
</div>
<div>
<span>Sunday</span> <span>10:00 AM - 08:00 PM</span>
</div>
</div>
</div>
</div>
);
default:
return (
<div className="contact-card-default">
<h3>Default Contact Card</h3>
<p>Please select a version from the right panel.</p>
</div>
);
}
};
return (
<>
{/* Panel derecho (InspectorControls) */}
<InspectorControls>
<PanelBody title={__("Card Type", "sage")} initialOpen={true}>
<SelectControl
label={__("Select Card Version", "sage")}
value={version}
options={[
{ label: "Version 1", value: "1" },
{ label: "Version 2", value: "2" },
{ label: "Version 3", value: "3" },
]}
onChange={onChangeVersion}
/>
</PanelBody>
{version === "1" && (
<PanelBody title={__("Card Image", "sage")} initialOpen={true}>
<MediaUploadCheck>
<MediaUpload
onSelect={onSelectImage}
allowedTypes={["image"]}
value={imageId}
render={({ open }) => (
<div>
{!imageUrl && (
<button className="button" onClick={open}>
Upload Image
</button>
)}
{imageUrl && (
<div style={{ textAlign: "center" }}>
<img
src={imageUrl}
alt="Card Image"
style={{ maxWidth: "100%", height: "auto" }}
/>
<div>
<button className="button" onClick={open}>
Change Image
</button>
<button
className="button"
onClick={onRemoveImage}
style={{ marginLeft: "8px" }}
>
Delete Image
</button>
</div>
</div>
)}
</div>
)}
/>
</MediaUploadCheck>
</PanelBody>
)}
</InspectorControls>
{/* Vista del bloque */}
<div className="leadbox-contact-us-block">
{renderBlockContent()}
<div className="block">
<div>Configure this block on the right panel.</div>
</div>
</div>
</>
);
},
save: () => null,
});
registerBlockType("sage/lbx-component", {
title: __("LBX Component", "sage"),
category: "leadbox",
icon: "admin-settings",
attributes: {
accordions: {
type: "array",
default: [{ label: "LBX Component" }],
},
fields: {
type: "string",
default: JSON.stringify([{ component: "", version: "" }]),
},
},
edit: ({ attributes, setAttributes }) => {
const fields = JSON.parse(attributes.fields);
const shortcode = `[lbx-component]`;
return (
<div className="leadbox-component-block">
<div className="block">
<h3>Component shortcode</h3>
<div>
This block will render on the frontend as a graphical LBX
component generated by the following shortcode:
</div>
<pre>
<PlainText className="shortcode" value={shortcode} />
</pre>
<label className="lbx-version-control-label">Version Control</label>
{fields.map((field, index) => (
<div className="version-container" key={index}>
<PlainText
placeholder="Component"
value={field.component}
onChange={(newComponent) => {
const newFields = fields.map((f, i) => {
if (i === index) {
return { ...f, component: newComponent };
}
return f;
});
setAttributes({ fields: JSON.stringify(newFields) });
}}
/>
<PlainText
placeholder="Version"
value={field.version}
onChange={(newVersion) => {
const newFields = fields.map((f, i) => {
if (i === index) {
return { ...f, version: newVersion };
}
return f;
});
setAttributes({ fields: JSON.stringify(newFields) });
}}
/>
<button
className="remove-field-button"
onClick={() => {
const newFields = fields.filter((f, i) => i !== index);
setAttributes({ fields: JSON.stringify(newFields) });
}}
>
✖
</button>
</div>
))}
<button
className="add-field-button"
onClick={() => {
const newFields = [...fields, { component: "", version: "" }];
setAttributes({ fields: JSON.stringify(newFields) });
}}
>
Add Fields
</button>
</div>
</div>
);
},
save: ({ attributes }) => {
return null;
const fields = JSON.parse(attributes.fields);
return (
<div>
<p>{attributes.template}</p>
{fields.map((field, index) => (
<div key={index}>
<p>{field.component}</p>
<p>{field.version}</p>
</div>
))}
</div>
);
},
//save: () => <InnerBlocks.Content />,
});
registerBlockType("sage/dealer-name", {
title: __("dealer-name", "sage"),
category: "leadbox",
icon: "admin-users",
attributes: {
accordions: {
type: "array",
default: [{ label: "Dealer Name" }],
},
},
edit: () => {
const shortcode = `[dealer-name]`;
return (
<div className="leadbox-showroom-block">
<div className="block">
<h3>Dealer Name</h3>
<pre>
<PlainText className="shortcode" value={shortcode} />
</pre>
</div>
</div>
);
},
save: () => <InnerBlocks.Content />,
});
registerBlockType("sage/dealer-phone", {
title: __("Dealer Phone", "sage"),
category: "leadbox",
icon: "phone",
attributes: {
version: {
type: "string",
default: "1", // Versión por defecto
},
},
edit: ({ attributes, setAttributes }) => {
return (
<div className="leadbox-component-block">
<div className="block">
<h3>Dealer Phone</h3>
<div>
This block will render on the frontend as a graphical LBX
component generated by the following shortcode:
</div>
<pre>
<code>[dealer-phone]</code>
</pre>
{/* Selector de versión */}
<div className="version-container">
<SelectControl
label="Version"
value={attributes.version}
options={[
{ label: "Version 1", value: "1" },
{ label: "Version 2", value: "2" },
]}
onChange={(newVersion) =>
setAttributes({ version: newVersion })
}
/>
</div>
</div>
</div>
);
},
save: () => null, // Bloque dinámico: se renderiza en PHP
});
registerBlockType("sage/dealer-address", {
title: __("Dealer Address", "sage"),
category: "leadbox",
icon: "admin-home",
attributes: {
version: {
type: "string",
default: "1", // Versión por defecto
},
},
edit: ({ attributes, setAttributes }) => {
return (
<div className="leadbox-component-block">
<div className="block">
<h3>Dealer Address</h3>
<div>
This block will render on the frontend as a graphical LBX
component generated by the following shortcode:
</div>
<pre>
<code>[dealer-address]</code>
</pre>
{/* Selector de versión */}
<div className="version-container">
<SelectControl
label="Version"
value={attributes.version}
options={[
{ label: "Version 1", value: "1" },
{ label: "Version 2", value: "2" },
]}
onChange={(newVersion) =>
setAttributes({ version: newVersion })
}
/>
</div>
</div>
</div>
);
},
save: () => null, // Bloque dinámico: se renderiza en PHP
});
registerBlockType("sage/dealer-social", {
title: __("Dealer Social Networks", "sage"),
category: "leadbox",
icon: "rss",
attributes: {
version: {
type: "string",
default: "1", // Versión por defecto
},
},
edit: ({ attributes, setAttributes }) => {
return (
<div className="leadbox-component-block">
<div className="block">
<h3>Dealer Social Networks</h3>
<div>
This block will render on the frontend as a graphical LBX
component generated by the following shortcode:
</div>
<pre>
<code>[dealer-social]</code>
</pre>
{/* Selector de versión */}
<div className="version-container">
<SelectControl
label="Version"
value={attributes.version}
options={[
{ label: "Version 1 (B/W)", value: "1" },
{ label: "Version 2 (Color)", value: "2" },
]}
onChange={(newVersion) =>
setAttributes({ version: newVersion })
}
/>
</div>
</div>
</div>
);
},
save: () => null, // Bloque dinámico: se renderiza en PHP
});
registerBlockType("sage/dealer-map", {
title: __("Dealer Map frame", "sage"),
category: "leadbox",
icon: "location-alt",
attributes: {
version: {
type: "string",
default: "1", // Versión por defecto
},
},
edit: ({ attributes, setAttributes }) => {
return (
<div className="leadbox-component-block">
<div className="block">
<h3>Dealer Map frame</h3>
<div>
This block will render on the frontend as a graphical LBX
component generated by the following shortcode:
</div>
<pre>
<code>[dealer-map]</code>
</pre>
{/* Selector de versión */}
<div className="version-container">
<SelectControl
label="Version"
value={attributes.version}
options={[
{ label: "Version 1 only map", value: "1" },
{ label: "Version 2", value: "2" },
]}
onChange={(newVersion) =>
setAttributes({ version: newVersion })
}
/>
</div>
</div>
</div>
);
},
save: () => null, // Bloque dinámico: se renderiza en PHP
});
registerBlockType("sage/dealer-hours", {
title: __("Dealer Service Hours", "sage"),
category: "leadbox",
icon: "hourglass",
attributes: {
version: {
type: "string",
default: "1", // Versión por defecto
},
},
edit: ({ attributes, setAttributes }) => {
return (
<div className="leadbox-component-block">
<div className="block">
<h3>Dealer Service Hours</h3>
<div>
This block will render on the frontend as a graphical LBX
component generated by the following shortcode:
</div>
<pre>
<code>[dealer-hours]</code>
</pre>
{/* Selector de versión */}
<div className="version-container">
<SelectControl
label="Version"
value={attributes.version}
options={[
{ label: "Version 1", value: "1" },
{ label: "Version 2", value: "2" },
{ label: "Version 3", value: "3" },
{ label: "Version 4", value: "4" },
]}
onChange={(newVersion) =>
setAttributes({ version: newVersion })
}
/>
</div>
</div>
</div>
);
},
save: () => null, // Bloque dinámico: se renderiza en PHP
});
registerBlockType("sage/special-offers", {
title: __("Special Offers", "sage"),
category: "leadbox",
icon: "text-page",
attributes: {
version: {
type: "string",
default: "1",
},
},
edit: (props) => {
const { attributes, setAttributes } = props;
const { version } = attributes;
const onChangeVersion = (newVersion) => {
setAttributes({
version: newVersion,
});
};
const renderBlockContent = () => {
switch (version) {
case "1":
return (
<div className="container special-offer-1">
<div className="columns-container">
<div className="column image-column">
<figure>
<img
src="http://wptraining.kinsta.cloud/wp-content/uploads/2025/02/24_frd_bro_wdtk_4d_oxwh_ps34-scaled.jpeg"
alt="2024 Bronco"
width="400"
/>
</figure>
</div>
<div className="column content-column">
<h2 className="title">2024 Bronco</h2>
<div className="row rebate-row">
<div className="col">
<p>Get a Rebate of $6,000</p>
<p>On all 2024 Bronco models</p>
</div>
</div>
<div className="button-group">
<a href="#" className="button">
View Inventory
</a>
<a href="#" className="button">
Offer Details
</a>
</div>
</div>
</div>
<div className="columns-container">
<div className="column image-column">
<figure>
<img
src="http://wptraining.kinsta.cloud/wp-content/uploads/2025/02/24_frd_bro_wdtk_4d_oxwh_ps34-scaled.jpeg"
alt="2024 Bronco"
width="400"
/>
</figure>
</div>
<div className="column content-column">
<h2 className="title">2024 Bronco</h2>
<div className="row rebate-row">
<div className="col">
<p>Get a Rebate of $6,000</p>
<p>On all 2024 Bronco models</p>
</div>
</div>
<div className="button-group">
<a href="#" className="button">
View Inventory
</a>
<a href="#" className="button">
Offer Details
</a>
</div>
</div>
</div>
<div className="columns-container">
<div className="column image-column">
<figure>
<img
src="http://wptraining.kinsta.cloud/wp-content/uploads/2025/02/24_frd_bro_wdtk_4d_oxwh_ps34-scaled.jpeg"
alt="2024 Bronco"
width="400"
/>
</figure>
</div>
<div className="column content-column">
<h2 className="title">2024 Bronco</h2>
<div className="row rebate-row">
<div className="col">
<p>Get a Rebate of $6,000</p>
<p>On all 2024 Bronco models</p>
</div>
</div>
<div className="button-group">
<a href="#" className="button">
View Inventory
</a>
<a href="#" className="button">
Offer Details
</a>
</div>
</div>
</div>
</div>
);
case "2":
return (
<div className="contact-card-v2">
<h2 className="contact-card-v2-title">Contact Us</h2>
<div className="contact-card-v2-address">
<p>
<strong>[Address Line 1], [Address Line 2],</strong>
</p>
<p>
<strong>[City], [Province], [Postal code]</strong>
</p>
<p>
<strong>General:</strong> 000-000-0000
</p>
</div>
</div>
);
default:
return (
<div className="contact-card-default">
<h3>Default Special Offer</h3>
<p>Please select a version from the right panel.</p>
</div>
);
}
};
return (
<>
{/* Panel derecho (InspectorControls) */}
<InspectorControls>
<PanelBody title={__("Template Type", "sage")} initialOpen={true}>
<SelectControl
label={__("Select Version", "sage")}
value={version}
options={[
{ label: "Version 1", value: "1" },
{ label: "Version 2", value: "2" },
]}
onChange={onChangeVersion}
/>
</PanelBody>
</InspectorControls>
{/* Vista del bloque */}
<div className="leadbox-special-offers-block">
{renderBlockContent()}
<div className="block">
<div>Configure this block on the right panel.</div>
</div>
</div>
</>
);
},
save: () => null,
});
registerBlockType("sage/open-today-line", {
title: __("Open Today Line", "sage"),
category: "leadbox",
icon: "store",
attributes: {
department: {
type: "string",
default: "sales",
},
phone: {
type: "string",
default: "sales",
},
},
edit: (props) => {
const { attributes, setAttributes } = props;
const { department, phone } = attributes;
const onChangeDepartment = (newDepartment) => {
setAttributes({
department: newDepartment,
});
};
const onChangePhone = (newPhone) => {
setAttributes({
phone: newPhone,
});
}
return (
<>
{/* Panel derecho (InspectorControls) */}
<InspectorControls>
<PanelBody title={__("Department", "sage")} initialOpen={true}>
<SelectControl
label={__("Select Department", "sage")}
value={department}
options={[
{ label: "Sales", value: "sales" },
{ label: "Service", value: "service" },
{ label: "Parts", value: "parts" },
{ label: "Additional Hours 1", value: "additional_1" },
{ label: "Additional Hours 2", value: "additional_2" },
{ label: "Additional Hours 3", value: "additional_3" },
]}
onChange={onChangeDepartment}
/>
</PanelBody>
<PanelBody title={__("Phone", "sage")} initialOpen={true}>
<SelectControl
label={__("Select Phone", "sage")}
value={phone}
options={[
{ label: "Sales Phone", value: "sales" },
{ label: "Service Phone", value: "service" },
{ label: "Parts Phone", value: "parts" },
{ label: "Fax Phone", value: "fax" },
{ label: "Body Shop Phone", value: "body_shop" },
{ label: "Collision Phone", value: "collision" },
{ label: "Accessories Phone", value: "accessories" },
{ label: "SMS Phone", value: "sms" },
]}
onChange={onChangePhone}
/>
</PanelBody>
</InspectorControls>
{/* Vista del bloque */}
<div className="leadbox-open-today-block">
<div className="block-container">
<h3>Open Today Line</h3>
<p><b>It will render a line like this:</b></p>
<p><i>Open Today <b>[DEPARTMENT]</b>: 8:00am – 5:00pm | Call us at: <b>[PHONE]</b></i></p>
</div>
<div className="block">
<div>Configure this block on the right panel.</div>
</div>
</div>
</>
);
},
save: () => null,
});
registerBlockType("sage/call-button", {
title: __("Call Button", "sage"),
category: "leadbox",
icon: "phone",
attributes: {
label: {
type: "string",
default: "",
},
phone: {
type: "string",
default: "sales",
},
},
edit: (props) => {
const { attributes, setAttributes } = props;
const { label, phone } = attributes;
const onChangeLabel = (newLabel) => {
setAttributes({
label: newLabel,
});
};
const onChangePhone = (newPhone) => {
setAttributes({
phone: newPhone,
});
}
return (
<>
{/* Panel derecho (InspectorControls) */}
<InspectorControls>
<PanelBody title={__("Button Label", "sage")} initialOpen={true}>
<TextControl
label={__("Button Label", "sage")}
value={label}
onChange={onChangeLabel}
help={__("Enter the label to display", "sage")}
/>
</PanelBody>
<PanelBody title={__("Phone", "sage")} initialOpen={true}>
<SelectControl
label={__("Select Phone", "sage")}
value={phone}
options={[
{ label: "Sales Phone", value: "sales" },
{ label: "Service Phone", value: "service" },
{ label: "Parts Phone", value: "parts" },
{ label: "Fax Phone", value: "fax" },
{ label: "Body Shop Phone", value: "body_shop" },
{ label: "Collision Phone", value: "collision" },
{ label: "Accessories Phone", value: "accessories" },
{ label: "SMS", value: "sms" },
]}
onChange={onChangePhone}
/>
</PanelBody>
</InspectorControls>
{/* Vista del bloque */}
<div className="leadbox-open-today-block">
<div className="block-container">
<h3>Call Button</h3>
</div>
<div className="block">
<div>Configure this block on the right panel.</div>
</div>
</div>
</>
);
},
save: () => null,
});
registerBlockType("sage/offers-by-categories", {
title: __("Offers By Categories", "sage"),
category: "leadbox",
icon: "portfolio",
attributes: {
category: {
type: "string",
default: "",
},
},
edit: (props) => {
const { attributes, setAttributes } = props;
const { category } = attributes;
const terms = useSelect((select) => {
return select("core").getEntityRecords("taxonomy", "offer_category", {
per_page: -1,
});
}, []);
const options = terms
? [
{ label: __("— Select a category —", "sage"), value: "" },
...terms.map((term) => ({
label: term.name,
value: term.slug,
})),
]
: [{ label: __("Loading...", "sage"), value: "" }];
return (
<>
<InspectorControls>
<PanelBody title={__("Select Category", "sage")} initialOpen={true}>
<SelectControl
label={__("Category", "sage")}
value={category}
options={options}
onChange={(newCategory) =>
setAttributes({ category: newCategory })
}
/>
</PanelBody>
</InspectorControls>
<div className="leadbox-open-today-block">
<div className="block-container">
<h3>Offers By Categories</h3>
<p>{category ? `Selected: ${category}` : "No category selected"}</p>
</div>
<div className="block">
<div>Configure this block on the right panel.</div>
</div>
</div>
</>
);
},
save: () => null,
});
// ─── Offers Slider Block ───────────────────────────────────────────
const { ToggleControl, RangeControl, ColorPalette, BaseControl } = wp.components;
registerBlockType("sage/lbx-slider", {
title: __("LBX Slider", "sage"),
category: "leadbox",
icon: "slides",
description: __("Display offers in a slider/carousel powered by Swiper.", "sage"),
attributes: {
sliderId: {
type: "string",
default: "",
},
layout: {
type: "string",
default: "",
},
columns: {
type: "number",
default: 3,
},
mediaColumnsMobile: {
type: "number",
default: 0,
},
mediaColumnsTablet: {
type: "number",
default: 0,
},
mediaColumnsDesktop: {
type: "number",
default: 0,
},
carouselColumnsMobile: {
type: "number",
default: 0,
},
carouselColumnsTablet: {
type: "number",
default: 0,
},
carouselColumnsDesktop: {
type: "number",
default: 0,
},
showArrows: {
type: "boolean",
default: true,
},
showPagination: {
type: "boolean",
default: true,
},
autoplay: {
type: "boolean",
default: false,
},
autoplaySpeed: {
type: "number",
default: 5000,
},
loop: {
type: "boolean",
default: false,
},
spaceBetween: {
type: "number",
default: 20,
},
gradientColor: {
type: "string",
default: "",
},
sliderHeight: {
type: "number",
default: 0,
},
showViewInventory: {
type: "boolean",
},
arrowStyle: { type: "string", default: "circle" },
arrowColor: { type: "string", default: "" },
arrowOpacity: { type: "number", default: 90 },
paginationColor: { type: "string", default: "" },
countdownEndDate: { type: "string", default: "" },
countdownStartDate: { type: "string", default: "" },
countdownStyle: { type: "string", default: "flip" },
countdownExpiredMessage: { type: "string", default: "" },
countdownBgImage: { type: "string", default: "" },
countdownBgImageId: { type: "number", default: 0 },
countdownOverlayOpacity: { type: "number", default: 60 },
contentAlign: { type: "string", default: "" },
transition: { type: "string", default: "" },
},
edit: (props) => {
const { attributes, setAttributes } = props;
const {
sliderId,
layout,
columns,
mediaColumnsMobile,
mediaColumnsTablet,
mediaColumnsDesktop,
carouselColumnsMobile,
carouselColumnsTablet,
carouselColumnsDesktop,
showArrows,
showPagination,
autoplay,
autoplaySpeed,
loop,
spaceBetween,
gradientColor,
sliderHeight,
showViewInventory,
arrowStyle,
arrowColor,
arrowOpacity,
paginationColor,
countdownEndDate,
countdownStartDate,
countdownStyle,
countdownExpiredMessage,
countdownBgImage,
countdownBgImageId,
countdownOverlayOpacity,
contentAlign,
transition: transitionEffect,
} = attributes;
const { useState, useEffect, useCallback, useRef } = wp.element;
const [minervaOffers, setMinervaOffers] = useState(null);
const [minervaLoading, setMinervaLoading] = useState(false);
const [previewSlide, setPreviewSlide] = useState(0);
const [customEditSlideIndex, setCustomEditSlideIndex] = useState(0);
const [mediaColumnsEditKey, setMediaColumnsEditKey] = useState("");
const [slideAligns, setSlideAligns] = useState({});
const [slideImageLinks, setSlideImageLinks] = useState({});
const [slideButtonLabels, setSlideButtonLabels] = useState({});
const [slideButtonUrls, setSlideButtonUrls] = useState({});
const [slideButtonTargets, setSlideButtonTargets] = useState({});
const [customSlideLinks, setCustomSlideLinks] = useState({});
const [customSlideAligns, setCustomSlideAligns] = useState({});
const [countdownImageLink, setCountdownImageLink] = useState("");
// Real-render preview (iframe of the actual front-end output)
const [previewUrl, setPreviewUrl] = useState("");
const [previewHeight, setPreviewHeight] = useState(420);
const [previewReload, setPreviewReload] = useState(0);
const [previewLang, setPreviewLang] = useState("en");
// Mutable copies — entity records are frozen so direct mutation is silently ignored
const slideSettingsRef = useRef({});
const customSlidesRef = useRef([]);
// Fetch available sliders from the plugin option via REST
const sliders = useSelect((select) => {
return select("core").getEntityRecord("root", "site");
}, []);
const sliderOptions = sliders && sliders.lbx_sliders
? [
{ label: __("— Select a slider —", "sage"), value: "" },
...Object.entries(sliders.lbx_sliders).map(([id, slider]) => ({
label: slider.name + (slider.enabled ? "" : " (" + __("Disabled", "sage") + ")"),
value: id,
})),
]
: [{ label: __("Loading sliders...", "sage"), value: "" }];
const selectedSlider = sliders && sliders.lbx_sliders && sliderId
? sliders.lbx_sliders[sliderId]
: null;
const selectedSource = selectedSlider ? (selectedSlider.source || "minerva") : null;
// Resolved values: Gutenberg attribute (if set) → admin setting → hardcoded default
const effectiveLayout = layout || (selectedSlider && selectedSlider.layout) || "carousel";
const effectiveTransition = transitionEffect || (selectedSlider && selectedSlider.transition) || "slide";
const effectiveContentAlign = contentAlign || (selectedSlider && selectedSlider.content_align) || "left";
const effectiveSliderHeight = (() => {
if (sliderHeight && Number(sliderHeight) > 0) return Number(sliderHeight);
const adminHeight = selectedSlider && selectedSlider.slider_height ? parseInt(selectedSlider.slider_height, 10) : 0;
return adminHeight > 0 ? adminHeight : 480;
})();
const effectiveMediaColumnsMobile = (mediaColumnsMobile && mediaColumnsMobile > 0) ? mediaColumnsMobile : (selectedSlider && selectedSlider.media_columns_mobile ? parseInt(selectedSlider.media_columns_mobile, 10) : 1);
const effectiveMediaColumnsTablet = (mediaColumnsTablet && mediaColumnsTablet > 0) ? mediaColumnsTablet : (selectedSlider && selectedSlider.media_columns_tablet ? parseInt(selectedSlider.media_columns_tablet, 10) : 2);
const effectiveMediaColumnsDesktop = (mediaColumnsDesktop && mediaColumnsDesktop > 0) ? mediaColumnsDesktop : (selectedSlider && selectedSlider.media_columns_desktop ? parseInt(selectedSlider.media_columns_desktop, 10) : 3);
const effectiveCarouselColumnsMobile = (carouselColumnsMobile && carouselColumnsMobile > 0) ? carouselColumnsMobile : (selectedSlider && selectedSlider.carousel_columns_mobile ? parseInt(selectedSlider.carousel_columns_mobile, 10) : 1);
const effectiveCarouselColumnsTablet = (carouselColumnsTablet && carouselColumnsTablet > 0) ? carouselColumnsTablet : (selectedSlider && selectedSlider.carousel_columns_tablet ? parseInt(selectedSlider.carousel_columns_tablet, 10) : 2);
const effectiveCarouselColumnsDesktop = (carouselColumnsDesktop && carouselColumnsDesktop > 0)
? carouselColumnsDesktop
: (selectedSlider && selectedSlider.carousel_columns_desktop
? parseInt(selectedSlider.carousel_columns_desktop, 10)
: ((columns && columns > 0) ? columns : 3));
const effectiveShowViewInventory = (typeof showViewInventory === "boolean")
? showViewInventory
: !!(selectedSlider && selectedSlider.show_view_inventory);
// Auto-force layout based on source type
useEffect(() => {
if (!selectedSource) return;
if (selectedSource === "countdown" && effectiveLayout !== "countdown") {
setAttributes({ layout: "countdown" });
} else if (selectedSource !== "countdown" && effectiveLayout === "countdown") {
setAttributes({ layout: "" });
}
}, [selectedSource]);
// Build layout options based on source type
const layoutOptions = (() => {
if (selectedSource === "countdown") {
return [{ label: __("Countdown Timer", "sage"), value: "countdown" }];
}
return [
{ label: __("Carousel", "sage"), value: "carousel" },
{ label: __("Hero Banner", "sage"), value: "offers-template" },
{ label: __("Split Panel", "sage"), value: "split" },
{ label: __("Media Columns", "sage"), value: "media-columns" },
];
})();
// Pre-populate countdown fields from admin config when selecting a countdown slider
useEffect(() => {
if (!selectedSlider || selectedSlider.source !== "countdown") return;
const updates = {};
if (!countdownEndDate && selectedSlider.countdown_end_date) updates.countdownEndDate = selectedSlider.countdown_end_date;
if (!countdownStartDate && selectedSlider.countdown_start_date) updates.countdownStartDate = selectedSlider.countdown_start_date;
if (!countdownStyle && selectedSlider.countdown_style) updates.countdownStyle = selectedSlider.countdown_style;
if (!countdownExpiredMessage && selectedSlider.countdown_expired_msg) updates.countdownExpiredMessage = selectedSlider.countdown_expired_msg;
if (!countdownBgImage && selectedSlider.countdown_bg_image) updates.countdownBgImage = selectedSlider.countdown_bg_image;
if (Object.keys(updates).length > 0) setAttributes(updates);
}, [sliderId, selectedSource]);
// Sync per-slide alignments from admin slider settings
useEffect(() => {
if (!selectedSlider || !selectedSlider.slide_settings) {
setSlideAligns({});
return;
}
const aligns = {};
Object.entries(selectedSlider.slide_settings).forEach(([key, ss]) => {
if (ss.align) aligns[key] = ss.align;
});
setSlideAligns(aligns);
}, [sliderId, selectedSlider && selectedSlider.slide_settings]);
// Sync image link local state and mutable refs from admin slider settings on slider change
useEffect(() => {
if (!selectedSlider) {
setSlideImageLinks({});
setSlideButtonLabels({});
setSlideButtonUrls({});
setSlideButtonTargets({});
setCustomSlideLinks({});
setCustomSlideAligns({});
setCountdownImageLink("");
slideSettingsRef.current = {};
customSlidesRef.current = [];
return;
}
// Deep-copy into mutable refs so saves accumulate correctly across keystrokes
slideSettingsRef.current = JSON.parse(JSON.stringify(selectedSlider.slide_settings || {}));
customSlidesRef.current = JSON.parse(JSON.stringify(selectedSlider.custom_slides || []));
const oemLinks = {};
Object.entries(selectedSlider.slide_settings || {}).forEach(([key, ss]) => {
if (ss.image_link_url) oemLinks[key] = ss.image_link_url;
});
setSlideImageLinks(oemLinks);
const oemBtnLabels = {};
const oemBtnUrls = {};
const oemBtnTargets = {};
Object.entries(selectedSlider.slide_settings || {}).forEach(([key, ss]) => {
// New format: read from ctas[0]
if (Array.isArray(ss.ctas) && ss.ctas[0]) {
if (ss.ctas[0].label) oemBtnLabels[key] = ss.ctas[0].label;
if (ss.ctas[0].url) oemBtnUrls[key] = ss.ctas[0].url;
if (ss.ctas[0].target) oemBtnTargets[key] = ss.ctas[0].target;
} else {
// Legacy fallback
if (ss.cta_label) oemBtnLabels[key] = ss.cta_label;
if (ss.cta_url) oemBtnUrls[key] = ss.cta_url;
if (ss.cta_target) oemBtnTargets[key] = ss.cta_target;
}
});
setSlideButtonLabels(oemBtnLabels);
setSlideButtonUrls(oemBtnUrls);
setSlideButtonTargets(oemBtnTargets);
const cLinks = {};
const cAligns = {};
(selectedSlider.custom_slides || []).forEach((s, i) => {
cLinks[i] = s.image_link_url || "";
cAligns[i] = s.align || "";
});
setCustomSlideLinks(cLinks);
setCustomSlideAligns(cAligns);
setCountdownImageLink(selectedSlider.countdown_image_link_url || "");
}, [sliderId, selectedSlider]);
useEffect(() => {
if (!selectedSlider || selectedSlider.source !== "custom") {
setCustomEditSlideIndex(0);
return;
}
const total = (selectedSlider.custom_slides || []).length;
if (customEditSlideIndex >= total) setCustomEditSlideIndex(0);
}, [sliderId, selectedSlider, customEditSlideIndex]);
// Reset preview position when context changes to avoid stale indices.
useEffect(() => {
setPreviewSlide(0);
if (selectedSource !== "custom") setCustomEditSlideIndex(0);
}, [sliderId, effectiveLayout, selectedSource]);
// Keep "Edit Slide" selector synced when preview changes via arrows/dots.
useEffect(() => {
if (!selectedSlider || selectedSlider.source !== "custom") return;
setCustomEditSlideIndex(previewSlide);
}, [previewSlide, selectedSlider && selectedSlider.source]);
// Helper: get offer key for slide_settings lookup
const getOfferKey = useCallback((offer) => {
if (!offer) return "";
return [offer.year, offer.make, offer.model].filter(Boolean).join("_").toLowerCase().replace(/\s+/g, "-");
}, []);
// Current preview slide alignment
const currentOffer = minervaOffers && minervaOffers.length > 0 ? minervaOffers[Math.min(previewSlide, minervaOffers.length - 1)] : null;
const currentSlideKey = getOfferKey(currentOffer);
const currentSlideAlign = currentSlideKey && slideAligns[currentSlideKey] ? slideAligns[currentSlideKey] : effectiveContentAlign;
const isMediaColumnsLayout = effectiveLayout === "media-columns";
const oemSlideOptions = (minervaOffers || []).map((offer, index) => {
const key = getOfferKey(offer);
return {
label: `${index + 1}. ${`${offer.year || ""} ${offer.make || ""} ${offer.model || ""}`.trim() || key}`,
value: key,
};
}).filter((opt) => !!opt.value);
useEffect(() => {
if (!selectedSlider || selectedSlider.source !== "minerva" || !oemSlideOptions.length) return;
const exists = oemSlideOptions.some((opt) => opt.value === mediaColumnsEditKey);
if (!exists) setMediaColumnsEditKey(oemSlideOptions[0].value);
}, [selectedSlider && selectedSlider.source, mediaColumnsEditKey, minervaOffers]);
useEffect(() => {
if (!selectedSlider || selectedSlider.source !== "minerva" || !currentSlideKey) return;
setMediaColumnsEditKey(currentSlideKey);
}, [selectedSlider && selectedSlider.source, currentSlideKey]);
const editingSlideKey = (selectedSlider && selectedSlider.source === "minerva" && mediaColumnsEditKey) ? mediaColumnsEditKey : currentSlideKey;
const editingSlideAlign = editingSlideKey && slideAligns[editingSlideKey] ? slideAligns[editingSlideKey] : effectiveContentAlign;
// Save any per-slide field to admin via AJAX
const saveSlideField = useCallback((key, field, value) => {
if (!selectedSlider || !sliderId) return;
if (field === "align") setSlideAligns((prev) => ({ ...prev, [key]: value }));
if (field === "image_link_url") setSlideImageLinks((prev) => ({ ...prev, [key]: value }));
if (field === "cta_label") setSlideButtonLabels((prev) => ({ ...prev, [key]: value }));
if (field === "cta_url") setSlideButtonUrls((prev) => ({ ...prev, [key]: value }));
if (field === "cta_target") setSlideButtonTargets((prev) => ({ ...prev, [key]: value }));
const editorConfig = window.lbxSliderEditor || {};
const nonce = editorConfig.ajaxNonce || "";
// Use the mutable ref — entity records are frozen so direct mutation on selectedSlider is silently ignored
const allSettings = slideSettingsRef.current;
if (!allSettings[key]) allSettings[key] = {};
allSettings[key][field] = value;
// Mirror cta_label/url/target into ctas[0] for the new format
if (field === "cta_label" || field === "cta_url" || field === "cta_target") {
if (!Array.isArray(allSettings[key].ctas) || !allSettings[key].ctas.length) {
allSettings[key].ctas = [{ label: "", url: "", target: "_self", color: "", position: "inline" }];
}
if (field === "cta_label") allSettings[key].ctas[0].label = value;
if (field === "cta_url") allSettings[key].ctas[0].url = value;
if (field === "cta_target") allSettings[key].ctas[0].target = value;
}
const fd = new FormData();
fd.append("action", "lbx_save_slider");
fd.append("nonce", nonce);
fd.append("slider_id", sliderId);
fd.append("slider_name", selectedSlider.name || "");
fd.append("slider_source", selectedSlider.source || "minerva");
fd.append("slide_settings", JSON.stringify(allSettings));
fd.append("_partial", "slide_settings");
fetch(window.ajaxurl, { method: "POST", body: fd, credentials: "same-origin" });
}, [selectedSlider, sliderId]);
const saveSlideAlign = useCallback((key, val) => saveSlideField(key, "align", val), [saveSlideField]);
// Save a custom slide field (by index) via AJAX
const saveCustomSlideField = useCallback((index, field, value) => {
if (!selectedSlider || !sliderId) return;
// Use the mutable ref — entity records are frozen so direct mutation on selectedSlider is silently ignored
const slides = customSlidesRef.current;
if (!slides[index]) return;
slides[index][field] = value;
if (field === "image_link_url") setCustomSlideLinks((prev) => ({ ...prev, [index]: value }));
if (field === "align") setCustomSlideAligns((prev) => ({ ...prev, [index]: value }));
const editorConfig = window.lbxSliderEditor || {};
const fd = new FormData();
fd.append("action", "lbx_save_slider");
fd.append("nonce", editorConfig.ajaxNonce || "");
fd.append("slider_id", sliderId);
fd.append("_partial", "custom_slides");
fd.append("custom_slides", JSON.stringify(slides));
fetch(window.ajaxurl, { method: "POST", body: fd, credentials: "same-origin" });
}, [selectedSlider, sliderId]);
// Save a top-level slider field via AJAX (e.g. countdown_image_link_url)
const saveSliderField = useCallback((fieldName, value) => {
if (!selectedSlider || !sliderId) return;
if (fieldName === "countdown_image_link_url") setCountdownImageLink(value);
const editorConfig = window.lbxSliderEditor || {};
const fd = new FormData();
fd.append("action", "lbx_save_slider");
fd.append("nonce", editorConfig.ajaxNonce || "");
fd.append("slider_id", sliderId);
fd.append("_partial", "slider_field");
fd.append("field_name", fieldName);
fd.append("field_value", value);
fetch(window.ajaxurl, { method: "POST", body: fd, credentials: "same-origin" });
}, [selectedSlider, sliderId]);
// Fetch Minerva offers for preview when source is minerva
useEffect(() => {
if (!selectedSlider || selectedSlider.source !== "minerva") {
setMinervaOffers(null);
return;
}
if (minervaOffers !== null) return; // already fetched
setMinervaLoading(true);
const editorConfig = window.lbxSliderEditor || {};
const url = editorConfig.restUrl || "/wp-json/lbx-slider/v1/minerva-preview";
wp.apiFetch({ path: "lbx-slider/v1/minerva-preview" })
.then((data) => {
setMinervaOffers(data.offers || []);
setMinervaLoading(false);
})
.catch(() => {
setMinervaOffers([]);
setMinervaLoading(false);
});
}, [selectedSlider ? selectedSlider.source : null]);
// Fetch the real-render preview URL for the selected slider
useEffect(() => {
if (!sliderId) {
setPreviewUrl("");
return;
}
let cancelled = false;
wp.apiFetch({ path: "lbx-slider/v1/preview-url?slider_id=" + encodeURIComponent(sliderId) })
.then((data) => {
if (!cancelled) setPreviewUrl((data && data.url) || "");
})
.catch(() => {
if (!cancelled) setPreviewUrl("");
});
return () => { cancelled = true; };
}, [sliderId]);
// Listen for height reports from the preview iframe to auto-size it
useEffect(() => {
const onMessage = (e) => {
const d = e && e.data;
if (d && typeof d === "object" && typeof d.lbxSliderPreviewHeight === "number") {
const h = Math.max(180, Math.round(d.lbxSliderPreviewHeight));
setPreviewHeight(h);
}
};
window.addEventListener("message", onMessage);
return () => window.removeEventListener("message", onMessage);
}, []);
// Refresh the preview iframe whenever the block (re)gains selection, so it always
// reflects the slider's latest saved content — the exact slides/images, every time.
useEffect(() => {
if (props.isSelected) setPreviewReload((k) => k + 1);
}, [props.isSelected]);
return (
<>
<InspectorControls>
<PanelBody title={__("Slider Source", "sage")} initialOpen={true}>
<SelectControl
label={__("Select Slider", "sage")}
value={sliderId}
options={sliderOptions}
onChange={(val) =>
setAttributes({
sliderId: val,
// Reset block-level overrides so the new slider inherits Admin settings.
layout: "",
transition: "",
contentAlign: "",
sliderHeight: 0,
mediaColumnsMobile: 0,
mediaColumnsTablet: 0,
mediaColumnsDesktop: 0,
carouselColumnsMobile: 0,
carouselColumnsTablet: 0,
carouselColumnsDesktop: 0,
showViewInventory: undefined,
})
}
help={__("Choose a slider configured in Leadbox Settings > Offers Slider.", "sage")}
/>
</PanelBody>
<PanelBody title={__("Slider Settings", "sage")} initialOpen={true}>
<SelectControl
label={__("Layout", "sage")}
value={effectiveLayout}
options={layoutOptions}
onChange={(val) => setAttributes({ layout: val })}
/>
{effectiveLayout !== "countdown" && (
<SelectControl
label={__("Transition Effect", "sage")}
value={effectiveTransition}
options={[
{ label: __("Slide (Default)", "sage"), value: "slide" },
{ label: __("Fade (Crossfade)", "sage"), value: "fade" },
{ label: __("Creative (3D Rotate)", "sage"), value: "creative" },
{ label: __("Flip (Card Flip)", "sage"), value: "flip" },
{ label: __("Coverflow (3D Depth)", "sage"), value: "coverflow" },
]}
onChange={(val) => setAttributes({ transition: val })}
help={["fade", "creative", "flip"].includes(effectiveTransition) ? __("This effect forces 1 slide per view.", "sage") : effectiveTransition === "coverflow" ? __("Centered 3D depth effect with shadows.", "sage") : ""}
/>
)}
{effectiveLayout !== "offers-template" && effectiveLayout !== "split" && effectiveLayout !== "countdown" && (effectiveLayout === "media-columns" || effectiveTransition === "slide" || effectiveTransition === "coverflow") && (
<>
{effectiveLayout === "media-columns" ? (
<>
<RangeControl
label={__("Slides Per View (Mobile <= 640px)", "sage")}
value={effectiveMediaColumnsMobile}
onChange={(val) => {
setAttributes({ mediaColumnsMobile: val });
saveSliderField("media_columns_mobile", String(val));
}}
min={1}
max={3}
/>
<RangeControl
label={__("Slides Per View (Tablet 641-1024px)", "sage")}
value={effectiveMediaColumnsTablet}
onChange={(val) => {
setAttributes({ mediaColumnsTablet: val });
saveSliderField("media_columns_tablet", String(val));
}}
min={1}
max={4}
/>
<RangeControl
label={__("Slides Per View (Desktop >= 1025px)", "sage")}
value={effectiveMediaColumnsDesktop}
onChange={(val) => {
setAttributes({ mediaColumnsDesktop: val });
saveSliderField("media_columns_desktop", String(val));
}}
min={1}
max={6}
/>
</>
) : (
<>
{effectiveLayout === "carousel" && effectiveTransition === "slide" && (
<>
<RangeControl
label={__("Carousel Slides (Mobile <= 640px)", "sage")}
value={effectiveCarouselColumnsMobile}
onChange={(val) => {
setAttributes({ carouselColumnsMobile: val });
saveSliderField("carousel_columns_mobile", String(val));
}}
min={1}
max={3}
/>
<RangeControl
label={__("Carousel Slides (Tablet 641-1024px)", "sage")}
value={effectiveCarouselColumnsTablet}
onChange={(val) => {
setAttributes({ carouselColumnsTablet: val });
saveSliderField("carousel_columns_tablet", String(val));
}}
min={1}
max={4}
/>
<RangeControl
label={__("Carousel Slides (Desktop >= 1025px)", "sage")}
value={effectiveCarouselColumnsDesktop}
onChange={(val) => {
setAttributes({
carouselColumnsDesktop: val,
columns: val,
});
saveSliderField("carousel_columns_desktop", String(val));
}}
min={1}
max={6}
/>
</>
)}
{!(effectiveLayout === "carousel" && effectiveTransition === "slide") && (
<RangeControl
label={__("Slides Per View", "sage")}
value={columns}
onChange={(val) => setAttributes({ columns: val })}
min={1}
max={5}
/>
)}
<RangeControl
label={__("Space Between (px)", "sage")}
value={spaceBetween}
onChange={(val) => setAttributes({ spaceBetween: val })}
min={0}
max={60}
step={5}
/>
</>
)}
</>
)}
<RangeControl
label={__("Slider Height (px)", "sage")}
value={effectiveSliderHeight}
onChange={(val) => {
setAttributes({ sliderHeight: val });
saveSliderField("slider_height", String(val));
}}
min={280}
max={800}
step={10}
/>
</PanelBody>
{effectiveLayout !== "countdown" && (
<PanelBody title={__("Slide Content", "sage")} initialOpen={true}>
<div style={{ marginBottom: "12px", fontSize: "11px", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.04em", color: "#6b7280" }}>
{__("Global (all slides)", "sage")}
</div>
<SelectControl
label={__("Default Content Alignment", "sage")}
value={effectiveContentAlign}
options={[
{ label: __("Left", "sage"), value: "left" },
{ label: __("Center", "sage"), value: "center" },
{ label: __("Right", "sage"), value: "right" },
]}
onChange={(val) => setAttributes({ contentAlign: val })}
help={__("Fallback for slides without individual alignment.", "sage")}
/>
<div style={{ borderTop: "1px solid #e5e7eb", margin: "14px 0 12px" }}></div>
<div style={{ marginBottom: "8px", fontSize: "11px", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.04em", color: "#6b7280" }}>
{__("Per-slide overrides", "sage")}
</div>
{/* OEM Minerva: per-slide alignment + image link */}
{editingSlideKey && (
<>
{selectedSlider && selectedSlider.source === "minerva" && oemSlideOptions.length > 1 && (
<SelectControl
label={__("Edit Slide", "sage")}
value={editingSlideKey}
options={oemSlideOptions}
onChange={(val) => {
setMediaColumnsEditKey(val);
const next = (minervaOffers || []).findIndex((offer) => getOfferKey(offer) === val);
if (next >= 0) setPreviewSlide(next);
}}
help={__("Choose the OEM slide you want to edit.", "sage")}
/>
)}
<SelectControl
label={__("Slide Alignment", "sage")}
value={editingSlideAlign}
options={[
{ label: __("Left", "sage"), value: "left" },
{ label: __("Center", "sage"), value: "center" },
{ label: __("Right", "sage"), value: "right" },
]}
onChange={(val) => saveSlideAlign(editingSlideKey, val)}
help={__("Alignment for this specific slide.", "sage")}
/>
<TextControl
label={__("Image Link URL", "sage")}
value={slideImageLinks[editingSlideKey] || ""}
onChange={(val) => saveSlideField(editingSlideKey, "image_link_url", val)}
placeholder="https://"
help={__("Opens when clicking the slide image.", "sage")}
/>
<SelectControl
label={__("Image Link Target", "sage")}
value={((selectedSlider.slide_settings || {})[editingSlideKey] || {}).image_link_target || "_self"}
options={[
{ label: __("Same tab", "sage"), value: "_self" },
{ label: __("New tab", "sage"), value: "_blank" },
]}
onChange={(val) => saveSlideField(editingSlideKey, "image_link_target", val)}
/>
{effectiveLayout === "media-columns" && (
<>
<TextControl
label={__("Button Label", "sage")}
value={slideButtonLabels[editingSlideKey] !== undefined ? slideButtonLabels[editingSlideKey] : (((selectedSlider.slide_settings || {})[editingSlideKey] || {}).cta_label || "")}
onChange={(val) => saveSlideField(editingSlideKey, "cta_label", val)}
placeholder={__("Enter", "sage")}
help={__("Optional override for this slide button text.", "sage")}
/>
<TextControl
label={__("Button Link URL", "sage")}
value={slideButtonUrls[editingSlideKey] !== undefined ? slideButtonUrls[editingSlideKey] : (((selectedSlider.slide_settings || {})[editingSlideKey] || {}).cta_url || "")}
onChange={(val) => saveSlideField(editingSlideKey, "cta_url", val)}
placeholder="https://"
help={__("Link used by the button on this slide.", "sage")}
/>
<SelectControl
label={__("Button Link Target", "sage")}
value={slideButtonTargets[editingSlideKey] !== undefined ? slideButtonTargets[editingSlideKey] : (((selectedSlider.slide_settings || {})[editingSlideKey] || {}).cta_target || "_self")}
options={[
{ label: __("Same tab", "sage"), value: "_self" },
{ label: __("New tab", "sage"), value: "_blank" },
]}
onChange={(val) => saveSlideField(editingSlideKey, "cta_target", val)}
/>
</>
)}
</>
)}
{/* Custom slides: image link per slide */}
{selectedSlider && selectedSlider.source === "custom" && (selectedSlider.custom_slides || []).length > 0 && (() => {
const slides = selectedSlider.custom_slides || [];
const idx = Math.min(customEditSlideIndex, slides.length - 1);
const slide = slides[idx];
const slideTitle = (slide.title || "").trim() || __("Untitled Slide", "sage");
return (
<>
{slides.length > 1 && (
<SelectControl
label={__("Edit Slide", "sage")}
value={String(idx)}
options={slides.map((s, i) => ({
label: `${i + 1}. ${(s.title || "").trim() || __("Untitled Slide", "sage")}`,
value: String(i),
}))}
onChange={(val) => {
const next = parseInt(val, 10) || 0;
setCustomEditSlideIndex(next);
setPreviewSlide(next);
}}
help={__("Choose the slide you want to edit.", "sage")}
/>
)}
<SelectControl
label={`${__("Slide Alignment", "sage")} — ${slideTitle}`}
value={customSlideAligns[idx] !== undefined ? customSlideAligns[idx] : (slide.align || effectiveContentAlign)}
options={[
{ label: __("Left", "sage"), value: "left" },
{ label: __("Center", "sage"), value: "center" },
{ label: __("Right", "sage"), value: "right" },
]}
onChange={(val) => saveCustomSlideField(idx, "align", val)}
help={__("Alignment for this specific slide in Hero/Split layouts.", "sage")}
/>
<TextControl
label={`${__("Image Link URL", "sage")} — ${slideTitle}`}
value={customSlideLinks[idx] !== undefined ? customSlideLinks[idx] : (slide.image_link_url || "")}
onChange={(val) => saveCustomSlideField(idx, "image_link_url", val)}
placeholder="https://"
help={__("Opens when clicking the slide image.", "sage")}
/>
<SelectControl
label={__("Image Link Target", "sage")}
value={slide.image_link_target || "_self"}
options={[
{ label: __("Same tab", "sage"), value: "_self" },
{ label: __("New tab", "sage"), value: "_blank" },
]}
onChange={(val) => saveCustomSlideField(idx, "image_link_target", val)}
/>
</>
);
})()}
{/* Countdown: image link */}
{selectedSlider && selectedSlider.source === "countdown" && (
<>
<TextControl
label={__("Image Link URL", "sage")}
value={countdownImageLink}
onChange={(val) => saveSliderField("countdown_image_link_url", val)}
placeholder="https://"
help={__("Opens when clicking the countdown background.", "sage")}
/>
<SelectControl
label={__("Image Link Target", "sage")}
value={selectedSlider.countdown_image_link_target || "_self"}
options={[
{ label: __("Same tab", "sage"), value: "_self" },
{ label: __("New tab", "sage"), value: "_blank" },
]}
onChange={(val) => saveSliderField("countdown_image_link_target", val)}
/>
</>
)}
{selectedSlider && selectedSlider.source === "minerva" && (effectiveLayout === "offers-template" || effectiveLayout === "split" || effectiveLayout === "carousel") && (
<ToggleControl
label={__("Show View Inventory Button", "sage")}
help={effectiveShowViewInventory ? __("OEM slides will show a View Inventory CTA when no custom CTA/image link exists.", "sage") : __("View Inventory fallback hidden on OEM slides.", "sage")}
checked={effectiveShowViewInventory}
onChange={(val) => setAttributes({ showViewInventory: val })}
/>
)}
{(effectiveLayout === "offers-template" || effectiveLayout === "split") && (
<BaseControl label={__("Gradient Overlay Color", "sage")} help={gradientColor ? gradientColor : __("Default: dark navy (#000032)", "sage")}>
<ColorPalette
colors={[
{ name: "Navy (Default)", color: "#000032" },
{ name: "Black", color: "#000000" },
{ name: "Dark Gray", color: "#1f2937" },
{ name: "Dark Blue", color: "#1e3a5f" },
{ name: "Dark Green", color: "#064e3b" },
{ name: "Dark Red", color: "#7f1d1d" },
{ name: "Dark Purple", color: "#4c1d95" },
]}
value={gradientColor || "#000032"}
onChange={(val) => setAttributes({ gradientColor: val || "" })}
clearable={true}
/>
</BaseControl>
)}
</PanelBody>
)}
{effectiveLayout === "countdown" && (
<PanelBody title={__("Countdown Settings", "sage")} initialOpen={true}>
<TextControl
label={__("End Date & Time", "sage")}
help={__("Format: YYYY-MM-DD HH:MM (e.g. 2026-04-30 23:59)", "sage")}
value={countdownEndDate}
onChange={(val) => setAttributes({ countdownEndDate: val })}
/>
<TextControl
label={__("Start Date & Time (optional)", "sage")}
help={__("If set, countdown is hidden before this date", "sage")}
value={countdownStartDate}
onChange={(val) => setAttributes({ countdownStartDate: val })}
/>
<SelectControl
label={__("Countdown Style", "sage")}
value={countdownStyle}
options={[
{ label: __("Flip Clock", "sage"), value: "flip" },
{ label: __("Minimal", "sage"), value: "minimal" },
{ label: __("Boxed", "sage"), value: "boxed" },
{ label: __("Neon Glow", "sage"), value: "neon" },
]}
onChange={(val) => setAttributes({ countdownStyle: val })}
/>
<TextControl
label={__("Expired Message", "sage")}
help={__("Shown when countdown reaches zero", "sage")}
value={countdownExpiredMessage}
placeholder={__("This offer has ended.", "sage")}
onChange={(val) => setAttributes({ countdownExpiredMessage: val })}
/>
<RangeControl
label={__("Overlay Opacity (%)", "sage")}
value={countdownOverlayOpacity}
onChange={(val) => setAttributes({ countdownOverlayOpacity: val })}
min={0}
max={100}
step={5}
/>
<BaseControl label={__("Background Image", "sage")}>
<MediaUploadCheck>
<MediaUpload
onSelect={(media) => setAttributes({ countdownBgImage: media.url, countdownBgImageId: media.id })}
allowedTypes={["image"]}
value={countdownBgImageId}
render={({ open }) => (
<div>
{countdownBgImage ? (
<div style={{ marginBottom: "8px" }}>
<img src={countdownBgImage} alt="" style={{ maxWidth: "100%", maxHeight: "150px", borderRadius: "4px", objectFit: "cover" }} />
<div style={{ display: "flex", gap: "8px", marginTop: "6px" }}>
<button className="components-button is-secondary is-small" onClick={open}>{__("Replace", "sage")}</button>
<button className="components-button is-destructive is-small" onClick={() => setAttributes({ countdownBgImage: "", countdownBgImageId: 0 })}>{__("Remove", "sage")}</button>
</div>
</div>
) : (
<button className="components-button is-secondary" onClick={open}>{__("Select Image", "sage")}</button>
)}
</div>
)}
/>
</MediaUploadCheck>
</BaseControl>
</PanelBody>
)}
</InspectorControls>
{selectedSlider ? (() => {
// Real-render preview: embed the actual front-end output in an iframe.
// 100% faithful — respects switches, order, additional/static slides and every layout.
// Falls through to the lightweight React placeholder below while the URL resolves.
if (previewUrl) {
// Always send an explicit lang (en|fr) so the backend forces the locale BOTH
// ways. Omitting it for EN would fall through to the site default — which is
// French on French-first sites, so the EN button would still render French.
// NOTE: the param MUST NOT be named "lang" — that's Polylang's reserved query
// var. Passing &lang=en reinterprets the request as the English language and
// drops the admin session on French-first sites (preview 500s). Use lbx_lang.
const langSrc = previewUrl + (previewUrl.indexOf("?") > -1 ? "&" : "?") + "lbx_lang=" + previewLang;
return (
<div style={{ position: "relative", width: "100%", borderRadius: "8px", overflow: "hidden", border: "1px solid rgba(0,0,0,0.08)", background: "#0b1020" }}>
<div style={{ position: "absolute", top: "8px", left: "8px", zIndex: 2, padding: "3px 10px", fontSize: "10px", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", borderRadius: "9999px", background: "rgba(0,0,0,0.55)", color: "#fff", pointerEvents: "none" }}>{__("Live preview", "sage")}</div>
{props.isSelected && (
<div style={{ position: "absolute", top: "6px", right: "8px", zIndex: 3, display: "flex", alignItems: "center", gap: "6px" }}>
<div style={{ display: "inline-flex", borderRadius: "9999px", overflow: "hidden", background: "rgba(0,0,0,0.55)" }}>
{["en", "fr"].map((lng) => (
<button
key={lng}
type="button"
onClick={() => setPreviewLang(lng)}
title={lng === "fr" ? __("Preview in French", "sage") : __("Preview in English", "sage")}
style={{ padding: "4px 10px", fontSize: "10px", fontWeight: 700, textTransform: "uppercase", border: "none", cursor: "pointer", background: previewLang === lng ? "#2563eb" : "transparent", color: "#fff" }}
>{lng}</button>
))}
</div>
<button
type="button"
onClick={() => setPreviewReload((k) => k + 1)}
title={__("Refresh preview", "sage")}
style={{ width: "26px", height: "26px", display: "flex", alignItems: "center", justifyContent: "center", borderRadius: "9999px", border: "none", cursor: "pointer", background: "rgba(0,0,0,0.55)", color: "#fff", fontSize: "14px", lineHeight: 1 }}
>↻</button>
</div>
)}
<iframe
key={previewUrl + "#" + previewLang + "#" + previewReload}
src={langSrc}
title={__("Slider live preview", "sage")}
scrolling="no"
/* pointer-events follow selection: when the block isn't selected, clicks
pass through so you can select it; once selected the slider becomes
interactive (arrows/dots work) for previewing navigation. */
style={{ display: "block", width: "100%", height: previewHeight + "px", border: 0, pointerEvents: props.isSelected ? "auto" : "none" }}
/>
</div>
);
}
const isHero = effectiveLayout === "offers-template" || effectiveLayout === "split" || effectiveLayout === "oem-offer-hero";
const slides = selectedSlider.custom_slides || [];
const firstSlide = slides.length > 0 ? slides[0] : null;
const sourceLabels = { minerva: "OEM Offers", custom: "Custom", countdown: "Countdown" };
// Helper: hex to rgb string
const hexToRgb = (hex) => {
const h = (hex || "#000032").replace("#", "");
const r = parseInt(h.substring(0, 2), 16);
const g = parseInt(h.substring(2, 4), 16);
const b = parseInt(h.substring(4, 6), 16);
return `${r},${g},${b}`;
};
const gc = gradientColor || "#000032";
const rgb = hexToRgb(gc);
const heroGradient = effectiveContentAlign === "right"
? `linear-gradient(to left, rgba(${rgb},0.85) 0%, rgba(${rgb},0.65) 35%, rgba(${rgb},0.1) 55%, transparent 70%)`
: effectiveContentAlign === "center"
? `radial-gradient(ellipse at center, rgba(${rgb},0.85) 0%, rgba(${rgb},0.55) 50%, rgba(${rgb},0.15) 80%, transparent 100%)`
: `linear-gradient(to right, rgba(${rgb},0.85) 0%, rgba(${rgb},0.65) 35%, rgba(${rgb},0.1) 55%, transparent 70%)`;
// ─── Arrow style helper for editor preview (admin is source of truth) ───
const effShowArrows = (selectedSlider.show_arrows !== undefined) ? !!selectedSlider.show_arrows : showArrows;
const effShowPagination = (selectedSlider.show_pagination !== undefined) ? !!selectedSlider.show_pagination : showPagination;
const effArrowStyle = selectedSlider.arrow_style || arrowStyle;
const effArrowColor = selectedSlider.arrow_color || arrowColor;
const effArrowOpacity = (selectedSlider.arrow_opacity != null) ? Number(selectedSlider.arrow_opacity) : arrowOpacity;
const effPaginationColor = selectedSlider.pagination_color || paginationColor;
const arrowBaseStyle = { position: "absolute", top: "50%", transform: "translateY(-50%)", zIndex: 10, display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", transition: "all 0.2s" };
const aOp = (effArrowOpacity || 90) / 100;
const hexToRgba = (hex, opacity) => { const h = (hex || "#ffffff").replace("#", ""); return `rgba(${parseInt(h.substring(0,2),16)},${parseInt(h.substring(2,4),16)},${parseInt(h.substring(4,6),16)},${opacity})`; };
const isLightColor = (hex) => { const h = (hex || "#ffffff").replace("#", ""); const r = parseInt(h.substring(0,2),16); const g = parseInt(h.substring(2,4),16); const b = parseInt(h.substring(4,6),16); return (r * 299 + g * 587 + b * 114) / 1000 > 128; };
const customBg = effArrowColor ? hexToRgba(effArrowColor, aOp) : null;
const customChevron = effArrowColor ? (isLightColor(effArrowColor) ? "#374151" : "#ffffff") : null;
const arrowVariants = {
circle: { width: "44px", height: "44px", borderRadius: "50%", background: customBg || `rgba(255,255,255,${aOp})`, border: "none", color: customChevron || "#374151", boxShadow: "0 2px 8px rgba(0,0,0,0.15)" },
square: { width: "44px", height: "44px", borderRadius: "6px", background: customBg || `rgba(255,255,255,${aOp})`, border: "none", color: customChevron || "#374151", boxShadow: "0 2px 8px rgba(0,0,0,0.15)" },
pill: { width: "52px", height: "38px", borderRadius: "999px", background: customBg || `rgba(255,255,255,${aOp})`, border: "none", color: customChevron || "#374151", boxShadow: "0 2px 8px rgba(0,0,0,0.15)" },
minimal: { width: "36px", height: "36px", borderRadius: "50%", background: "transparent", border: "none", color: customChevron || "#fff", boxShadow: "none", filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.5))" },
dark: { width: "44px", height: "44px", borderRadius: "50%", background: customBg || `rgba(0,0,0,${aOp * 0.67})`, border: "2px solid rgba(255,255,255,0.3)", color: customChevron || "#fff", boxShadow: "0 2px 8px rgba(0,0,0,0.4)" },
};
const aStyle = arrowVariants[effArrowStyle] || arrowVariants.circle;
const prevArrowStyle = { ...arrowBaseStyle, ...aStyle, left: "12px" };
const nextArrowStyle = { ...arrowBaseStyle, ...aStyle, right: "12px" };
const chevronSize = effArrowStyle === "minimal" ? "28px" : "20px";
const chevronColor = aStyle.color || "#374151";
const renderArrows = (onPrev, onNext) => effShowArrows && (
<>
<button onClick={onPrev} style={prevArrowStyle}>
<svg viewBox="0 0 24 24" fill="none" stroke={chevronColor} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ width: chevronSize, height: chevronSize }}><polyline points="15 18 9 12 15 6" /></svg>
</button>
<button onClick={onNext} style={nextArrowStyle}>
<svg viewBox="0 0 24 24" fill="none" stroke={chevronColor} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ width: chevronSize, height: chevronSize }}><polyline points="9 18 15 12 9 6" /></svg>
</button>
</>
);
const dotColor = effPaginationColor || "#fff";
const renderDots = (total, activeIdx, onDot) => effShowPagination && total > 1 && (
<div style={{ position: "absolute", bottom: "16px", left: "50%", transform: "translateX(-50%)", display: "flex", gap: "6px", zIndex: 10 }}>
{Array.from({ length: total }, (_, i) => (
<button key={i} onClick={() => onDot(i)} style={{ width: i === activeIdx ? "24px" : "8px", height: "8px", borderRadius: "4px", background: i === activeIdx ? dotColor : (effPaginationColor ? effPaginationColor + "66" : "rgba(255,255,255,0.4)"), border: "none", cursor: "pointer", transition: "all 0.2s", padding: 0, opacity: i === activeIdx ? 1 : 0.5 }} />
))}
</div>
);
// ─── Countdown layout preview ───
if (effectiveLayout === "countdown") {
// Resolve countdown data: Gutenberg overrides > admin config
const adminCd = selectedSlider || {};
const cdEndDate = countdownEndDate || adminCd.countdown_end_date || "";
const cdStyle = countdownStyle || adminCd.countdown_style || "flip";
const cdOverlayOp = ((countdownOverlayOpacity != null ? countdownOverlayOpacity : (adminCd.countdown_overlay_opacity || 60))) / 100;
const cdBgImg = countdownBgImage || adminCd.countdown_bg_image || (firstSlide ? firstSlide.image_url : null);
const cdTitle = adminCd.countdown_title || (firstSlide ? firstSlide.title : "");
const cdCtaLabel = adminCd.countdown_cta_label || (firstSlide && firstSlide.main_cta ? firstSlide.main_cta.label : "");
const cdCtaColor = (firstSlide && firstSlide.main_cta ? firstSlide.main_cta.color : "") || "#1d4ed8";
const styleMap = { flip: "Flip Clock", minimal: "Minimal", boxed: "Boxed", neon: "Neon Glow" };
const digitStyle = cdStyle === "neon"
? { fontSize: "48px", fontWeight: 700, color: "#fff", textShadow: `0 0 10px ${gc}, 0 0 20px ${gc}, 0 0 40px ${gc}`, fontFamily: "monospace" }
: cdStyle === "boxed"
? { fontSize: "42px", fontWeight: 700, color: "#fff", background: gc, padding: "12px 18px", borderRadius: "8px", minWidth: "70px", textAlign: "center" }
: cdStyle === "minimal"
? { fontSize: "52px", fontWeight: 300, color: "#fff", fontFamily: "inherit", letterSpacing: "-2px" }
: { fontSize: "44px", fontWeight: 700, color: "#fff", background: "rgba(0,0,0,0.5)", padding: "10px 16px", borderRadius: "6px", minWidth: "68px", textAlign: "center", borderBottom: "2px solid rgba(255,255,255,0.15)" };
const labelStyle = { fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.15em", opacity: 0.7, marginTop: "6px", color: "#fff" };
return (
<div style={{ position: "relative", overflow: "hidden", height: effectiveSliderHeight + "px", background: gc, display: "flex", alignItems: "center", justifyContent: "center" }}>
{cdBgImg && <img src={cdBgImg} alt="" style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" }} />}
<div style={{ position: "absolute", inset: 0, background: `rgba(0,0,0,${cdOverlayOp})` }}></div>
<div style={{ position: "relative", zIndex: 2, textAlign: "center", color: "#fff", padding: "40px" }}>
<div style={{ fontSize: "11px", textTransform: "uppercase", letterSpacing: "0.15em", opacity: 0.5, marginBottom: "8px" }}>
{styleMap[cdStyle] || "Flip Clock"} · {__("Countdown Timer", "sage")}
</div>
{cdTitle && (
<h3 style={{ fontSize: "28px", fontWeight: 700, textTransform: "uppercase", margin: "0 0 24px", textShadow: "0 2px 8px rgba(0,0,0,0.5)" }}>{cdTitle}</h3>
)}
<div style={{ display: "flex", gap: cdStyle === "minimal" ? "24px" : "12px", justifyContent: "center", alignItems: "flex-start" }}>
{["12", "08", "45", "30"].map((val, i) => (
<div key={i} style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
<span style={digitStyle}>{val}</span>
<span style={labelStyle}>{[__("Days", "sage"), __("Hours", "sage"), __("Min", "sage"), __("Sec", "sage")][i]}</span>
</div>
))}
</div>
{cdEndDate && (
<p style={{ marginTop: "16px", fontSize: "12px", opacity: 0.5 }}>{__("Ends:", "sage")} {cdEndDate}</p>
)}
{cdCtaLabel && (
<span style={{ display: "inline-block", marginTop: "20px", padding: "10px 24px", fontSize: "13px", fontWeight: 600, textTransform: "uppercase", borderRadius: "9999px", background: cdCtaColor, color: "#fff" }}>{cdCtaLabel}</span>
)}
</div>
</div>
);
}
// Alignment helpers for editor preview
const alignMap = {
left: { justifyContent: "flex-start", textAlign: "left", ctaJustify: "flex-start" },
center: { justifyContent: "center", textAlign: "center", ctaJustify: "center" },
right: { justifyContent: "flex-end", textAlign: "right", ctaJustify: "flex-end" },
};
const align = alignMap[effectiveContentAlign] || alignMap.left;
// Per-slide alignment: check local state first (for instant updates), then admin settings
const getSlideAlign = (offer) => {
if (!offer) return effectiveContentAlign;
const key = [offer.year, offer.make, offer.model].filter(Boolean).join("_").toLowerCase().replace(/\s+/g, "-");
if (slideAligns[key]) return slideAligns[key];
const ss = (selectedSlider.slide_settings || {})[key];
return (ss && ss.align) ? ss.align : effectiveContentAlign;
};
const getAlignFor = (offer) => alignMap[getSlideAlign(offer)] || alignMap.left;
const getGradientFor = (offer) => {
const a = getSlideAlign(offer);
return a === "right"
? `linear-gradient(to left, rgba(${rgb},0.85) 0%, rgba(${rgb},0.65) 35%, rgba(${rgb},0.1) 55%, transparent 70%)`
: a === "center"
? `radial-gradient(ellipse at center, rgba(${rgb},0.85) 0%, rgba(${rgb},0.55) 50%, rgba(${rgb},0.15) 80%, transparent 100%)`
: `linear-gradient(to right, rgba(${rgb},0.85) 0%, rgba(${rgb},0.65) 35%, rgba(${rgb},0.1) 55%, transparent 70%)`;
};
const transLabel = { slide: "", fade: "Fade", creative: "3D Rotate", flip: "Flip", coverflow: "Coverflow" }[effectiveTransition] || "";
// ─── Split Panel layout preview (Minerva) ───
if (effectiveLayout === "split" && selectedSlider.source === "minerva" && minervaOffers && minervaOffers.length > 0) {
const idx = Math.min(previewSlide, minervaOffers.length - 1);
const offer = minervaOffers[idx];
const total = minervaOffers.length;
const sa = getSlideAlign(offer);
const isReversed = sa !== "right";
const panelBg = `rgba(${rgb},0.95)`;
return (
<div style={{ position: "relative", overflow: "hidden", height: effectiveSliderHeight + "px", display: "flex", flexDirection: isReversed ? "row-reverse" : "row", background: gc }}>
{/* Image panel */}
<div style={{ width: "58%", position: "relative", overflow: "hidden" }}>
{offer.image && (
<img src={offer.image} alt={offer.model || ""} style={{ width: "100%", height: "100%", objectFit: "cover", objectPosition: "center center" }} />
)}
</div>
{/* Content panel */}
<div style={{ width: "42%", background: panelBg, display: "flex", flexDirection: "column", justifyContent: "center", padding: "40px 36px", color: "#fff" }}>
{total > 1 && (
<span style={{ fontSize: "11px", opacity: 0.5, marginBottom: "12px", textTransform: "uppercase", letterSpacing: "0.1em" }}>{idx + 1} / {total} {__("offers", "sage")}</span>
)}
<h3 style={{ margin: "0 0 20px", fontSize: "26px", fontWeight: 700, textTransform: "uppercase", lineHeight: 1.2 }}>
{offer.year} {offer.model} {offer.trim}
</h3>
{(offer.finance || offer.lease) && (
<div style={{ display: "flex", gap: "10px", marginBottom: "16px", flexWrap: "wrap" }}>
{offer.finance && (
<span style={{ padding: "6px 14px", fontSize: "12px", fontWeight: 600, borderRadius: "6px", background: "rgba(255,255,255,0.15)", border: "1px solid rgba(255,255,255,0.25)" }}>
{offer.finance}
</span>
)}
{offer.lease && (
<span style={{ padding: "6px 14px", fontSize: "12px", fontWeight: 600, borderRadius: "6px", background: "rgba(255,255,255,0.15)", border: "1px solid rgba(255,255,255,0.25)" }}>
{offer.lease}
</span>
)}
</div>
)}
{offer.payment && (
<div style={{ fontSize: "22px", fontWeight: 700, marginBottom: "6px" }}>{offer.payment}</div>
)}
{(offer.normal1 || offer.normal2) && (
<p style={{ fontSize: "13px", lineHeight: 1.5, opacity: 0.8, marginBottom: "20px" }}>
{offer.normal1}{offer.normal1 && offer.normal2 && <br />}{offer.normal2}
</p>
)}
{effectiveShowViewInventory && !(slideImageLinks[getOfferKey(offer)] || (((selectedSlider.slide_settings || {})[getOfferKey(offer)] || {}).image_link_url)) && (
<span style={{ padding: "10px 24px", fontSize: "13px", fontWeight: 600, textTransform: "uppercase", borderRadius: "9999px", background: "#1d4ed8", color: "#fff", alignSelf: "flex-start" }}>
{__("VIEW INVENTORY", "sage")}
</span>
)}
</div>
{total > 1 && renderArrows(
() => setPreviewSlide((idx - 1 + total) % total),
() => setPreviewSlide((idx + 1) % total)
)}
{renderDots(total, idx, setPreviewSlide)}
</div>
);
}
// ─── Split Panel layout preview (Custom) ───
if (effectiveLayout === "split" && selectedSlider.source === "custom" && firstSlide) {
const idx = Math.min(previewSlide, slides.length - 1);
const slide = slides[idx];
const total = slides.length;
const slideAlign = (customSlideAligns[idx] !== undefined ? customSlideAligns[idx] : slide.align) || effectiveContentAlign;
const isReversed = slideAlign !== "right";
const panelBg = `rgba(${rgb},0.95)`;
return (
<div style={{ position: "relative", overflow: "hidden", height: effectiveSliderHeight + "px", display: "flex", flexDirection: isReversed ? "row-reverse" : "row", background: gc }}>
<div style={{ width: "58%", position: "relative", overflow: "hidden" }}>
{slide.media_type === "video" && slide.video_url ? (
<video src={slide.video_url} muted autoPlay loop playsInline style={{ width: "100%", height: "100%", objectFit: "cover", objectPosition: "center center" }} />
) : slide.media_type === "video" ? (
<div style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", background: "#111827", color: "#9ca3af", fontSize: "12px", textTransform: "uppercase", letterSpacing: "0.12em" }}>{__("Video slide", "sage")}</div>
) : slide.image_url && (
<img src={slide.image_url} alt={slide.title || ""} style={{ width: "100%", height: "100%", objectFit: "cover", objectPosition: "center center" }} />
)}
</div>
<div style={{ width: "42%", background: panelBg, display: "flex", flexDirection: "column", justifyContent: "center", padding: "40px 36px", color: "#fff" }}>
{total > 1 && (
<span style={{ fontSize: "11px", opacity: 0.5, marginBottom: "8px" }}>{idx + 1} / {total}</span>
)}
<h3 style={{ margin: "0 0 16px", fontSize: "26px", fontWeight: 700, textTransform: "uppercase", lineHeight: 1.2, color: slide.title_color || "#fff", fontStyle: slide.title_italic ? "italic" : "normal" }}>
{slide.title || __("(No title)", "sage")}
</h3>
{slide.description && (
<div style={{ fontSize: "14px", lineHeight: 1.6, marginBottom: "16px", opacity: 0.9, color: slide.desc_color || "#fff" }} dangerouslySetInnerHTML={{ __html: slide.description }}></div>
)}
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap" }}>
{(Array.isArray(slide.ctas) ? slide.ctas : []).filter(c => c.label && (!c.position || c.position === "inline")).map((cta, ci) => (
<span key={ci} style={{ padding: "10px 24px", fontSize: "13px", fontWeight: 600, textTransform: "uppercase", borderRadius: "9999px", background: cta.color || (ci === 0 ? "#1d4ed8" : "rgba(255,255,255,0.2)"), color: "#fff", border: ci > 0 && !cta.color ? "1px solid rgba(255,255,255,0.5)" : "none" }}>{cta.label}</span>
))}
</div>
</div>
{(Array.isArray(slide.ctas) ? slide.ctas : []).filter(c => c.label && c.position && c.position !== "inline").map((cta, ci) => (
<span key={"f" + ci} style={{ position: "absolute", zIndex: 15, padding: "8px 18px", fontSize: "12px", fontWeight: 600, textTransform: "uppercase", borderRadius: "9999px", background: cta.color || "#1d4ed8", color: "#fff", ...(cta.position.includes("top") ? { top: "16px" } : cta.position.includes("bottom") ? { bottom: "16px" } : { top: "50%", transform: "translateY(-50%)" }), ...(cta.position.includes("left") ? { left: "16px" } : cta.position.includes("right") ? { right: "16px" } : { left: "50%", transform: (cta.position.includes("middle") ? "translate(-50%, -50%)" : "translateX(-50%)") }) }}>{cta.label}</span>
))}
{total > 1 && renderArrows(
() => setPreviewSlide((idx - 1 + total) % total),
() => setPreviewSlide((idx + 1) % total)
)}
{renderDots(total, idx, setPreviewSlide)}
</div>
);
}
// ─── Media Columns layout preview (OEM) ───
if (effectiveLayout === "media-columns" && selectedSlider.source === "minerva" && minervaOffers && minervaOffers.length > 0) {
const cols = Math.max(1, Math.min(effectiveMediaColumnsDesktop || 3, 6));
const previewOffers = minervaOffers.slice(0, cols);
return (
<div style={{ position: "relative", overflow: "hidden", minHeight: effectiveSliderHeight + "px", background: "#0f172a", display: "grid", gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`, gap: 0 }}>
{previewOffers.map((offer, i) => (
<div key={i} style={{ position: "relative", minHeight: effectiveSliderHeight + "px", overflow: "hidden", borderLeft: i > 0 ? "1px solid rgba(255,255,255,0.16)" : "none" }}>
{(() => {
const key = [offer.year, offer.make, offer.model].filter(Boolean).join("_").toLowerCase().replace(/\s+/g, "-");
const ss = (selectedSlider.slide_settings || {})[key] || {};
const btnLabel = ss.cta_label || "";
const btnUrl = ss.cta_url || "";
return (
<>
{offer.image && (
<img src={offer.image} alt={offer.model || ""} style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" }} />
)}
<div style={{ position: "absolute", inset: 0, background: "linear-gradient(to top, rgba(0,0,0,0.72) 0%, rgba(0,0,0,0.2) 60%, rgba(0,0,0,0.1) 100%)", zIndex: 3 }}></div>
<div style={{ position: "absolute", left: "0", right: "0", bottom: "0", textAlign: "center", padding: "20px 14px 22px", zIndex: 4, color: "#fff" }}>
<h4 style={{ margin: "0 0 10px", fontSize: "34px", lineHeight: 1.1, textTransform: "uppercase", letterSpacing: "0.08em" }}>
{[offer.year, offer.model].filter(Boolean).join(" ") || __("Untitled", "sage")}
</h4>
{btnLabel && btnUrl && (
<span style={{ display: "inline-block", minWidth: "86px", padding: "8px 14px", borderRadius: "3px", background: "rgba(255,255,255,0.95)", color: "#111827", border: "1px solid rgba(255,255,255,0.8)", fontSize: "10px", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em" }}>
{btnLabel}
</span>
)}
</div>
</>
);
})()}
</div>
))}
</div>
);
}
// ─── Media Columns layout preview (Custom) ───
if (effectiveLayout === "media-columns" && selectedSlider.source === "custom" && firstSlide) {
const cols = Math.max(1, Math.min(effectiveMediaColumnsDesktop || 3, 6));
const previewSlides = slides.slice(0, cols);
return (
<div style={{ position: "relative", overflow: "hidden", minHeight: effectiveSliderHeight + "px", background: "#0f172a", display: "grid", gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`, gap: "0px" }}>
{previewSlides.map((slide, i) => (
<div key={i} style={{ position: "relative", minHeight: effectiveSliderHeight + "px", overflow: "hidden", borderLeft: i > 0 ? "1px solid rgba(255,255,255,0.16)" : "none" }}>
{slide.media_type === "video" && slide.video_url ? (
<video src={slide.video_url} muted autoPlay loop playsInline style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" }} />
) : slide.media_type !== "video" && slide.image_url && (
<img src={slide.image_url} alt={slide.title || ""} style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" }} />
)}
{slide.media_type === "video" && !slide.video_url && (
<div style={{ position: "absolute", inset: 0, background: "rgba(0,0,0,0.24)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 2 }}>
<span style={{ color: "#fff", fontSize: "11px", textTransform: "uppercase", letterSpacing: "0.12em", border: "1px solid rgba(255,255,255,0.5)", borderRadius: "999px", padding: "4px 10px" }}>
{__("Video slide", "sage")}
</span>
</div>
)}
<div style={{ position: "absolute", inset: 0, background: "linear-gradient(to top, rgba(0,0,0,0.72) 0%, rgba(0,0,0,0.2) 60%, rgba(0,0,0,0.1) 100%)", zIndex: 3 }}></div>
<div style={{ position: "absolute", left: "0", right: "0", bottom: "0", textAlign: "center", padding: "20px 14px 22px", zIndex: 4, color: "#fff" }}>
<h4 style={{ margin: "0 0 10px", fontSize: "34px", lineHeight: 1.1, textTransform: "uppercase", letterSpacing: "0.08em", color: slide.title_color || "#fff", fontStyle: slide.title_italic ? "italic" : "normal" }}>
{slide.title || __("Untitled", "sage")}
</h4>
{(Array.isArray(slide.ctas) ? slide.ctas : []).filter(c => c.label && c.url && (!c.position || c.position === "inline")).slice(0, 1).map((cta, ci) => (
<span key={ci} style={{ display: "inline-block", minWidth: "86px", padding: "8px 14px", borderRadius: "3px", background: cta.color || "rgba(255,255,255,0.95)", color: cta.color ? "#fff" : "#111827", border: "1px solid rgba(255,255,255,0.8)", fontSize: "10px", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em" }}>
{cta.label}
</span>
))}
</div>
</div>
))}
</div>
);
}
// ─── Carousel layout preview (Custom) ───
if (effectiveLayout === "carousel" && selectedSlider.source === "custom" && firstSlide) {
const idx = Math.min(previewSlide, slides.length - 1);
const slide = slides[idx];
const total = slides.length;
const slideAlign = (customSlideAligns[idx] !== undefined ? customSlideAligns[idx] : slide.align) || effectiveContentAlign;
const slideAlignObj = alignMap[slideAlign] || alignMap.left;
return (
<div style={{ position: "relative", overflow: "hidden", height: effectiveSliderHeight + "px", background: gc }}>
{slide.media_type === "video" && slide.video_url ? (
<video src={slide.video_url} muted autoPlay loop playsInline style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", objectPosition: slideAlign === "right" ? "left center" : slideAlign === "center" ? "center center" : "right center" }} />
) : slide.media_type !== "video" && slide.image_url && (
<img src={slide.image_url} alt={slide.title || ""} style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", objectPosition: slideAlign === "right" ? "left center" : slideAlign === "center" ? "center center" : "right center" }} />
)}
<div style={{ position: "absolute", inset: 0, background: slideAlign === "right"
? `linear-gradient(to left, rgba(${rgb},0.85) 0%, rgba(${rgb},0.65) 35%, rgba(${rgb},0.1) 55%, transparent 70%)`
: slideAlign === "center"
? `radial-gradient(ellipse at center, rgba(${rgb},0.85) 0%, rgba(${rgb},0.55) 50%, rgba(${rgb},0.15) 80%, transparent 100%)`
: `linear-gradient(to right, rgba(${rgb},0.85) 0%, rgba(${rgb},0.65) 35%, rgba(${rgb},0.1) 55%, transparent 70%)` }}></div>
<div style={{ position: "relative", zIndex: 2, padding: "48px", color: "#fff", maxWidth: slideAlign === "center" ? "65%" : "45%", display: "flex", flexDirection: "column", justifyContent: "center", height: "100%", textAlign: slideAlignObj.textAlign, marginLeft: slideAlign === "right" ? "auto" : undefined }}>
{total > 1 && <span style={{ fontSize: "11px", opacity: 0.5, marginBottom: "8px" }}>{idx + 1} / {total}</span>}
<h3 style={{ margin: "0 0 16px", fontSize: "28px", fontWeight: 700, textTransform: "uppercase", lineHeight: 1.2, textShadow: "0 2px 8px rgba(0,0,0,0.5)", color: slide.title_color || "#fff", fontStyle: slide.title_italic ? "italic" : "normal" }}>
{slide.title || __("(No title)", "sage")}
</h3>
{slide.description && <div style={{ fontSize: "14px", lineHeight: 1.6, marginBottom: "16px", color: slide.desc_color || "#fff" }} dangerouslySetInnerHTML={{ __html: slide.description }}></div>}
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap", justifyContent: slideAlignObj.ctaJustify }}>
{(Array.isArray(slide.ctas) ? slide.ctas : []).filter(c => c.label && (!c.position || c.position === "inline")).map((cta, ci) => (
<span key={ci} style={{ padding: "10px 24px", fontSize: "13px", fontWeight: 600, textTransform: "uppercase", borderRadius: "9999px", background: cta.color || (ci === 0 ? "#1d4ed8" : "rgba(255,255,255,0.2)"), color: "#fff", border: ci > 0 && !cta.color ? "1px solid rgba(255,255,255,0.5)" : "none" }}>{cta.label}</span>
))}
</div>
</div>
{(Array.isArray(slide.ctas) ? slide.ctas : []).filter(c => c.label && c.position && c.position !== "inline").map((cta, ci) => (
<span key={"f" + ci} style={{ position: "absolute", zIndex: 15, padding: "8px 18px", fontSize: "12px", fontWeight: 600, textTransform: "uppercase", borderRadius: "9999px", background: cta.color || "#1d4ed8", color: "#fff", ...(cta.position.includes("top") ? { top: "16px" } : cta.position.includes("bottom") ? { bottom: "16px" } : { top: "50%", transform: "translateY(-50%)" }), ...(cta.position.includes("left") ? { left: "16px" } : cta.position.includes("right") ? { right: "16px" } : { left: "50%", transform: (cta.position.includes("middle") ? "translate(-50%, -50%)" : "translateX(-50%)") }) }}>{cta.label}</span>
))}
{total > 1 && renderArrows(
() => setPreviewSlide((idx - 1 + total) % total),
() => setPreviewSlide((idx + 1) % total)
)}
{renderDots(total, idx, setPreviewSlide)}
</div>
);
}
// ─── Carousel layout preview (Minerva) ───
if (effectiveLayout === "carousel" && selectedSlider.source === "minerva" && minervaOffers && minervaOffers.length > 0) {
const total = minervaOffers.length;
const isSingleSlideEffect = ["fade", "creative", "flip"].includes(effectiveTransition);
const vw = (typeof window !== "undefined" && window.innerWidth) ? window.innerWidth : 1280;
const responsiveCols = vw <= 640
? effectiveCarouselColumnsMobile
: vw <= 1024
? effectiveCarouselColumnsTablet
: effectiveCarouselColumnsDesktop;
const cardsPerView = isSingleSlideEffect
? 1
: Math.max(1, Math.min(Number(responsiveCols) || 3, 6));
const gap = Math.max(0, Number(spaceBetween) || 0);
const start = Math.min(previewSlide, total - 1);
const visibleOffers = Array.from({ length: Math.min(cardsPerView, total) }, (_, i) => {
return minervaOffers[(start + i) % total];
});
return (
<div style={{ position: "relative", overflow: "hidden", height: effectiveSliderHeight + "px", background: gc }}>
<div style={{ display: "grid", gridTemplateColumns: `repeat(${visibleOffers.length}, minmax(0, 1fr))`, gap: `${gap}px`, height: "100%", padding: "0px" }}>
{visibleOffers.map((offer, i) => {
const sa = getSlideAlign(offer);
const sAlign = getAlignFor(offer);
const sGradient = getGradientFor(offer);
const details = [offer.finance, offer.lease, offer.payment, offer.normal1, offer.normal2].filter(Boolean).slice(0, 2).join(" · ");
return (
<div key={`${offer.year || ""}-${offer.make || ""}-${offer.model || ""}-${i}`} style={{ position: "relative", overflow: "hidden", height: "100%", background: "#0f172a" }}>
{offer.image && (
<img src={offer.image} alt={offer.model || ""} style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", objectPosition: sa === "right" ? "left center" : sa === "center" ? "center center" : "right center" }} />
)}
<div style={{ position: "absolute", inset: 0, background: sGradient }}></div>
<div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "flex-end", justifyContent: sAlign.justifyContent, textAlign: sAlign.textAlign, color: "#fff", padding: "20px 14px 22px", zIndex: 2 }}>
<div style={{ width: "100%" }}>
<h4 style={{ margin: "0 0 8px", fontSize: "32px", lineHeight: 1.1, textTransform: "uppercase", letterSpacing: "0.05em", textShadow: "0 2px 8px rgba(0,0,0,0.55)" }}>
{[offer.year, offer.model, offer.trim].filter(Boolean).join(" ") || __("Untitled", "sage")}
</h4>
{details && (
<div style={{ fontSize: "12px", lineHeight: 1.35, opacity: 0.92, marginBottom: (effectiveShowViewInventory && !(slideImageLinks[getOfferKey(offer)] || (((selectedSlider.slide_settings || {})[getOfferKey(offer)] || {}).image_link_url))) ? "10px" : "0" }}>
{details}
</div>
)}
{effectiveShowViewInventory && !(slideImageLinks[getOfferKey(offer)] || (((selectedSlider.slide_settings || {})[getOfferKey(offer)] || {}).image_link_url)) && (
<span style={{ display: "inline-block", padding: "8px 14px", fontSize: "10px", fontWeight: 700, textTransform: "uppercase", borderRadius: "9999px", background: "#1d4ed8", color: "#fff", letterSpacing: "0.1em" }}>
{__("VIEW INVENTORY", "sage")}
</span>
)}
</div>
</div>
</div>
);
})}
</div>
{total > 1 && renderArrows(
() => setPreviewSlide((start - 1 + total) % total),
() => setPreviewSlide((start + 1) % total)
)}
{renderDots(total, start, setPreviewSlide)}
</div>
);
}
// Custom source with offers-template: show one slide at a time
if (selectedSlider.source === "custom" && isHero && firstSlide) {
const idx = Math.min(previewSlide, slides.length - 1);
const slide = slides[idx];
const total = slides.length;
const slideAlign = (customSlideAligns[idx] !== undefined ? customSlideAligns[idx] : slide.align) || effectiveContentAlign;
const slideAlignObj = alignMap[slideAlign] || alignMap.left;
return (
<div style={{ position: "relative", overflow: "hidden", height: effectiveSliderHeight + "px", background: gc }}>
{slide.media_type === "video" && slide.video_url ? (
<video src={slide.video_url} muted autoPlay loop playsInline style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", objectPosition: slideAlign === "right" ? "left center" : slideAlign === "center" ? "center center" : "right center" }} />
) : slide.media_type !== "video" && slide.image_url && (
<img src={slide.image_url} alt={slide.title || ""} style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", objectPosition: slideAlign === "right" ? "left center" : slideAlign === "center" ? "center center" : "right center" }} />
)}
<div style={{ position: "absolute", inset: 0, background: slideAlign === "right"
? `linear-gradient(to left, rgba(${rgb},0.85) 0%, rgba(${rgb},0.65) 35%, rgba(${rgb},0.1) 55%, transparent 70%)`
: slideAlign === "center"
? `radial-gradient(ellipse at center, rgba(${rgb},0.85) 0%, rgba(${rgb},0.55) 50%, rgba(${rgb},0.15) 80%, transparent 100%)`
: `linear-gradient(to right, rgba(${rgb},0.85) 0%, rgba(${rgb},0.65) 35%, rgba(${rgb},0.1) 55%, transparent 70%)` }}></div>
<div style={{ position: "relative", zIndex: 2, padding: "48px", color: "#fff", maxWidth: slideAlign === "center" ? "65%" : "45%", display: "flex", flexDirection: "column", justifyContent: "center", height: "100%", textAlign: slideAlignObj.textAlign, marginLeft: slideAlign === "right" ? "auto" : undefined }}>
{total > 1 && (
<span style={{ fontSize: "11px", opacity: 0.5, marginBottom: "8px" }}>{idx + 1} / {total}</span>
)}
<h3 style={{ margin: "0 0 16px", fontSize: "28px", fontWeight: 700, textTransform: "uppercase", lineHeight: 1.2, textShadow: "0 2px 8px rgba(0,0,0,0.5)", color: slide.title_color || "#fff", fontStyle: slide.title_italic ? "italic" : "normal" }}>
{slide.title || __("(No title)", "sage")}
</h3>
{slide.description && (
<div style={{ border: "1px solid rgba(255,255,255,0.4)", borderRadius: "8px", padding: "14px 18px", marginBottom: "16px" }}>
<div style={{ fontSize: "14px", lineHeight: 1.5, color: slide.desc_color || "#fff" }} dangerouslySetInnerHTML={{ __html: slide.description }}></div>
</div>
)}
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap", justifyContent: slideAlignObj.ctaJustify }}>
{(Array.isArray(slide.ctas) ? slide.ctas : []).filter(c => c.label && (!c.position || c.position === "inline")).map((cta, ci) => (
<span key={ci} style={{ padding: "10px 24px", fontSize: "13px", fontWeight: 600, textTransform: "uppercase", borderRadius: "9999px", background: cta.color || (ci === 0 ? "#1d4ed8" : "rgba(255,255,255,0.2)"), color: "#fff", border: ci > 0 && !cta.color ? "1px solid rgba(255,255,255,0.5)" : "none" }}>{cta.label}</span>
))}
</div>
</div>
{(Array.isArray(slide.ctas) ? slide.ctas : []).filter(c => c.label && c.position && c.position !== "inline").map((cta, ci) => (
<span key={"f" + ci} style={{ position: "absolute", zIndex: 15, padding: "8px 18px", fontSize: "12px", fontWeight: 600, textTransform: "uppercase", borderRadius: "9999px", background: cta.color || "#1d4ed8", color: "#fff", ...(cta.position.includes("top") ? { top: "16px" } : cta.position.includes("bottom") ? { bottom: "16px" } : { top: "50%", transform: "translateY(-50%)" }), ...(cta.position.includes("left") ? { left: "16px" } : cta.position.includes("right") ? { right: "16px" } : { left: "50%", transform: (cta.position.includes("middle") ? "translate(-50%, -50%)" : "translateX(-50%)") }) }}>{cta.label}</span>
))}
{total > 1 && renderArrows(
() => setPreviewSlide((idx - 1 + total) % total),
() => setPreviewSlide((idx + 1) % total)
)}
{renderDots(total, idx, setPreviewSlide)}
</div>
);
}
// ─── Minerva source with hero layout: show real offers ───
const isOEM = selectedSlider.source === "minerva";
if (isOEM && isHero && minervaOffers && minervaOffers.length > 0) {
const idx = Math.min(previewSlide, minervaOffers.length - 1);
const offer = minervaOffers[idx];
const total = minervaOffers.length;
const sa = getSlideAlign(offer);
const sAlign = getAlignFor(offer);
const sGradient = getGradientFor(offer);
return (
<div style={{ position: "relative", overflow: "hidden", height: effectiveSliderHeight + "px", background: gc }}>
{offer.image && (
<img src={offer.image} alt={offer.model || ""} style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", objectPosition: sa === "right" ? "left center" : sa === "center" ? "center center" : "right center" }} />
)}
<div style={{ position: "absolute", inset: 0, background: sGradient }}></div>
{total > 1 && renderArrows(
() => setPreviewSlide((idx - 1 + total) % total),
() => setPreviewSlide((idx + 1) % total)
)}
<div style={{ position: "relative", zIndex: 2, padding: "48px", color: "#fff", maxWidth: sa === "center" ? "65%" : "45%", display: "flex", flexDirection: "column", justifyContent: "center", height: "100%", textAlign: sAlign.textAlign, marginLeft: sa === "right" ? "auto" : undefined }}>
{total > 1 && (
<span style={{ fontSize: "11px", opacity: 0.5, marginBottom: "8px" }}>{idx + 1} / {total} {__("offers", "sage")}</span>
)}
<h3 style={{ margin: "0 0 16px", fontSize: "28px", fontWeight: 700, textTransform: "uppercase", lineHeight: 1.2, textShadow: "0 2px 8px rgba(0,0,0,0.5)", color: "#fff" }}>
{offer.year} {offer.model} {offer.trim}
</h3>
{(offer.finance || offer.lease || offer.payment || offer.normal1) && (
<div style={{ border: "1px solid rgba(255,255,255,0.4)", borderRadius: "8px", padding: "14px 18px", marginBottom: "16px" }}>
{offer.finance && <div style={{ fontSize: "14px", lineHeight: 1.6 }}>{offer.finance}</div>}
{offer.lease && <div style={{ fontSize: "14px", lineHeight: 1.6 }}>{offer.lease}</div>}
{offer.payment && <div style={{ fontSize: "14px", lineHeight: 1.6 }}>{offer.payment}</div>}
{offer.normal1 && <div style={{ fontSize: "14px", lineHeight: 1.6 }}>{offer.normal1}</div>}
{offer.normal2 && <div style={{ fontSize: "14px", lineHeight: 1.6 }}>{offer.normal2}</div>}
</div>
)}
{effectiveShowViewInventory && !(slideImageLinks[getOfferKey(offer)] || (((selectedSlider.slide_settings || {})[getOfferKey(offer)] || {}).image_link_url)) && (
<span style={{ padding: "10px 24px", fontSize: "13px", fontWeight: 600, textTransform: "uppercase", borderRadius: "9999px", background: "#1d4ed8", color: "#fff", alignSelf: "flex-start" }}>
{__("VIEW INVENTORY", "sage")}
</span>
)}
</div>
{renderDots(total, idx, setPreviewSlide)}
</div>
);
}
// ─── Loading / fallback preview ───
const previewImg = firstSlide ? firstSlide.image_url : null;
const slideCount = isOEM ? (minervaOffers ? minervaOffers.length : null) : (selectedSlider.source === "custom" ? slides.length : null);
return (
<div style={{ position: "relative", overflow: "hidden", minHeight: isHero ? (effectiveSliderHeight + "px") : "200px", background: gc }}>
{previewImg && (
<img src={previewImg} alt="" style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", objectPosition: "right center" }} />
)}
<div style={{ position: "absolute", inset: 0, background: `linear-gradient(to right, rgba(${rgb},0.92) 0%, rgba(${rgb},0.75) 30%, rgba(${rgb},0.2) 55%, transparent 70%)` }}></div>
<div style={{ position: "relative", zIndex: 2, padding: isHero ? "48px" : "24px 32px", color: "#fff", maxWidth: isHero ? "50%" : "55%", display: "flex", flexDirection: "column", justifyContent: "center", minHeight: "inherit" }}>
<div style={{ display: "inline-flex", alignItems: "center", gap: "6px", marginBottom: "16px", background: "rgba(255,255,255,0.15)", padding: "4px 12px", borderRadius: "4px", alignSelf: "flex-start", fontSize: "11px", textTransform: "uppercase", letterSpacing: "0.1em" }}>
<span className="dashicons dashicons-slides" style={{ fontSize: "14px", width: "14px", height: "14px" }}></span>
{__("LBX Slider", "sage")}
</div>
<h3 style={{ margin: "0 0 12px", fontSize: isHero ? "28px" : "20px", fontWeight: 700, textTransform: "uppercase", lineHeight: 1.2, textShadow: "0 2px 8px rgba(0,0,0,0.5)", color: "#fff" }}>
{selectedSlider.name}
{!selectedSlider.enabled && <span style={{ color: "#ff6b6b", fontSize: "13px", marginLeft: "10px", fontWeight: 400, textTransform: "none" }}>({__("Disabled", "sage")})</span>}
{(selectedSlider.start_date || selectedSlider.end_date) && (
<span style={{ display: "inline-flex", alignItems: "center", gap: "4px", marginLeft: "10px", fontSize: "11px", fontWeight: 500, textTransform: "none", background: "rgba(59,130,246,0.3)", border: "1px solid rgba(59,130,246,0.5)", padding: "2px 8px", borderRadius: "4px", verticalAlign: "middle" }}>
<span className="dashicons dashicons-calendar-alt" style={{ fontSize: "12px", width: "12px", height: "12px" }}></span>
{__("Scheduled", "sage")}
</span>
)}
</h3>
<div style={{ border: "1px solid rgba(255,255,255,0.4)", borderRadius: "8px", padding: "14px 18px", marginBottom: "16px" }}>
<p style={{ margin: 0, fontSize: "13px" }}>
<strong>{sourceLabels[selectedSlider.source] || selectedSlider.source}</strong>
{" · "}{effectiveLayout}
{transLabel && <>{" · "}{transLabel}</>}
{!isHero && effectiveTransition === "slide" && <>{" · "}{columns} {__("col", "sage")}</>}
{slideCount !== null && <>{" · "}<strong>{slideCount}</strong> {__("slide(s)", "sage")}</>}
</p>
{(selectedSlider.start_date || selectedSlider.end_date) && (
<p style={{ margin: "6px 0 0", fontSize: "11px", opacity: 0.7 }}>
{selectedSlider.start_date && <>{__("Publishes", "sage")}: {selectedSlider.start_date.replace("T", " ")}</>}
{selectedSlider.start_date && selectedSlider.end_date && " · "}
{selectedSlider.end_date && <>{__("Expires", "sage")}: {selectedSlider.end_date.replace("T", " ")}</>}
</p>
)}
</div>
{isOEM && minervaLoading && (
<p style={{ fontSize: "13px", opacity: 0.6 }}>{__("Loading OEM offers preview...", "sage")}</p>
)}
{isOEM && !minervaLoading && minervaOffers && minervaOffers.length === 0 && (
<p style={{ fontSize: "13px", opacity: 0.6 }}>{__("No active OEM offers found.", "sage")}</p>
)}
</div>
{renderArrows(() => {}, () => {})}
{renderDots(slideCount || 3, 0, () => {})}
</div>
);
})() : (
<div style={{ padding: "40px 20px", textAlign: "center", background: "#f8f9fa", borderRadius: "4px", border: "2px dashed #c3c4c7" }}>
<span className="dashicons dashicons-slides" style={{ fontSize: "40px", color: "#c3c4c7", marginBottom: "8px", display: "block" }}></span>
<p style={{ color: "#757575", margin: 0, fontSize: "14px" }}>{__("Select a slider from the block settings panel →", "sage")}</p>
</div>
)}
</>
);
},
save: () => null,
});
};
/**
* Initialize
*
* @see https://webpack.js.org/api/hot-module-replacement
*/
domReady(main);
import.meta.webpackHot?.accept(main);