| 1 |
/** |
| 2 |
* WordPress dependencies |
| 3 |
*/ |
| 4 |
import { registerPlugin } from "@wordpress/plugins"; |
| 5 |
import { __ } from "@wordpress/i18n"; |
| 6 |
import { useEffect, useState, useCallback, useMemo, useRef } from "@wordpress/element"; |
| 7 |
import { dispatch, useSelect, useDispatch, createReduxStore, register } from "@wordpress/data"; |
| 8 |
import "./editor.scss"; |
| 9 |
|
| 10 |
import { PanelBody, Button, Notice, __experimentalPaletteEdit as PaletteEdit } from "@wordpress/components"; |
| 11 |
import apiFetch from "@wordpress/api-fetch"; |
| 12 |
import domReady from "@wordpress/dom-ready"; |
| 13 |
import { SPToggleGroupControl } from "../components"; |
| 14 |
import { DefaultPresetAttr, generateRootCSS } from "./default-settings"; |
| 15 |
import { breakpointDefault, globalTypographyDefault, shadowDefaultList } from "./defaultConstant"; |
| 16 |
import { SmartPostShowLogoIcon } from "../icons/icons"; |
| 17 |
|
| 18 |
import TypographySettings from "./typography/typography"; |
| 19 |
import ShadowSettings from "./shadow/shadow"; |
| 20 |
import Breakpoint from "./breakpoint/breakpoint"; |
| 21 |
import CustomCss from "./custom-css/customCss"; |
| 22 |
import { googleFonts } from "../controls/controls"; |
| 23 |
import { useEditorSaveStatus } from "../hooks/useEditorSaveStatus"; |
| 24 |
|
| 25 |
// ============================================================================ |
| 26 |
// Constants & Helpers |
| 27 |
// ============================================================================ |
| 28 |
|
| 29 |
const colorStacks = DefaultPresetAttr("colorStacks"); |
| 30 |
|
| 31 |
const COLOR_KEYS = [ |
| 32 |
{ name: "Light (Text)", slug: "light-text" }, |
| 33 |
{ name: "Background", slug: "background" }, |
| 34 |
{ name: "Primary Light", slug: "primary-light" }, |
| 35 |
{ name: "Primary", slug: "primary" }, |
| 36 |
{ name: "Primary Dark", slug: "primary-dark" }, |
| 37 |
{ name: "Secondary", slug: "secondary" }, |
| 38 |
{ name: "Dark 2 (Text)", slug: "dark-2-text" }, |
| 39 |
{ name: "Dark (Text)", slug: "dark-text" }, |
| 40 |
{ name: "Black", slug: "black" }, |
| 41 |
]; |
| 42 |
|
| 43 |
const mapColorStackToPreset = (stackColors) => { |
| 44 |
return stackColors.map((color, index) => ({ |
| 45 |
name: COLOR_KEYS[index]?.name || `color-${index}`, |
| 46 |
slug: COLOR_KEYS[index]?.slug || `color-${index}`, |
| 47 |
color, |
| 48 |
})); |
| 49 |
}; |
| 50 |
|
| 51 |
function normalizeGradient(gradient) { |
| 52 |
if (!gradient) { |
| 53 |
return ""; |
| 54 |
} |
| 55 |
return gradient |
| 56 |
.toLowerCase() |
| 57 |
.replace(/\s+/g, "") |
| 58 |
.replace(/(\d+)\.0+deg/g, "$1deg"); |
| 59 |
} |
| 60 |
|
| 61 |
// ============================================================================ |
| 62 |
// Default Settings |
| 63 |
// ============================================================================ |
| 64 |
|
| 65 |
const DEFAULT_SETTINGS = { |
| 66 |
colors: { |
| 67 |
defaultColors: mapColorStackToPreset(colorStacks?.color_type_1), |
| 68 |
selectedColorStack: "color_type_1", |
| 69 |
presetColors: [], |
| 70 |
customColors: [], |
| 71 |
defaultGradient: [ |
| 72 |
{ |
| 73 |
name: "Ocean Breeze", |
| 74 |
gradient: "linear-gradient(46deg,rgb(0,97,255) 4%,rgb(96,239,255) 93%)", |
| 75 |
slug: "ocean_breeze", |
| 76 |
}, |
| 77 |
{ |
| 78 |
name: "Cosmic Fusion", |
| 79 |
gradient: "linear-gradient(46deg,rgb(232,28,255) 4%,rgb(64,201,255) 93%)", |
| 80 |
slug: "cosmic_fusion", |
| 81 |
}, |
| 82 |
{ |
| 83 |
name: "Sunset Blaze", |
| 84 |
gradient: "linear-gradient(46deg,rgb(255,15,123) 4%,rgb(248,155,41) 93%)", |
| 85 |
slug: "sunset_blaze", |
| 86 |
}, |
| 87 |
{ |
| 88 |
name: "Dreamy Violet", |
| 89 |
gradient: "linear-gradient(46deg,rgb(211,151,250) 4%,rgb(131,100,232) 93%)", |
| 90 |
slug: "dreamy_violet", |
| 91 |
}, |
| 92 |
{ |
| 93 |
name: "Peach Bloom", |
| 94 |
gradient: "linear-gradient(46deg,rgb(246,213,247) 4%,rgb(251,233,215) 93%)", |
| 95 |
slug: "peach_bloom", |
| 96 |
}, |
| 97 |
{ |
| 98 |
name: "Aqua Depths", |
| 99 |
gradient: "linear-gradient(46deg,rgb(0,173,181) 4%,rgb(9,95,92) 93%)", |
| 100 |
slug: "aqua_depths", |
| 101 |
}, |
| 102 |
{ |
| 103 |
name: "Midnight Steel", |
| 104 |
gradient: "linear-gradient(46deg,rgb(36,55,72) 4%,rgb(75,116,159) 93%)", |
| 105 |
slug: "midnight_steel", |
| 106 |
}, |
| 107 |
], |
| 108 |
customGradients: [], |
| 109 |
}, |
| 110 |
typography: globalTypographyDefault, |
| 111 |
shadow: shadowDefaultList, |
| 112 |
breakpoint: breakpointDefault, |
| 113 |
customCss: "", |
| 114 |
rootcss: |
| 115 |
":root{ --sp-smart-breakpoint-tablet: 1023px; --sp-smart-breakpoint-mobile: 767px; --smart-post-light-text: #FAFAFA; --smart-post-background: #FFFFFF; --smart-post-primary-light: #EBEBEB; --smart-post-primary: #999999; --smart-post-primary-dark: #1D1D1D; --smart-post-secondary: #0054FB; --smart-post-dark-2-text: #3E3E3E; --smart-post-dark-text: #0A0A0A; --smart-post-black: #000000;} :root { --smart-post-shadow-subtle-1dp: 0px 1px 2px 0px rgba(0, 0, 0, 0.12); --smart-post-shadow-light-2dp: 0px 2px 4px 0px rgba(0, 0, 0, 0.14); --smart-post-shadow-medium-4dp: 0px 4px 6px 0px rgba(0, 0, 0, 0.16); --smart-post-shadow-strong-8dp: 0px 8px 18px 0px rgba(0, 0, 0, 0.18); --smart-post-shadow-deep-12dp: 0px 12px 17px 0px rgba(0, 0, 0, 0.20); --smart-post-shadow-sharp-4dp: 4px 4px 0px 0px rgba(0, 0, 0, 0.25);}", |
| 116 |
}; |
| 117 |
|
| 118 |
// ============================================================================ |
| 119 |
// Redux Store |
| 120 |
// ============================================================================ |
| 121 |
|
| 122 |
const smartpostStore = createReduxStore("smartpost/global-settings", { |
| 123 |
reducer: (state = DEFAULT_SETTINGS, action) => { |
| 124 |
switch (action.type) { |
| 125 |
case "SET_SETTINGS": |
| 126 |
return { |
| 127 |
...state, |
| 128 |
...action.settings, |
| 129 |
_isDirty: false, |
| 130 |
}; |
| 131 |
|
| 132 |
case "UPDATE_SETTING": |
| 133 |
const isComputedUpdate = |
| 134 |
action.isComputed || |
| 135 |
(action.category === "typography" && |
| 136 |
(action.key === "typographyCss" || action.key === "fontList")) || |
| 137 |
(action.category === "shadow" && action.key === "shadowRootCSS") || |
| 138 |
action.key === "rootcss"; |
| 139 |
return { |
| 140 |
...state, |
| 141 |
[action.category]: { |
| 142 |
...state[action.category], |
| 143 |
[action.key]: action.value, |
| 144 |
}, |
| 145 |
_isDirty: isComputedUpdate ? state._isDirty : true, |
| 146 |
}; |
| 147 |
|
| 148 |
case "ADD_CUSTOM_COLOR": |
| 149 |
return { |
| 150 |
...state, |
| 151 |
colors: { |
| 152 |
...state.colors, |
| 153 |
customColors: [...state.colors.customColors, action.color], |
| 154 |
}, |
| 155 |
_isDirty: true, |
| 156 |
}; |
| 157 |
|
| 158 |
case "ADD_DEFAULT_COLOR": |
| 159 |
return { |
| 160 |
...state, |
| 161 |
colors: { |
| 162 |
...state.colors, |
| 163 |
// Changed from defaultColor to defaultColors. |
| 164 |
defaultColors: [...state.colors.defaultColors, action.color], |
| 165 |
}, |
| 166 |
_isDirty: true, |
| 167 |
}; |
| 168 |
|
| 169 |
case "SET_ROOT_CSS": |
| 170 |
return { |
| 171 |
...state, |
| 172 |
rootcss: action.css, |
| 173 |
_isDirty: state._isDirty, |
| 174 |
}; |
| 175 |
|
| 176 |
case "SET_CUSTOM_CSS": |
| 177 |
return { |
| 178 |
...state, |
| 179 |
customCss: action.css, |
| 180 |
_isDirty: true, |
| 181 |
}; |
| 182 |
|
| 183 |
case "REMOVE_CUSTOM_COLOR": |
| 184 |
return { |
| 185 |
...state, |
| 186 |
colors: { |
| 187 |
...state.colors, |
| 188 |
customColors: state.colors.customColors.filter((_, i) => i !== action.index), |
| 189 |
}, |
| 190 |
_isDirty: true, |
| 191 |
}; |
| 192 |
|
| 193 |
case "UPDATE_PRESET_COLORS": |
| 194 |
return { |
| 195 |
...state, |
| 196 |
colors: { |
| 197 |
...state.colors, |
| 198 |
presetColors: action.colors, |
| 199 |
}, |
| 200 |
_isDirty: true, |
| 201 |
}; |
| 202 |
|
| 203 |
case "SET_SELECTED_COLOR_STACK": |
| 204 |
return { |
| 205 |
...state, |
| 206 |
colors: { |
| 207 |
...state.colors, |
| 208 |
selectedColorStack: action.stackKey, |
| 209 |
}, |
| 210 |
_isDirty: true, |
| 211 |
}; |
| 212 |
|
| 213 |
default: |
| 214 |
return state; |
| 215 |
} |
| 216 |
}, |
| 217 |
actions: { |
| 218 |
setSettings: (settings) => ({ type: "SET_SETTINGS", settings }), |
| 219 |
updateSetting: (category, key, value) => ({ |
| 220 |
type: "UPDATE_SETTING", |
| 221 |
category, |
| 222 |
key, |
| 223 |
value, |
| 224 |
}), |
| 225 |
addCustomColor: (color) => ({ type: "ADD_CUSTOM_COLOR", color }), |
| 226 |
addDefaultColor: (color) => ({ type: "ADD_DEFAULT_COLOR", color }), |
| 227 |
removeCustomColor: (index) => ({ |
| 228 |
type: "REMOVE_CUSTOM_COLOR", |
| 229 |
index, |
| 230 |
}), |
| 231 |
setRootCSS: (css) => ({ type: "SET_ROOT_CSS", css }), |
| 232 |
setCustomCSS: (css) => ({ type: "SET_CUSTOM_CSS", css }), |
| 233 |
updatePresetColors: (colors) => ({ |
| 234 |
type: "UPDATE_PRESET_COLORS", |
| 235 |
colors, |
| 236 |
}), |
| 237 |
setSelectedColorStack: (stackKey) => ({ |
| 238 |
type: "SET_SELECTED_COLOR_STACK", |
| 239 |
stackKey, |
| 240 |
}), |
| 241 |
}, |
| 242 |
selectors: { |
| 243 |
getSettings: (state) => state, |
| 244 |
getSetting: (state, category, key) => state[category]?.[key], |
| 245 |
getCategory: (state, category) => state[category] || {}, |
| 246 |
getSelectedColorStack: (state) => state.colors?.selectedColorStack, |
| 247 |
getPresetColors: (state) => state.colors?.presetColors || [], |
| 248 |
getCustomColors: (state) => state.colors?.customColors || [], |
| 249 |
getDefaultColors: (state) => state.colors?.defaultColors || [], |
| 250 |
getDefaultGradient: (state) => state.colors?.defaultGradient || [], |
| 251 |
getCustomGradients: (state) => state.colors?.customGradients || [], |
| 252 |
getRootCSS: (state) => state.rootcss || "", |
| 253 |
getCustomCSS: (state) => state.customCss || "", |
| 254 |
getAllColors: (state) => state.colors || [], |
| 255 |
isDirty: (state) => state._isDirty || false, |
| 256 |
}, |
| 257 |
}); |
| 258 |
|
| 259 |
register(smartpostStore); |
| 260 |
|
| 261 |
// ============================================================================ |
| 262 |
// API Functions |
| 263 |
// ============================================================================ |
| 264 |
|
| 265 |
const loadGlobalSettings = async () => { |
| 266 |
try { |
| 267 |
const fetchedSettings = await apiFetch({ |
| 268 |
path: "/smart-post/v2/get-global-settings", |
| 269 |
}); |
| 270 |
|
| 271 |
// Ensure we have a selected color stack |
| 272 |
const colorStacks = DefaultPresetAttr("colorStacks"); |
| 273 |
const firstStackKey = Object.keys(colorStacks)[0]; |
| 274 |
|
| 275 |
if (!fetchedSettings.colors?.selectedColorStack && firstStackKey) { |
| 276 |
// Changed from undefined 'settings' to 'fetchedSettings' |
| 277 |
fetchedSettings.colors = fetchedSettings.colors || {}; |
| 278 |
fetchedSettings.colors.selectedColorStack = firstStackKey; |
| 279 |
fetchedSettings.colors.presetColors = mapColorStackToPreset(colorStacks[firstStackKey]); |
| 280 |
} |
| 281 |
if (!fetchedSettings.colors.defaultGradient) { |
| 282 |
fetchedSettings.colors.defaultGradient = DEFAULT_SETTINGS?.colors?.defaultGradient; |
| 283 |
} |
| 284 |
|
| 285 |
dispatch("smartpost/global-settings").setSettings(fetchedSettings); |
| 286 |
return fetchedSettings; |
| 287 |
} catch (error) { |
| 288 |
console.log("Failed to load settings:", error); |
| 289 |
// Initialize with first color stack as fallback. |
| 290 |
const colorStacks = DefaultPresetAttr("colorStacks"); |
| 291 |
const firstStackKey = Object.keys(colorStacks)[0]; |
| 292 |
const fallbackSettings = { ...DEFAULT_SETTINGS }; |
| 293 |
|
| 294 |
if (firstStackKey) { |
| 295 |
fallbackSettings.colors.selectedColorStack = firstStackKey; |
| 296 |
fallbackSettings.colors.presetColors = mapColorStackToPreset(colorStacks[firstStackKey]); |
| 297 |
} |
| 298 |
|
| 299 |
dispatch("smartpost/global-settings").setSettings(fallbackSettings); |
| 300 |
return fallbackSettings; |
| 301 |
} |
| 302 |
}; |
| 303 |
|
| 304 |
const saveGlobalSettings = async (settings) => { |
| 305 |
try { |
| 306 |
const updated = await apiFetch({ |
| 307 |
path: "/smart-post/v2/global-settings", |
| 308 |
method: "POST", |
| 309 |
data: settings, |
| 310 |
}); |
| 311 |
return updated; |
| 312 |
} catch (error) { |
| 313 |
console.log("Failed to save SmartPost settings:", error); |
| 314 |
// Return error object instead of throwing to allow graceful handling. |
| 315 |
return { error: true, message: error.message }; |
| 316 |
} |
| 317 |
}; |
| 318 |
|
| 319 |
// ============================================================================ |
| 320 |
// CSS Generation & Updates |
| 321 |
// ============================================================================ |
| 322 |
|
| 323 |
const generateAllRootCSS = (settings) => { |
| 324 |
const cssParts = []; |
| 325 |
const globalFontFamily = settings?.typography?.fontList?.filter(item => { |
| 326 |
const [name] = item.split(":"); |
| 327 |
return name.trim() !== ""; |
| 328 |
}); |
| 329 |
if (globalFontFamily) { |
| 330 |
const fontList = globalFontFamily?.map( |
| 331 |
(item) => `family=${item?.replaceAll(/[:\s]+/g, (match) => (match === ":" ? ":wght@" : "+"))}&` |
| 332 |
); |
| 333 |
const fontsCss = fontList?.length > 0 ? `@import url('https://fonts.googleapis.com/css2?${fontList.join("")}display=swap');` : ""; |
| 334 |
|
| 335 |
cssParts.push(fontsCss); |
| 336 |
} |
| 337 |
|
| 338 |
if (settings?.typography?.typographyCss) { |
| 339 |
cssParts.push(settings.typography.typographyCss); |
| 340 |
} |
| 341 |
if (settings?.rootcss) { |
| 342 |
cssParts.push(settings.rootcss); |
| 343 |
} |
| 344 |
if (settings?.shadow?.shadowRootCSS) { |
| 345 |
cssParts.push(settings.shadow.shadowRootCSS); |
| 346 |
} |
| 347 |
if (settings?.customCss) { |
| 348 |
cssParts.push(settings.customCss); |
| 349 |
} |
| 350 |
|
| 351 |
return cssParts.join(" "); |
| 352 |
}; |
| 353 |
|
| 354 |
// const updateRootCSS = (settings) => { |
| 355 |
// const googleFonts = ""; |
| 356 |
|
| 357 |
// const css = generateAllRootCSS(settings); |
| 358 |
// const existingTag = document.getElementById("sp-smart-post-global-root-inline-css"); |
| 359 |
// let styleTag = document.getElementById("sp-smart-post-global-root-inline-css-extra"); |
| 360 |
|
| 361 |
// if (!styleTag) { |
| 362 |
// styleTag = document.createElement("style"); |
| 363 |
// styleTag.id = "sp-smart-post-global-root-inline-css-extra"; |
| 364 |
// if (existingTag && existingTag.parentNode) { |
| 365 |
// // Insert new style tag immediately after the existing one |
| 366 |
// existingTag.parentNode.insertBefore(styleTag, existingTag.nextSibling); |
| 367 |
// } |
| 368 |
// } |
| 369 |
// styleTag.innerHTML = css; |
| 370 |
// }; |
| 371 |
const updateRootCSS = (settings) => { |
| 372 |
const googleFonts = ""; // keep this if needed |
| 373 |
|
| 374 |
const css = generateAllRootCSS(settings); |
| 375 |
const existingTag = document.getElementById("sp-smart-post-global-root-inline-css"); |
| 376 |
let styleTag = document.getElementById("sp-smart-post-global-root-inline-css-extra"); |
| 377 |
|
| 378 |
// Insert CSS in Main Document |
| 379 |
if (!styleTag) { |
| 380 |
styleTag = document.createElement("style"); |
| 381 |
styleTag.id = "sp-smart-post-global-root-inline-css-extra"; |
| 382 |
if (existingTag && existingTag.parentNode) { |
| 383 |
existingTag.parentNode.insertBefore(styleTag, existingTag.nextSibling); |
| 384 |
} else { |
| 385 |
document.head.appendChild(styleTag); |
| 386 |
} |
| 387 |
} |
| 388 |
|
| 389 |
styleTag.innerHTML = css; |
| 390 |
|
| 391 |
// --------------------------------------------- |
| 392 |
// ADD CSS ALSO INSIDE THE GUTENBERG IFRAME |
| 393 |
// --------------------------------------------- |
| 394 |
const applyCssToIframe = (iframe) => { |
| 395 |
if (!iframe || !iframe.contentDocument) return; |
| 396 |
|
| 397 |
const iframeDoc = iframe.contentDocument; |
| 398 |
let iframeStyleTag = iframeDoc.getElementById("sp-smart-post-global-root-inline-css-extra"); |
| 399 |
|
| 400 |
if (!iframeStyleTag) { |
| 401 |
iframeStyleTag = iframeDoc.createElement("style"); |
| 402 |
iframeStyleTag.id = "sp-smart-post-global-root-inline-css-extra"; |
| 403 |
iframeDoc.head?.appendChild(iframeStyleTag); |
| 404 |
} |
| 405 |
|
| 406 |
iframeStyleTag.innerHTML = css; |
| 407 |
}; |
| 408 |
|
| 409 |
// Detect Gutenberg iframe (current WP versions) |
| 410 |
// Gutenberg canvas iframe usually looks like: |
| 411 |
// iframe[name="editor-canvas"] OR div.editor-styles-wrapper iframe |
| 412 |
const iframeSelectors = ['iframe[name="editor-canvas"]', ".editor-styles-wrapper iframe", "iframe.editor-canvas"]; |
| 413 |
|
| 414 |
iframeSelectors.forEach((selector) => { |
| 415 |
const iframe = document.querySelector(selector); |
| 416 |
if (iframe) applyCssToIframe(iframe); |
| 417 |
}); |
| 418 |
|
| 419 |
// If iframe loads later (React re-renders), observe and reapply |
| 420 |
const observer = new MutationObserver(() => { |
| 421 |
iframeSelectors.forEach((selector) => { |
| 422 |
const iframe = document.querySelector(selector); |
| 423 |
if (iframe) applyCssToIframe(iframe); |
| 424 |
}); |
| 425 |
}); |
| 426 |
|
| 427 |
observer.observe(document.body, { childList: true, subtree: true }); |
| 428 |
}; |
| 429 |
|
| 430 |
// ============================================================================ |
| 431 |
// Color Settings Component |
| 432 |
// ============================================================================ |
| 433 |
|
| 434 |
const ColorSettings = () => { |
| 435 |
const { |
| 436 |
selectedColorStack, |
| 437 |
presetColors, |
| 438 |
customColors, |
| 439 |
defaultGradient: storedDefaultGradient, |
| 440 |
customGradients, |
| 441 |
allColors, |
| 442 |
breakPoint, |
| 443 |
} = useSelect((select) => { |
| 444 |
const store = select("smartpost/global-settings"); |
| 445 |
return { |
| 446 |
selectedColorStack: store.getSelectedColorStack(), |
| 447 |
presetColors: store.getPresetColors(), |
| 448 |
customColors: store.getCustomColors(), |
| 449 |
defaultGradient: store.getDefaultGradient(), |
| 450 |
customGradients: store.getCustomGradients(), |
| 451 |
allColors: store.getAllColors(), |
| 452 |
breakPoint: store.getCategory("breakpoint"), |
| 453 |
}; |
| 454 |
}, []); |
| 455 |
|
| 456 |
const { updateSetting, setSelectedColorStack, updatePresetColors, setRootCSS } = |
| 457 |
useDispatch("smartpost/global-settings"); |
| 458 |
const [selectedTab, setSelectedTab] = useState("solid"); |
| 459 |
// Memoize base gradient to prevent recreating on every render |
| 460 |
const baseGradient = useMemo(() => DEFAULT_SETTINGS?.colors?.defaultGradient, []); |
| 461 |
// Use fallback if default gradient is empty. |
| 462 |
const defaultGradient = !storedDefaultGradient?.length ? baseGradient : storedDefaultGradient; |
| 463 |
|
| 464 |
const handleColorStackSelection = useCallback( |
| 465 |
(stackKey, stackColors) => { |
| 466 |
setSelectedColorStack(stackKey); |
| 467 |
const mappedPresetColors = mapColorStackToPreset(stackColors); |
| 468 |
updatePresetColors(mappedPresetColors); |
| 469 |
}, |
| 470 |
[setSelectedColorStack, updatePresetColors] |
| 471 |
); |
| 472 |
|
| 473 |
const popoverProps = useMemo( |
| 474 |
() => ({ |
| 475 |
placement: "bottom", |
| 476 |
offset: 36, |
| 477 |
shift: true, |
| 478 |
flip: true, |
| 479 |
}), |
| 480 |
[] |
| 481 |
); |
| 482 |
|
| 483 |
// Better dependency management for CSS generation |
| 484 |
useEffect(() => { |
| 485 |
if (allColors) { |
| 486 |
const generatedCSS = generateRootCSS(allColors, breakPoint); |
| 487 |
setRootCSS(generatedCSS); |
| 488 |
} |
| 489 |
}, [allColors, breakPoint, setRootCSS]); |
| 490 |
const tabs = useMemo( |
| 491 |
() => [ |
| 492 |
{ |
| 493 |
name: "solid", |
| 494 |
label: __("Solid", "post-carousel"), |
| 495 |
value: "solid", |
| 496 |
}, |
| 497 |
{ |
| 498 |
name: "gradient", |
| 499 |
label: __("Gradient", "post-carousel"), |
| 500 |
value: "gradient", |
| 501 |
}, |
| 502 |
], |
| 503 |
[] |
| 504 |
); |
| 505 |
|
| 506 |
return ( |
| 507 |
<PanelBody className="sp-smart-post-color-settings" title={__("Color Palette", "post-carousel")}> |
| 508 |
<SPToggleGroupControl attributes={selectedTab} attributesKey="" onClick={setSelectedTab} items={tabs} /> |
| 509 |
|
| 510 |
{selectedTab === "solid" && ( |
| 511 |
<div> |
| 512 |
<p className="sp-smart-post-setting-title" style={{ fontSize: "13px", marginBottom: "8px" }}> |
| 513 |
{__("Color Palettes", "post-carousel")} |
| 514 |
</p> |
| 515 |
<div className="smart-post-global-content"> |
| 516 |
{Object.keys(colorStacks).map((stackKey) => ( |
| 517 |
<div |
| 518 |
key={stackKey} |
| 519 |
onClick={() => handleColorStackSelection(stackKey, colorStacks[stackKey])} |
| 520 |
style={{ cursor: "pointer" }} |
| 521 |
className={`smart-post-global-current-content ${ |
| 522 |
selectedColorStack === stackKey ? "selected" : "" |
| 523 |
}`} |
| 524 |
> |
| 525 |
<div className="smart-post-preset-color-show"> |
| 526 |
{colorStacks[stackKey] |
| 527 |
// .slice(0, Math.ceil(colorStacks[stackKey].length / 2)) |
| 528 |
.filter((_, i) => i === 0 || i % 2 !== 0) |
| 529 |
.map((color, i) => ( |
| 530 |
<span |
| 531 |
key={i} |
| 532 |
className="smart-post-global-color" |
| 533 |
style={{ |
| 534 |
backgroundColor: color, |
| 535 |
}} |
| 536 |
/> |
| 537 |
))} |
| 538 |
</div> |
| 539 |
</div> |
| 540 |
))} |
| 541 |
</div> |
| 542 |
|
| 543 |
{presetColors.length > 0 && ( |
| 544 |
<PaletteEdit |
| 545 |
colors={presetColors} |
| 546 |
paletteLabel={__("Colors", "post-carousel")} |
| 547 |
onChange={(newColors) => { |
| 548 |
updatePresetColors(newColors); |
| 549 |
updateSetting("colors", "defaultColors", newColors); |
| 550 |
}} |
| 551 |
canOnlyChangeValues |
| 552 |
paletteLabelHeadingLevel={3} |
| 553 |
popoverProps={popoverProps} |
| 554 |
/> |
| 555 |
)} |
| 556 |
|
| 557 |
<PaletteEdit |
| 558 |
colors={customColors || []} |
| 559 |
onChange={(newColors) => { |
| 560 |
updateSetting("colors", "customColors", newColors || []); |
| 561 |
}} |
| 562 |
paletteLabel={__("Custom", "post-carousel")} |
| 563 |
paletteLabelHeadingLevel={3} |
| 564 |
slugPrefix="custom-" |
| 565 |
popoverProps={popoverProps} |
| 566 |
/> |
| 567 |
</div> |
| 568 |
)} |
| 569 |
|
| 570 |
{selectedTab === "gradient" && ( |
| 571 |
<div> |
| 572 |
{/* <h3>{ __( "Gradients", "post-carousel" ) }</h3> */} |
| 573 |
<PaletteEdit |
| 574 |
canReset={ |
| 575 |
normalizeGradient(JSON.stringify(baseGradient)) !== |
| 576 |
normalizeGradient(JSON.stringify(defaultGradient)) |
| 577 |
} |
| 578 |
defaultPalette={baseGradient} |
| 579 |
gradients={defaultGradient} |
| 580 |
onChange={(newColors) => { |
| 581 |
updateSetting("colors", "defaultGradient", !newColors?.length ? baseGradient : newColors); |
| 582 |
}} |
| 583 |
paletteLabel={__("Gradients", "post-carousel")} |
| 584 |
paletteLabelLevel={3} |
| 585 |
popoverProps={popoverProps} |
| 586 |
hideAddButton |
| 587 |
canOnlyChangeValues |
| 588 |
/> |
| 589 |
<PaletteEdit |
| 590 |
gradients={customGradients || []} |
| 591 |
onChange={(newGradients) => { |
| 592 |
updateSetting("colors", "customGradients", newGradients || []); |
| 593 |
}} |
| 594 |
paletteLabel={__("Custom", "post-carousel")} |
| 595 |
paletteLabelLevel={3} |
| 596 |
slugPrefix="custom-gradient-" |
| 597 |
popoverProps={popoverProps} |
| 598 |
/> |
| 599 |
</div> |
| 600 |
)} |
| 601 |
</PanelBody> |
| 602 |
); |
| 603 |
}; |
| 604 |
|
| 605 |
// ============================================================================ |
| 606 |
// Global Settings Component |
| 607 |
// ============================================================================ |
| 608 |
|
| 609 |
const GlobalSettings = () => { |
| 610 |
const [userCanManageOptions] = useState(true); |
| 611 |
const [loading, setLoading] = useState(true); |
| 612 |
const [saving, setSaving] = useState(false); |
| 613 |
const [saveStatus, setSaveStatus] = useState(null); |
| 614 |
|
| 615 |
// Use ref to track if save was triggered by post save |
| 616 |
const saveTimeoutRef = useRef(null); |
| 617 |
const lastSaveRef = useRef(null); |
| 618 |
|
| 619 |
const { isSaving } = useEditorSaveStatus(); |
| 620 |
const { isDirty } = useSelect((select) => { |
| 621 |
const settingsStore = select("smartpost/global-settings"); |
| 622 |
return { |
| 623 |
isDirty: settingsStore.isDirty(), |
| 624 |
}; |
| 625 |
}); |
| 626 |
|
| 627 |
const settings = useSelect((select) => select("smartpost/global-settings").getSettings(), []); |
| 628 |
useEffect(() => { |
| 629 |
if (!loading && settings) { |
| 630 |
updateRootCSS(settings); |
| 631 |
} |
| 632 |
}, [settings, loading]); |
| 633 |
// Load settings on mount |
| 634 |
useEffect(() => { |
| 635 |
const initSettings = async () => { |
| 636 |
try { |
| 637 |
await loadGlobalSettings(); |
| 638 |
} catch (error) { |
| 639 |
// eslint-disable-next-line no-console |
| 640 |
console.log("Failed to initialize settings:", error); |
| 641 |
} finally { |
| 642 |
setLoading(false); |
| 643 |
} |
| 644 |
}; |
| 645 |
initSettings(); |
| 646 |
}, []); |
| 647 |
|
| 648 |
// Stable save function using useCallback |
| 649 |
const handleSave = useCallback(async () => { |
| 650 |
if (saving) { |
| 651 |
return; |
| 652 |
} |
| 653 |
|
| 654 |
setSaving(true); |
| 655 |
setSaveStatus(null); |
| 656 |
|
| 657 |
try { |
| 658 |
const result = await saveGlobalSettings(settings); |
| 659 |
|
| 660 |
if (result?.error) { |
| 661 |
setSaveStatus("error"); |
| 662 |
} else { |
| 663 |
updateRootCSS(settings); |
| 664 |
setSaveStatus("success"); |
| 665 |
lastSaveRef.current = Date.now(); |
| 666 |
// Reset dirty state after successful save |
| 667 |
dispatch("smartpost/global-settings").setSettings({ |
| 668 |
...settings, |
| 669 |
_isDirty: false, |
| 670 |
}); |
| 671 |
// Clear success message after 3 seconds |
| 672 |
if (saveTimeoutRef.current) { |
| 673 |
clearTimeout(saveTimeoutRef.current); |
| 674 |
} |
| 675 |
saveTimeoutRef.current = setTimeout(() => { |
| 676 |
setSaveStatus(null); |
| 677 |
}, 3000); |
| 678 |
} |
| 679 |
} catch (error) { |
| 680 |
console.log("Save error:", error); |
| 681 |
setSaveStatus("error"); |
| 682 |
} finally { |
| 683 |
setSaving(false); |
| 684 |
} |
| 685 |
}, [settings, saving]); |
| 686 |
|
| 687 |
// Cleanup timeout on unmount |
| 688 |
useEffect(() => { |
| 689 |
return () => { |
| 690 |
if (saveTimeoutRef.current) { |
| 691 |
clearTimeout(saveTimeoutRef.current); |
| 692 |
} |
| 693 |
}; |
| 694 |
}, []); |
| 695 |
|
| 696 |
useEffect(() => { |
| 697 |
if (isSaving && !saving) { |
| 698 |
// Debounce saves to prevent multiple rapid saves |
| 699 |
const now = Date.now(); |
| 700 |
if (!lastSaveRef.current || now - lastSaveRef.current > 1000) { |
| 701 |
handleSave(); |
| 702 |
} |
| 703 |
} |
| 704 |
}, [isSaving, saving]); |
| 705 |
|
| 706 |
const PluginSidebar = |
| 707 |
window.wp.editor?.PluginSidebar || window.wp.editSite?.PluginSidebar || window.wp.editPost?.PluginSidebar; |
| 708 |
|
| 709 |
if (!PluginSidebar || !userCanManageOptions) { |
| 710 |
return null; |
| 711 |
} |
| 712 |
|
| 713 |
return ( |
| 714 |
<PluginSidebar |
| 715 |
name="sidebar" |
| 716 |
title={__("SmartPost Global Settings", "post-carousel")} |
| 717 |
className="smart-post-global-settings__inspector sp-smart-post-tab-panel" |
| 718 |
icon={<SmartPostShowLogoIcon />} |
| 719 |
> |
| 720 |
<div> |
| 721 |
<PanelBody> |
| 722 |
<div style={{ textAlign: "center" }}> |
| 723 |
<Button |
| 724 |
variant="primary" |
| 725 |
isBusy={saving} |
| 726 |
disabled={saving || !isDirty} |
| 727 |
onClick={handleSave} |
| 728 |
style={{ |
| 729 |
minWidth: "100%", |
| 730 |
display: "inline-block", |
| 731 |
}} |
| 732 |
> |
| 733 |
{saving ? __("Saving.", "post-carousel") : __("Save Settings", "post-carousel")} |
| 734 |
</Button> |
| 735 |
</div> |
| 736 |
</PanelBody> |
| 737 |
|
| 738 |
{loading && ( |
| 739 |
<Notice status="info" isDismissible={false}> |
| 740 |
{__("Loading settings.", "post-carousel")} |
| 741 |
</Notice> |
| 742 |
)} |
| 743 |
|
| 744 |
{saveStatus === "error" && ( |
| 745 |
<Notice status="error" isDismissible={false}> |
| 746 |
{__("Failed to save settings. Please try again.", "post-carousel")} |
| 747 |
</Notice> |
| 748 |
)} |
| 749 |
|
| 750 |
{!loading && ( |
| 751 |
<div className="sp-smart-post-tab-panel"> |
| 752 |
<ColorSettings /> |
| 753 |
<TypographySettings /> |
| 754 |
<ShadowSettings /> |
| 755 |
<Breakpoint /> |
| 756 |
<CustomCss /> |
| 757 |
</div> |
| 758 |
)} |
| 759 |
</div> |
| 760 |
</PluginSidebar> |
| 761 |
); |
| 762 |
}; |
| 763 |
|
| 764 |
// ============================================================================ |
| 765 |
// Plugin Registration |
| 766 |
// ============================================================================ |
| 767 |
|
| 768 |
domReady(() => { |
| 769 |
registerPlugin("smart-post-show-pro-global-settings", { |
| 770 |
render: GlobalSettings, |
| 771 |
}); |
| 772 |
|
| 773 |
// Export global helper |
| 774 |
window.SmartPostSettings = { |
| 775 |
getSettings: () => wp.data.select("smartpost/global-settings").getSettings(), |
| 776 |
getSetting: (category, key) => wp.data.select("smartpost/global-settings").getSetting(category, key), |
| 777 |
getColors: () => wp.data.select("smartpost/global-settings").getCategory("colors"), |
| 778 |
getTypography: () => wp.data.select("smartpost/global-settings").getCategory("typography"), |
| 779 |
getSelectedColorStack: () => wp.data.select("smartpost/global-settings").getSelectedColorStack(), |
| 780 |
getPresetColors: () => wp.data.select("smartpost/global-settings").getPresetColors(), |
| 781 |
getCustomColors: () => wp.data.select("smartpost/global-settings").getCustomColors(), |
| 782 |
}; |
| 783 |
}); |
| 784 |
|