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 / blocks / smart-search / render.js

render.js in Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News 4.0.7, at src/blocks/smart-search/render.js

393 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import classNames from "classnames";
2 import { SearchIcon } from "./icons";
3 import { useState, useEffect, useRef, useCallback } from "@wordpress/element";
4 import useSearch from "../../hooks/useSearch";
5 import { Excerpt } from "../shared/templates/templates-parts/templates-parts";
6 import { findDataFromArray } from "../../controls/controls";
7 import useMetaData from "../../hooks/useMetaData";
8
9 const Render = ({ attributes }) => {
10 const { displayType, searchResultDisplayType } = attributes;
11 const [isPopupOpen, setIsPopupOpen] = useState(false);
12
13 return (
14 <>
15 {"popup" === displayType && (
16 <div className="sp-smart-post-search-trigger">
17 <button
18 className="sp-smart-post-search-trigger-button"
19 onClick={() => setIsPopupOpen((prev) => !prev)}
20 >
21 <SearchIcon />
22 </button>
23 </div>
24 )}
25 {displayType === "popup" && (
26 <div
27 className={classNames(
28 "sp-smart-post-search-overlay",
29 searchResultDisplayType,
30 isPopupOpen && "sp-d-block"
31 )}
32 onClick={() => setIsPopupOpen(false)}
33 ></div>
34 )}
35 <SmartSearchBody attributes={attributes} isPopupOpen={isPopupOpen} setIsPopupOpen={setIsPopupOpen} />
36 </>
37 );
38 };
39
40 const SmartSearchBody = ({ attributes, isPopupOpen, setIsPopupOpen }) => {
41 const { displayType, popupHeadingText, enablePopupHeading, popupHeadingGlobalTypography, enablePopupCloseButton } =
42 attributes;
43 const { posts, remaining_posts, fetchPosts } = useSearch(attributes);
44 const [isLoading, setIsLoading] = useState(false);
45 const [searchText, setSearchText] = useState("");
46 const [isShow, setIsShow] = useState(!!searchText && !isLoading);
47 const [popupClass, setPopupClass] = useState("");
48 const searchRef = useRef(null);
49
50 useEffect(() => {
51 if (!!searchText && !isLoading) {
52 setIsShow(true);
53 }
54 }, [searchText, isLoading]);
55
56 useEffect(() => {
57 if (!searchRef.current) {
58 return;
59 }
60
61 const updatePosition = () => {
62 const rect = searchRef.current.getBoundingClientRect();
63 const viewportWidth = document.documentElement.clientWidth;
64
65 if (rect.right > viewportWidth) {
66 setPopupClass("sp-popup-position-right");
67 } else {
68 setPopupClass("sp-popup-position-left");
69 }
70 };
71
72 updatePosition();
73 window.addEventListener("resize", updatePosition);
74
75 return () => {
76 window.removeEventListener("resize", updatePosition);
77 };
78 }, []);
79
80 return (
81 <div
82 ref={searchRef}
83 tabIndex={-1}
84 onBlur={(e) => {
85 // If focus left the entire search wrapper → close dropdown
86 if (!searchRef.current.contains(e.relatedTarget)) {
87 setIsShow(false);
88 }
89 }}
90 className={classNames(
91 "sp-smart-post-search-wrapper",
92 displayType === "popup" && "sp-smart-post-search-popup",
93 isPopupOpen && displayType === "popup" && "sp-smart-post-search-popup--active",
94 popupClass
95 )}
96 >
97 {displayType === "popup" && enablePopupHeading && (
98 <h4
99 className={classNames(
100 "sp-smart-post-popup-heading",
101 popupHeadingGlobalTypography?.class ? popupHeadingGlobalTypography?.class : ""
102 )}
103 >
104 {popupHeadingText}
105 </h4>
106 )}
107 {displayType === "popup" && enablePopupCloseButton && (
108 <span className="sp-smart-post-popup-close-icon" onClick={() => setIsPopupOpen(false)}>
109 <svg width={10} height={10} viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
110 <path
111 d="M5.00003 6.06069L8.71233 9.77299L9.77299 8.71233L6.06069 5.00003L9.773 1.28772L8.71234 0.227059L5.00003 3.93937L1.28771 0.227051L0.227051 1.28771L3.93937 5.00003L0.22706 8.71234L1.28772 9.773L5.00003 6.06069Z"
112 fill="#1E1E1E"
113 />
114 </svg>
115 </span>
116 )}
117 {/* <select>
118 <option>01</option>
119 <option>02</option>
120 <option>03</option>
121 <option>04</option>
122 </select> */}
123 <SearchForm
124 attributes={attributes}
125 setIsLoading={setIsLoading}
126 searchText={searchText}
127 setSearchText={setSearchText}
128 isLoading={isLoading}
129 fetchPosts={fetchPosts}
130 />
131
132 <SearchResult
133 isShow={isShow}
134 attributes={attributes}
135 posts={posts}
136 remaining_posts={remaining_posts}
137 isLoading={isLoading}
138 />
139 </div>
140 );
141 };
142
143 const SearchForm = ({ attributes, setSearchText, setIsLoading, searchText, isLoading, fetchPosts }) => {
144 const {
145 searchFormPreset,
146 placeholder,
147 placeholderEnable,
148 searchBtnLabel,
149 searchIcon,
150 inputPlaceHolderGlobalTypography,
151 selectedTaxonomy,
152 selectedTerms,
153 postType,
154 filterLabelText,
155 taxonomyFilterEnable,
156 searchBtnLabelText,
157 } = attributes;
158 const { allTaxonomies } = useMetaData(attributes, "editSite");
159 const selectedTaxonomyData = findDataFromArray(allTaxonomies, "name", selectedTaxonomy);
160 const _terms = selectedTaxonomyData?.terms_items.map((item) => ({
161 ...item,
162 id: item.value,
163 }));
164 const terms = selectedTerms?.length > 0 ? selectedTerms : _terms;
165
166 const [selectedTerm, setSelectedTerm] = useState("");
167
168 const debounceTimer = useRef(null);
169 const inputRef = useRef(null);
170
171 const handleSearch = useCallback(
172 async (query, term) => {
173 try {
174 setIsLoading(true);
175 await fetchPosts(query, term);
176 } catch (error) {
177 console.log("Error fetching posts:", error.message);
178 } finally {
179 setIsLoading(false);
180 }
181 },
182 [searchText, selectedTerm, postType]
183 );
184
185 useEffect(() => {
186 if (debounceTimer.current) {
187 clearTimeout(debounceTimer.current);
188 }
189
190 if (searchText) {
191 debounceTimer.current = setTimeout(() => {
192 handleSearch(searchText, selectedTerm);
193 }, 500);
194 }
195
196 return () => clearTimeout(debounceTimer.current);
197 }, [searchText, handleSearch, postType]);
198
199 useEffect(() => {
200 if (selectedTerm) {
201 handleSearch(searchText, selectedTerm);
202 }
203 }, [selectedTerm]);
204
205 const onSubmit = (e) => {
206 e.preventDefault();
207 handleSearch(searchText, selectedTerm);
208 };
209 const handleTermChange = (e) => {
210 e.stopPropagation();
211 const value = e.target.value;
212 if (value) {
213 setSelectedTerm(value);
214 }
215 };
216 const formHandler = (e) => {
217 e.stopPropagation();
218 inputRef.current.focus();
219 };
220
221 return (
222 <form
223 role="search"
224 onSubmit={onSubmit}
225 method="get"
226 className={classNames("sp-smart-post-search-form", searchFormPreset)}
227 // onClick={(e) => formHandler ( e )}
228 >
229 <div className="sp-smart-post-search-input-wrapper">
230 <input
231 ref={inputRef}
232 type="search"
233 className={classNames(
234 "sp-smart-post-search-input",
235 inputPlaceHolderGlobalTypography?.class ? inputPlaceHolderGlobalTypography?.class : ""
236 )}
237 {...(placeholder && placeholderEnable ? { placeholder } : {})}
238 name="s"
239 value={searchText}
240 required
241 readOnly={isLoading}
242 onChange={(e) => setSearchText(e.target.value)}
243 />
244 {terms?.length > 0 && postType !== "" && taxonomyFilterEnable && (
245 <div className="sp-smart-post-search-select">
246 <span className="sp-smart-post-search-dropdown-icon">
247 <svg
248 width={21}
249 height={20}
250 viewBox="0 0 21 20"
251 fill="none"
252 xmlns="http://www.w3.org/2000/svg"
253 >
254 <path
255 d="M5.7373 6L10.7373 11L15.7373 6L17.7373 7L10.7373 14L3.7373 7L5.7373 6Z"
256 fill="#2F2F2F"
257 />
258 </svg>
259 </span>
260 <select className="sp-smart-post-search-cate-list" onChange={handleTermChange}>
261 <option value="">{filterLabelText || "Filter by"}</option>
262 {terms.map((term) => (
263 <option key={term.slug} value={term.id}>
264 {term.label}
265 </option>
266 ))}
267 </select>
268 </div>
269 )}
270 </div>
271
272 <button type="submit" className={"sp-smart-post-search-button"}>
273 {searchIcon && <SearchIcon />}
274 {!["smart-search-form-preset-one", "smart-search-form-preset-two"].includes(searchFormPreset) &&
275 searchBtnLabel &&
276 searchBtnLabelText}
277 </button>
278 </form>
279 );
280 };
281
282 const SearchResult = ({ attributes, posts, remaining_posts, isShow }) => {
283 const {
284 searchResultDisplayType,
285 searchResultNotFoundText,
286 moreResultLabelText,
287 moreResultsEnable,
288 showResultsCount,
289 } = attributes;
290
291 return (
292 <div className={classNames("sp-smart-post-search-results", isShow && "sp-smart-post-search-results--show")}>
293 <div className={classNames("sp-smart-post-search-list", searchResultDisplayType)}>
294 {posts?.length > 0 ? (
295 posts.map((post, index) => {
296 return <SearchItem key={index} post={post} attributes={attributes} />;
297 })
298 ) : (
299 <div className="sp-smart-post-search-empty">{searchResultNotFoundText}</div>
300 )}
301 </div>
302
303 {/* TODO: Need to manage loading state if needed in future */}
304 {/* {isLoading ? (
305 <div className="sp-smart-post-search-loader">
306 <LoaderIcon />
307 </div>
308 ) : (
309 <>
310
311 </>
312 )} */}
313 {moreResultsEnable && remaining_posts > 0 && (
314 <div className="sp-smart-post-search-load-more">
315 <button type="button" className="sp-smart-post-search-load-more-button">
316 {moreResultLabelText} {showResultsCount && remaining_posts + "+" + " " + "Posts"}
317 </button>
318 </div>
319 )}
320 </div>
321 );
322 };
323
324 const SearchItem = ({ post, attributes }) => {
325 const { categories, author, image_alt, post_thumbnail_url, date, excerpt, postTitle } = post;
326
327 const {
328 searchResultDisplayType,
329 searchResultTitleGlobalTypography,
330 searchResultMetaGlobalTypography,
331 searchResultShowImage,
332 searchResultShowExcerpt,
333 searchResultShowTaxonomy,
334 searchResultExcerptLimit,
335 searchResultShowDate,
336 searchResultShowAuthor,
337 } = attributes;
338
339 return (
340 <div className="sp-smart-post-search-item">
341 {searchResultDisplayType !== "smart-search-result-layout-two" && searchResultShowImage && (
342 <div className="sp-smart-post-search-item-img">
343 <img
344 src={
345 post_thumbnail_url
346 ? post_thumbnail_url
347 : sp_smart_post_block_localize.placeholderImg + "public/assets/img/placeholder.png"
348 }
349 alt={image_alt ? image_alt : postTitle}
350 />
351 </div>
352 )}
353 <div className="sp-smart-post-search-item-desc">
354 {searchResultShowTaxonomy && categories?.length > 0 && (
355 <div className={classNames("sp-smart-post-categories", searchResultMetaGlobalTypography?.class)}>
356 {categories?.map((category, index) => {
357 return (
358 <span key={index} className="sp-smart-post-category">
359 {category}
360 </span>
361 );
362 })}
363 </div>
364 )}
365 <a
366 // href={"#"}
367 className={classNames("sp-smart-post-search-item-title", searchResultTitleGlobalTypography?.class)}
368 >
369 {post?.title}
370 </a>
371 {searchResultShowExcerpt && excerpt && (
372 <Excerpt
373 excerpt={excerpt}
374 attributes={{
375 ellipsisPointsEndingExcerpt: "...",
376 excerptType: "limited",
377 excerptLength: searchResultExcerptLimit,
378 excerptGlobalTypography: searchResultMetaGlobalTypography,
379 }}
380 />
381 )}
382 {(searchResultShowDate || searchResultShowAuthor) && (
383 <div className={classNames("sp-smart-post-meta-list", searchResultMetaGlobalTypography?.class)}>
384 {author && searchResultShowAuthor && <span className="sp-smart-post-meta">{author}, </span>}
385 {date && searchResultShowDate && <span className="sp-smart-post-meta">{date}</span>}
386 </div>
387 )}
388 </div>
389 </div>
390 );
391 };
392 export default Render;
393