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

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