PluginProbe
Extendify / 3.0.6
Extendify v3.0.6
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Agent / workflows / theme / components / change-site-design / SelectSiteDesign.jsx

SelectSiteDesign.jsx in Extendify 3.0.6, at src/Agent/workflows/theme/components/change-site-design/SelectSiteDesign.jsx

398 lines 11.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useSiteVibesOverride } from '@agent/hooks/useSiteVibesOverride';
2 import { useSiteVibesVariations } from '@agent/hooks/useSiteVibesVariations';
3 import { useVariationOverride } from '@agent/hooks/useVariationOverride';
4 import { DesignOption } from '@agent/workflows/theme/components/change-site-design/DesignOption';
5 import { removeAnimationClasses } from '@agent/workflows/theme/components/change-site-design/utils/removeAnimationClasses';
6 import apiFetch from '@wordpress/api-fetch';
7 import { registerCoreBlocks } from '@wordpress/block-library';
8 import { getBlockTypes, parse, serialize } from '@wordpress/blocks';
9 import { Spinner } from '@wordpress/components';
10 import { useDispatch } from '@wordpress/data';
11 import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
12 import { __ } from '@wordpress/i18n';
13 import classnames from 'classnames';
14
15 let originalHeroElement = null;
16
17 const undoHeroSectionChange = () => {
18 const preview = document.querySelector('.ext-hero-section-preview');
19 if (preview && originalHeroElement) {
20 preview.replaceWith(originalHeroElement);
21 } else {
22 preview?.remove();
23 }
24 originalHeroElement = null;
25 };
26
27 const updateHeroSection = (content) => {
28 const tempDiv = document.createElement('div');
29 tempDiv.innerHTML = content;
30 const newNode = tempDiv.firstElementChild;
31 newNode.classList.add('ext-hero-section-preview');
32
33 const existingPreview = document.querySelector('.ext-hero-section-preview');
34 if (existingPreview) {
35 existingPreview.replaceWith(newNode);
36 return;
37 }
38
39 const heroSectionElement = document.querySelector('.ext-hero-section');
40 if (heroSectionElement) {
41 originalHeroElement = heroSectionElement;
42 heroSectionElement.replaceWith(newNode);
43 return;
44 }
45
46 const contentArea =
47 document.querySelector('.entry-content') ?? document.querySelector('main');
48 if (!contentArea) return;
49 contentArea.insertAdjacentElement('afterbegin', newNode);
50 };
51
52 const { context } = window.extAgentData;
53 const isAdmin = context?.adminPage;
54
55 const PAGE_SIZE = 5;
56
57 export const SelectSiteDesign = ({ onConfirm, onCancel }) => {
58 const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
59 const [isLoading, setIsLoading] = useState(false);
60 const [isSaving, setIsSaving] = useState(false);
61
62 const [heroPatterns, setHeroPatterns] = useState();
63 const [colorAndFontsVariations, setColorAndFontsVariations] = useState();
64 const [blockEditorStyles, setBlockEditorStyles] = useState();
65 const [currentHeroHtml, setCurrentHeroHtml] = useState();
66 const [currentCssVibe, setCurrentCssVibe] = useState();
67
68 const [selectedColorAndFonts, setSelectedColorAndFonts] = useState();
69 const [selectedHeroPattern, setSelectedHeroPattern] = useState();
70 const [selectedVibe, setSelectedVibe] = useState();
71
72 const { updateSettings } = useDispatch('core/block-editor');
73
74 const injectedLinksRef = useRef([]);
75
76 const injectLinkStyles = (linkStyles) => {
77 injectedLinksRef.current.forEach((link) => {
78 link.remove();
79 });
80 injectedLinksRef.current = [];
81
82 (linkStyles ?? []).forEach((href) => {
83 if (document.querySelector(`link[href="${href}"]`)) return;
84 const link = document.createElement('link');
85 link.rel = 'stylesheet';
86 link.href = href;
87 document.head.appendChild(link);
88 injectedLinksRef.current.push(link);
89 });
90 };
91
92 const removeInjectedLinks = () => {
93 injectedLinksRef.current.forEach((link) => {
94 link.remove();
95 });
96 injectedLinksRef.current = [];
97 };
98
99 const { undoChange: undoColorAndFontsChange } = useVariationOverride({
100 css: !isAdmin && selectedColorAndFonts?.css,
101 duotoneTheme:
102 !isAdmin && selectedColorAndFonts?.settings?.color?.duotone?.theme,
103 });
104
105 const { undoChange: undoVibesChange } = useSiteVibesOverride({
106 css: !isAdmin && selectedVibe?.css,
107 slug: !isAdmin && selectedVibe?.slug,
108 });
109
110 const { data: vibesData, isLoading: isLoadingVibes } =
111 useSiteVibesVariations();
112
113 const vibes = useMemo(() => {
114 if (isLoadingVibes) return null;
115
116 const vibes = Object.entries(vibesData.css)
117 .filter(([slug]) => slug !== vibesData.currentVibe)
118 .map(([slug, css]) => ({
119 slug,
120 css: css?.replaceAll(slug, 'natural-1'),
121 }))
122 .sort(() => Math.random() - 0.5);
123
124 return [...vibes, ...vibes.slice(0, 3)];
125 }, [vibesData, isLoadingVibes]);
126
127 useEffect(() => {
128 window.scrollTo({ top: 0, behavior: 'smooth' });
129 const heroEl = document.querySelector('.ext-hero-section');
130 const cssVibeEl = document.getElementById(
131 'block-style-variation-styles-inline-css',
132 );
133
134 if (heroEl) setCurrentHeroHtml(removeAnimationClasses(heroEl).outerHTML);
135 if (cssVibeEl) setCurrentCssVibe(cssVibeEl.textContent);
136 }, []);
137
138 useEffect(() => {
139 setIsLoading(true);
140
141 const heroSectionElement = document.querySelector('.ext-hero-section');
142
143 const title = heroSectionElement?.querySelector('h1')?.textContent ?? null;
144 const description =
145 heroSectionElement?.querySelector('p')?.textContent ?? null;
146 const cta =
147 heroSectionElement?.querySelector('.wp-block-button__link') ?? null;
148 const heroPatternName =
149 [...(heroSectionElement?.classList ?? [])]
150 .find((className) => className.startsWith('ext-hero-section--'))
151 ?.replace('ext-hero-section--', '') ?? null;
152 const images = [...(heroSectionElement?.querySelectorAll('img') ?? [])]
153 .filter(
154 (img) =>
155 !img.src.includes('.svg') && !img.src.includes('data:image/svg+xml'),
156 )
157 .map((img) => {
158 try {
159 const url = new URL(img.src);
160 return url.origin + url.pathname;
161 } catch {
162 return img.src;
163 }
164 });
165
166 apiFetch({
167 path: '/extendify/v1/agent/site-design-variations',
168 method: 'POST',
169 data: {
170 title,
171 images,
172 description,
173 currentHeroPattern: heroPatternName,
174 cta: {
175 label: cta?.textContent,
176 link: cta?.href,
177 },
178 },
179 })
180 .then((data) => {
181 setHeroPatterns(data?.patterns?.flat() ?? []);
182 setColorAndFontsVariations(
183 [...(data?.colorAndFontsVariations ?? [])].sort(
184 () => Math.random() - 0.5,
185 ),
186 );
187
188 setBlockEditorStyles(data?.blockEditorSettings);
189 })
190 .finally(() => {
191 setIsLoading(false);
192 });
193 }, []);
194
195 useEffect(() => {
196 if (blockEditorStyles) {
197 updateSettings(blockEditorStyles);
198 }
199 }, [blockEditorStyles, updateSettings]);
200
201 const currentDesignOption = useMemo(
202 () => ({ id: 'current', isCurrent: true, renderedHtml: currentHeroHtml }),
203 [currentHeroHtml],
204 );
205
206 useEffect(() => {
207 if (currentDesignOption) setSelectedHeroPattern(currentDesignOption);
208 }, [currentDesignOption]);
209
210 const visibleHeroPatterns = heroPatterns?.slice(0, visibleCount);
211 const hasMore = visibleCount < heroPatterns?.length;
212
213 const undoChanges = () => {
214 undoHeroSectionChange();
215 undoColorAndFontsChange();
216 undoVibesChange();
217 removeInjectedLinks();
218 };
219
220 const handleCancel = () => {
221 undoChanges();
222 onCancel();
223 };
224
225 const handleConfirm = async () => {
226 if (!selectedHeroPattern) return;
227
228 if (selectedHeroPattern.isCurrent) {
229 onConfirm({
230 data: { postId: context?.postId },
231 shouldRefreshPage: false,
232 });
233 return;
234 }
235
236 if (!selectedVibe || !selectedColorAndFonts) return;
237
238 const postId = context?.postId;
239
240 try {
241 const page = await apiFetch({
242 path: `/wp/v2/pages/${postId}?context=edit`,
243 });
244
245 // parse() depends on block types being registered
246 if (getBlockTypes().length === 0) registerCoreBlocks();
247
248 const pageBlocks = parse(page.content.raw);
249
250 let heroPatternUpdated = false;
251 const updatedPageBlocks = serialize(
252 pageBlocks.map((block) => {
253 if (
254 heroPatternUpdated ||
255 !block.attributes.className.split(' ').includes('ext-hero-section')
256 )
257 return block;
258
259 heroPatternUpdated = true;
260
261 return parse(selectedHeroPattern.code)?.[0] || block;
262 }),
263 );
264
265 onConfirm({
266 data: {
267 updatedPageBlocks,
268 postId,
269 vibeSlug: selectedVibe.slug,
270 colorAndFontsVariation: selectedColorAndFonts,
271 },
272 shouldRefreshPage: true,
273 });
274 } catch (error) {
275 console.log(error);
276 } finally {
277 setIsSaving(false);
278 }
279 };
280
281 if (isLoading || isLoadingVibes) {
282 return (
283 <div className="flex justify-center flex-col gap-1">
284 <Spinner />
285 </div>
286 );
287 }
288
289 return (
290 <div className="mb-4 ml-10 mr-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50 rtl:ml-2 rtl:mr-10">
291 <div className="rounded-lg border-b border-gray-300 bg-white">
292 <div className="flex flex-col gap-4 p-3">
293 {currentHeroHtml && (
294 <DesignOption
295 renderedHtml={currentHeroHtml}
296 isSelected={selectedHeroPattern?.id === 'current'}
297 styles={{ vibes: currentCssVibe }}
298 onClick={() => {
299 setSelectedHeroPattern(currentDesignOption);
300 setSelectedColorAndFonts(null);
301 setSelectedVibe(null);
302
303 undoChanges();
304 }}
305 />
306 )}
307 {visibleHeroPatterns?.map((heroPattern, i) => (
308 <DesignOption
309 key={heroPattern.id}
310 renderedHtml={heroPattern.renderedHtml}
311 isSelected={selectedHeroPattern?.id === heroPattern.id}
312 styles={{
313 linkStyles: heroPattern.linkStyles,
314 colorAndFontsVariations: colorAndFontsVariations[i].css,
315 duotoneTheme:
316 colorAndFontsVariations[i]?.settings?.color?.duotone?.theme,
317 vibes: vibes[i]?.css,
318 blockSupportsCss: heroPattern.blockSupportsCss,
319 }}
320 onClick={() => {
321 setSelectedHeroPattern(heroPattern);
322 setSelectedColorAndFonts(colorAndFontsVariations[i]);
323 setSelectedVibe(vibes[i]);
324
325 if (!isAdmin) {
326 const blockSupportsCss = heroPattern.blockSupportsCss;
327 if (blockSupportsCss) {
328 let el = document.getElementById('ext-block-supports-css');
329 if (!el) {
330 el = document.createElement('style');
331 el.id = 'ext-block-supports-css';
332 document.head.appendChild(el);
333 }
334 el.textContent = blockSupportsCss;
335 }
336
337 injectLinkStyles(heroPattern.linkStyles);
338
339 updateHeroSection(heroPattern.renderedHtml);
340 }
341 }}
342 />
343 ))}
344
345 {hasMore && (
346 <button
347 type="button"
348 className={classnames(
349 'w-full rounded-sm border border-gray-300 bg-white p-2 text-sm text-gray-800',
350 {
351 'hover:bg-gray-50': !isSaving,
352 },
353 )}
354 onClick={() => setVisibleCount((value) => value + PAGE_SIZE)}
355 disabled={isSaving}
356 >
357 {__('Load more', 'extendify-local')}
358 </button>
359 )}
360 </div>
361 </div>
362 <div className="flex justify-start gap-2 p-3">
363 <button
364 type="button"
365 className={classnames(
366 'w-full rounded-sm border border-gray-300 bg-white p-2 text-sm text-gray-800',
367 {
368 'hover:bg-gray-50': !isSaving,
369 },
370 )}
371 onClick={handleCancel}
372 disabled={isSaving}
373 >
374 {__('Cancel', 'extendify-local')}
375 </button>
376 <button
377 type="button"
378 className={classnames(
379 'w-full rounded-sm border border-design-main bg-design-main p-2 text-sm text-white',
380 {
381 'cursor-not-allowed': !selectedHeroPattern || isSaving,
382 'hover:bg-gray-800': !isSaving,
383 },
384 )}
385 disabled={!selectedHeroPattern || isSaving}
386 onClick={handleConfirm}
387 >
388 {isSaving ? (
389 <Spinner className="m-0" />
390 ) : (
391 __('Save', 'extendify-local')
392 )}
393 </button>
394 </div>
395 </div>
396 );
397 };
398