PluginProbe
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News / 4.0.7
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News v4.0.7
4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 2.3.5 2.3.6 2.4.0 2.4.1 2.4.10 2.4.11 2.4.12 2.4.13 2.4.14 2.4.15 2.4.16 2.4.17 2.4.18 2.4.19 2.4.2 2.4.20 2.4.21 All 88 releases
post-carousel / src / global-settings / index.js

index.js in Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News 4.0.7, at src/global-settings/index.js

784 lines 23.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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