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

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