PluginProbe
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News / 4.0.2
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News v4.0.2
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 / prebuild-library / Library.js

Library.js in Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News 4.0.2, at src/prebuild-library/Library.js

590 lines 15.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import ReadyPatterns from "./readyPatterns";
2 import ErrorBoundary from "./ErrorBoundary";
3 import { __ } from "@wordpress/i18n";
4 import { CloseIcon } from "./icons";
5 import { API_ENDPOINTS, KEYBOARD_KEYS } from "./constants";
6 import { parse } from "@wordpress/blocks";
7 import { Fragment, useState, useEffect } from "@wordpress/element";
8 import { SmartPostShowLogoIcon } from "../icons/icons";
9
10 // Default attribute values
11 const DEFAULT_ATTRIBUTES = {
12 postType: "multiple_post_type",
13 multiplePostType: [
14 {
15 id: 1,
16 label: "Posts",
17 value: "post",
18 },
19 ],
20 quickQuery: "",
21 postLimit: "8",
22 offset: 0,
23 filterByAuthor: [],
24 filterByDate: "",
25 filterByKeyword: "",
26 filterByCustomFields: [],
27 orderBy: "date",
28 orderDirection: "DESC",
29 excludePost: [],
30 excludeTerm: [],
31 excludeAuthor: [],
32 excludeStickyPosts: false,
33 excludeCurrentPosts: true,
34 excludeProtectedPosts: false,
35 excludeChildrenPosts: false,
36 excludePostWithoutImagePosts: false,
37 filterProduct: "recent",
38 relation: "AND",
39 specificMonth: "1",
40 specificYear: "2024",
41 blockName: "",
42 displayAdvertisement: false,
43 liveSearchText: "",
44 postQuery: "",
45 taxonomies: [
46 {
47 id: 1,
48 type: "",
49 value: [],
50 operator: "IN",
51 initialOpen: true,
52 },
53 ],
54 categories: [],
55 termId: "",
56 keywordSearch: "",
57 currentPage: 1,
58 itemsPerPage: 6,
59 page_id: "",
60 // Get current date/time in format: Y-m-d H:i:s
61 getCurrentDate: () => {
62 const now = new Date();
63 const year = now.getFullYear();
64 const month = String(now.getMonth() + 1).padStart(2, "0");
65 const day = String(now.getDate()).padStart(2, "0");
66 const hours = String(now.getHours()).padStart(2, "0");
67 const minutes = String(now.getMinutes()).padStart(2, "0");
68 const seconds = String(now.getSeconds()).padStart(2, "0");
69 return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
70 },
71 };
72
73 /**
74 * Modify block attributes after parsing but before insertion
75 * This function recursively processes blocks and their innerBlocks
76 * to reset query-related attributes to their default values
77 *
78 * @param {Array} blocks - Array of parsed block objects
79 * @returns {Array} - Modified blocks array
80 */
81 const modifyBlockAttributes = (blocks) => {
82 if (!Array.isArray(blocks)) {
83 return blocks;
84 }
85
86 return blocks.map((block) => {
87 // Create a new block object to avoid mutating the original
88 const modifiedBlock = {
89 ...block,
90 attributes: {
91 ...block.attributes,
92 },
93 };
94
95 // Only modify Smart Post Show blocks
96 if (block.name && block.name.startsWith("sp-smart-post-show/") && block.attributes) {
97 // Reset all query-related attributes to default values if they exist
98 const attributesToReset = [
99 "postType",
100 "multiplePostType",
101 "quickQuery",
102 "postLimit",
103 "offset",
104 "filterByAuthor",
105 "filterByDate",
106 "filterByKeyword",
107 "filterByCustomFields",
108 "orderBy",
109 "orderDirection",
110 "excludePost",
111 "excludeTerm",
112 "excludeAuthor",
113 "excludeStickyPosts",
114 "excludeCurrentPosts",
115 "excludeProtectedPosts",
116 "excludeChildrenPosts",
117 "excludePostWithoutImagePosts",
118 "filterProduct",
119 "relation",
120 "specificMonth",
121 "specificYear",
122 "blockName",
123 "displayAdvertisement",
124 "liveSearchText",
125 "postQuery",
126 "taxonomies",
127 "categories",
128 "termId",
129 "keywordSearch",
130 "currentPage",
131 "itemsPerPage",
132 "page_id",
133 ];
134
135 attributesToReset.forEach((attrName) => {
136 if (modifiedBlock.attributes.hasOwnProperty(attrName)) {
137 // Get default value
138 const defaultValue = DEFAULT_ATTRIBUTES[attrName];
139
140 // Handle array defaults (deep clone)
141 if (Array.isArray(defaultValue)) {
142 modifiedBlock.attributes[attrName] = JSON.parse(JSON.stringify(defaultValue));
143 } else {
144 modifiedBlock.attributes[attrName] = defaultValue;
145 }
146 }
147 });
148
149 // Reset date fields to current date/time if they exist
150 const dateFields = [
151 "specificDate",
152 "specificPeriodAfter",
153 "specificPeriodBefore",
154 "specificDateBefore",
155 "specificDateAfter",
156 "excludeDateAfter",
157 "excludeDateBefore",
158 ];
159 const currentDate = DEFAULT_ATTRIBUTES.getCurrentDate();
160 dateFields.forEach((fieldName) => {
161 if (modifiedBlock.attributes.hasOwnProperty(fieldName)) {
162 modifiedBlock.attributes[fieldName] = currentDate;
163 }
164 });
165 }
166
167 // Recursively process innerBlocks
168 if (block.innerBlocks && Array.isArray(block.innerBlocks) && block.innerBlocks.length > 0) {
169 modifiedBlock.innerBlocks = modifyBlockAttributes(block.innerBlocks);
170 }
171
172 return modifiedBlock;
173 });
174 };
175
176 // Header Component.
177 const LibraryHeader = ({ onClose, tabState, setTabState }) => {
178 return (
179 <div className="sp-smart-popup-header">
180 <div className="sp-smart-popup-filter-title">
181 <div className="sp-smart-popup-filter-image-head">
182 <SmartPostShowLogoIcon />
183 <span>{__("Smart Design Library", "post-carousel")}</span>
184 </div>
185 <div className="sp-smart-popup-filter-nav">
186 <div className={`sp-smart-popup-tab-title ${tabState === "ready-pattern" ? "sp-smart-active" : ""}`} onClick={() => setTabState("ready-pattern")}>
187 {__("Ready Patterns", "post-carousel")}
188 </div>
189 </div>
190 <div className="sp-smart-popup-filter-sync-close">
191 <button
192 className="sp-smart-btn-close"
193 onClick={onClose}
194 id="sp-smart-btn-close"
195 aria-label={__("Close", "post-carousel")}
196 >
197 <CloseIcon />
198 </button>
199 </div>
200 </div>
201 </div>
202 );
203 };
204
205 // Main Library Component.
206 const Library = (props) => {
207 const [wishListArr, setWishlistArr] = useState([]);
208 const [tabState, setTabState] = useState("ready-pattern");
209 const isBlockPattern = props.currentBlockName ? true : false;
210 let currentBlockName = props.currentBlockName || "all";
211 // let currentSitesName = props.currentSitesName || "all";
212 if (isBlockPattern && props.currentBlockName === "post-thumbnail-slider") {
213 currentBlockName = "thumbnail-slider";
214 }
215
216 const [state, setState] = useState({
217 isPopup: props.isShow || false,
218 designs: [],
219 reloadId: "",
220 reload: false,
221 error: false,
222 fetching: false,
223 designFilter: currentBlockName || "all",
224 current: [],
225 sidebarOpen: true,
226 templatekitCol: "sp-smart-pattern-col3",
227 loading: false,
228 sitesDesign: [],
229 sitesCurrent: [],
230 sitesCategory: [],
231 });
232
233 const { isPopup, designFilter } = state;
234 const localizedData = sp_smart_post_block_localize || {};
235 // Transform nested category data to flat array.
236 const handleDesignData = (data) => {
237 const transformedData = [];
238 for (const category in data) {
239 const items = data[category];
240 if ( Array.isArray( items ) ) {
241 items.forEach( item => {
242 transformedData.push({
243 ...item,
244 category,
245 })
246 })
247 } else if ( typeof items === "object" ) {
248 transformedData.push({
249 ...items,
250 });
251 }
252 }
253 return transformedData;
254 };
255
256 // Fetch Pre-made pattern data from Smart Post Show REST API.
257 const fetchTemplates = async () => {
258 setState((prev) => ({ ...prev, loading: true, error: false }));
259 try {
260 const response = await wp.apiFetch({
261 path: API_ENDPOINTS.PATTERNS,
262 method: "POST",
263 data: { type: "get_data" },
264 });
265
266 if (!response) {
267 throw new Error(__("No response received from server", "post-carousel"));
268 }
269
270 if (response.success && response.data) {
271 const allDesignData = JSON.parse(response.data);
272 let designData = handleDesignData(allDesignData);
273 if (isBlockPattern && currentBlockName) {
274 designData = allDesignData[currentBlockName] || [];
275 }
276 setState((prev) => ({
277 ...prev,
278 current: designData,
279 designs: designData,
280 loading: false,
281 error: false,
282 }));
283 } else {
284 throw new Error(response.message || __("Failed to load patterns", "post-carousel"));
285 }
286 } catch (error) {
287 console.error("Error fetching templates:", error);
288 setState((prev) => ({
289 ...prev,
290 loading: false,
291 error: error.message || __("An unexpected error occurred", "post-carousel"),
292 }));
293 }
294 };
295 // Fetch Pre-made sites data from Smart Post Show REST API.
296 const fetchSitesData = async () => {
297 setState((prev) => ({ ...prev, loading: true, error: false }));
298 try {
299 const response = await wp.apiFetch({
300 path: API_ENDPOINTS.SITES,
301 method: "POST",
302 // data: { type: "get_data" },
303 });
304
305 if (!response) {
306 throw new Error(__("No response received from server", "post-carousel"));
307 }
308
309 if (response.success && response.data) {
310 const allDesignData = JSON.parse(response.data);
311 const sitesData = handleDesignData(allDesignData);
312 // if (isBlockPattern && currentBlockName) {
313 // sitesData = allDesignData[currentSitesName] || [];
314 // }
315
316
317 setState((prev) => ({
318 ...prev,
319 sitesCurrent: sitesData,
320 sitesDesign: sitesData,
321 loading: false,
322 error: false,
323 }));
324 } else {
325 throw new Error(response.message || __("Failed to load patterns", "post-carousel"));
326 }
327 } catch (error) {
328 console.error("Error fetching templates:", error);
329 setState((prev) => ({
330 ...prev,
331 loading: false,
332 error: error.message || __("An unexpected error occurred", "post-carousel"),
333 }));
334 }
335 };
336
337 // Force fetch and refresh local JSON cache
338 const fetchAllData = async () => {
339 setState((prev) => ({ ...prev, fetching: true }));;
340
341 try {
342 const response = await wp.apiFetch({
343 path: API_ENDPOINTS.PATTERNS,
344 method: "POST",
345 data: { type: "refresh" }, // tells PHP to re-fetch from remote
346 });
347
348 if (!response) {
349 throw new Error(__("No response received from server", "post-carousel"));
350 }
351
352 if (response.success) {
353 // after successful refresh, reload data
354 await fetchTemplates();
355 await fetchSitesData();
356 } else {
357 throw new Error(response.message || __("Failed to refresh patterns", "post-carousel"));
358 }
359 } catch (error) {
360 console.error("Error fetching all data:", error);
361 setState((prev) => ({
362 ...prev,
363 error: error.message || __("Failed to refresh data", "post-carousel"),
364 }));
365 } finally {
366 setState((prev) => ({ ...prev, fetching: false }));
367 }
368 };
369
370 // Close modal
371 const closeModal = () => {
372 const element = document.querySelector(".sp-smart-builder-modal");
373 if (element) {
374 element.remove();
375 }
376 setState((prev) => ({ ...prev, isPopup: false }));
377 };
378
379 // Handle ESC key press
380 const handleKeyDown = (e) => {
381 if (e.keyCode === KEYBOARD_KEYS.ESCAPE) {
382 closeModal();
383 }
384 };
385
386 // Insert block into editor
387 const insertBlock = async (templateID) => {
388 if (!templateID) {
389 console.log("Template ID is required");
390 return;
391 }
392
393 setState((prev) => ({ ...prev, reload: true, reloadId: templateID }));
394
395 try {
396 const response = await fetch(API_ENDPOINTS.SINGLE_PATTERN, {
397 method: "POST",
398 headers: {
399 "Content-Type": "application/x-www-form-urlencoded",
400 },
401 body: new URLSearchParams({
402 license: "",
403 template_id: templateID,
404 }),
405 });
406
407 if (!response.ok) {
408 throw new Error(`HTTP error! status: ${response.status}`);
409 }
410
411 const jsonData = await response.json();
412 if (jsonData.success && jsonData.rawData) {
413 const blockEditor = wp.data.dispatch("core/block-editor");
414 if (blockEditor && blockEditor.insertBlocks) {
415 // Parse the raw block data - parse() returns an array of blocks
416 let blocks = [];
417 try {
418 blocks = parse(jsonData.rawData);
419 } catch (parseError) {
420 console.error("Error parsing block data:", parseError);
421 throw new Error(__("Failed to parse block data", "post-carousel"));
422 }
423
424 // Validate that blocks is an array and not empty
425 if (!Array.isArray(blocks)) {
426 console.error("Parsed blocks is not an array:", blocks);
427 throw new Error(__("Invalid block data format", "post-carousel"));
428 }
429
430 if (blocks.length === 0) {
431 console.warn("No blocks found in parsed data");
432 throw new Error(__("No blocks found in pattern", "post-carousel"));
433 }
434
435 // Log parsed blocks for debugging (shows array length and structure)
436 console.log(`Parsed ${blocks.length} block(s):`, blocks);
437
438 // Modify block attributes after parsing but before insertion
439 // This allows you to change default attributes, reset IDs, etc.
440 blocks = modifyBlockAttributes(blocks);
441
442 // Log modified blocks for debugging
443 console.log(`Modified ${blocks.length} block(s) before insertion:`, blocks);
444
445 // Insert all blocks into the editor
446 // insertBlocks accepts an array of block objects
447 blockEditor.insertBlocks(blocks);
448
449 closeModal();
450 setState((prev) => ({
451 ...prev,
452 isPopup: false,
453 reload: false,
454 reloadId: "",
455 error: false,
456 }));
457 } else {
458 throw new Error(__("Block editor is not available", "post-carousel"));
459 }
460 } else {
461 throw new Error(jsonData.message || __("Failed to import pattern", "post-carousel"));
462 }
463 } catch (error) {
464 console.error("Error inserting block:", error);
465 setState((prev) => ({
466 ...prev,
467 error: error.message || __("Failed to import pattern", "post-carousel"),
468 reload: false,
469 }));
470 }
471 };
472
473 // Handle block import
474 const handleBlockImport = (templateID, isPro) => {
475 if (isPro && !localizedData.isPro) {
476 return;
477 }
478 insertBlock(templateID);
479 };
480
481 // Split archive data by category key.
482 const filterByCategoryKey = (data = [], key = "") => {
483 // Return early if no data or not an array
484 if (!Array.isArray(data) || data.length === 0) return [];
485
486 // If key is empty or 'all', return all data
487 if (!key || key === "all") return data;
488
489
490 return data.filter((item) => {
491 const category = item?.category;
492
493 // Handle category as array
494 if (Array.isArray(category)) {
495 const pattern = category.some((cat) => cat?.slug === key) || null;
496 const starter = category.includes( key ) || null;
497 return pattern || starter;
498 }
499
500 // Handle category as string or object
501 if (typeof category === "string") {
502 return category === key;
503 }
504
505 if (category && typeof category === "object") {
506 return category.slug === key;
507 }
508
509 return false;
510 });
511 };
512
513 // Handle wishlist actions
514 const handleWishlistAction = async (id, action = "", type = "") => {
515 try {
516 const response = await wp.apiFetch({
517 path: API_ENDPOINTS.WISHLIST,
518 method: "POST",
519 data: { id, action, type },
520 });
521
522 if (!response) {
523 throw new Error(__("No response received from server", "post-carousel"));
524 }
525
526 if (response.success) {
527 const wishlist = Array.isArray(response.wishListArr)
528 ? response.wishListArr
529 : Object.values(response.wishListArr || {});
530 setWishlistArr(wishlist);
531 } else {
532 throw new Error(response.message || __("Failed to update wishlist", "post-carousel"));
533 }
534 } catch (error) {
535 console.error("Error updating wishlist:", error);
536 // Optionally show user-friendly error message
537 }
538 };
539
540 // Initialize on mount
541 useEffect(() => {
542 handleWishlistAction("", "", "fetchData");
543 fetchTemplates();
544 fetchSitesData();
545 // fetchCategories();
546 document.addEventListener("keydown", handleKeyDown);
547
548 return () => {
549 document.removeEventListener("keydown", handleKeyDown);
550 };
551 }, []);
552
553 return (
554 <Fragment>
555 {isPopup && (
556 <ErrorBoundary
557 onError={(error, errorInfo) => {
558 console.error("Library Error Boundary:", error, errorInfo);
559 }}
560 onRetry={() => { fetchTemplates(); fetchSitesData() }}
561 >
562 <div className="sp-smart-builder-modal-shadow">
563 <div className="sp-smart-popup-wrap">
564 {!isBlockPattern && <LibraryHeader onClose={closeModal} tabState={tabState} setTabState={setTabState} />}
565 { tabState === "ready-pattern" && (
566 <ReadyPatterns
567 filterValue={designFilter}
568 currentBlockName={props.currentBlockName}
569 isSingleBlock={isBlockPattern}
570 onClose={closeModal}
571 state={state}
572 setState={setState}
573 _fetchFile={fetchAllData}
574 _changeVal={handleBlockImport}
575 filterByCategoryKey={filterByCategoryKey}
576 setWListAction={handleWishlistAction}
577 wishListArr={wishListArr}
578 setWishlistArr={setWishlistArr}
579 />
580 )}
581 </div>
582 </div>
583 </ErrorBoundary>
584 )}
585 </Fragment>
586 );
587 };
588
589 export default Library;
590