PluginProbe
Extendify / 3.1.4
Extendify v3.1.4
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.1.4, at src/Agent/workflows/theme/components/change-site-design/SelectSiteDesign.jsx

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