PluginProbe
Extendify / trunk
Extendify vtrunk
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
← All changes | src/Agent/workflows/theme/components/change-site-design/SelectSiteDesign.jsx +162 -52 3.0.4 → trunk View file →
@@ -1,9 +1,19 @@
1 +import { usePaletteOverride } from '@agent/hooks/usePaletteOverride';
2 +import { usePalettes } from '@agent/hooks/usePalettes';
1 3 import { useSiteVibesOverride } from '@agent/hooks/useSiteVibesOverride';
2 4 import { useSiteVibesVariations } from '@agent/hooks/useSiteVibesVariations';
3 5 import { useVariationOverride } from '@agent/hooks/useVariationOverride';
6 +import { refreshBlockHighlight } from '@agent/lib/block-highlight';
4 7 import { DesignOption } from '@agent/workflows/theme/components/change-site-design/DesignOption';
8 +import { fontsOnlyVariation } from '@agent/workflows/theme/components/change-site-design/utils/fontsOnlyVariation';
5 9 import { removeAnimationClasses } from '@agent/workflows/theme/components/change-site-design/utils/removeAnimationClasses';
10 +import { handleSiteImages } from '@auto-launch/fetchers/get-images';
11 +import { useUserSelectionStore } from '@launch/state/user-selections';
12 +import { paletteDuotone } from '@shared/lib/palette-preview';
13 +import { samplePalettes } from '@shared/lib/palettes';
14 +import { safeParseJson } from '@shared/lib/parsing';
15 +import { normalizeSiteImages } from '@shared/lib/site-images';
6 16 import apiFetch from '@wordpress/api-fetch';
7 17 import { registerCoreBlocks } from '@wordpress/block-library';
8 18 import { getBlockTypes, parse, serialize } from '@wordpress/blocks';
9 19 import { Spinner } from '@wordpress/components';
@@ -11,37 +21,43 @@
11 21 import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
12 22 import { __ } from '@wordpress/i18n';
13 23 import classnames from 'classnames';
14 24
25 +let originalHeroElement = null;
26 +
15 27 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';
28 + const preview = document.querySelector('.ext-hero-section-preview');
29 + if (preview && originalHeroElement) {
30 + preview.replaceWith(originalHeroElement);
31 + } else {
32 + preview?.remove();
33 + }
34 + originalHeroElement = null;
21 35 };
22 36
23 37 const updateHeroSection = (content) => {
24 - document.querySelector('.ext-hero-section-preview')?.remove();
38 + const tempDiv = document.createElement('div');
39 + tempDiv.innerHTML = content;
40 + const newNode = tempDiv.firstElementChild;
41 + newNode.classList.add('ext-hero-section-preview');
25 42
43 + const existingPreview = document.querySelector('.ext-hero-section-preview');
44 + if (existingPreview) {
45 + existingPreview.replaceWith(newNode);
46 + return;
47 + }
48 +
26 49 const heroSectionElement = document.querySelector('.ext-hero-section');
27 -
28 50 if (heroSectionElement) {
29 - heroSectionElement.style.display = 'none';
51 + originalHeroElement = heroSectionElement;
52 + heroSectionElement.replaceWith(newNode);
53 + return;
30 54 }
31 55
32 56 const contentArea =
33 57 document.querySelector('.entry-content') ?? document.querySelector('main');
34 -
35 58 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');
59 + contentArea.insertAdjacentElement('afterbegin', newNode);
44 60 };
45 61
46 62 const { context } = window.extAgentData;
47 63 const isAdmin = context?.adminPage;
@@ -47,8 +63,31 @@
47 63 const isAdmin = context?.adminPage;
48 64
49 65 const PAGE_SIZE = 5;
50 66
67 +const hasImages = ({ hero, general }) => hero.length > 0 || general.length > 0;
68 +
69 +const resolveSiteImages = async ({ storedSiteImages, siteProfile }) => {
70 + const stored = normalizeSiteImages(storedSiteImages);
71 + if (hasImages(stored)) return stored;
72 +
73 + // A browser that didn't run Launch otherwise reuses one image on every option.
74 + const localized = normalizeSiteImages(window.extSharedData.siteImages);
75 + if (hasImages(localized)) return localized;
76 +
77 + if (siteProfile) {
78 + const { siteImages } = await handleSiteImages({ siteProfile });
79 + const fetched = normalizeSiteImages(siteImages);
80 + if (hasImages(fetched)) return fetched;
81 + }
82 +
83 + const saved = await apiFetch({
84 + path: '/extendify/v1/shared/site-images',
85 + }).catch(() => null);
86 +
87 + return normalizeSiteImages(saved?.siteImages);
88 +};
89 +
51 90 export const SelectSiteDesign = ({ onConfirm, onCancel }) => {
52 91 const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
53 92 const [isLoading, setIsLoading] = useState(false);
54 93 const [isSaving, setIsSaving] = useState(false);
@@ -61,8 +100,9 @@
61 100
62 101 const [selectedColorAndFonts, setSelectedColorAndFonts] = useState();
63 102 const [selectedHeroPattern, setSelectedHeroPattern] = useState();
64 103 const [selectedVibe, setSelectedVibe] = useState();
104 + const [selectedPalette, setSelectedPalette] = useState();
65 105
66 106 const { updateSettings } = useDispatch('core/block-editor');
67 107
68 108 const injectedLinksRef = useRef([]);
@@ -89,14 +129,35 @@
89 129 });
90 130 injectedLinksRef.current = [];
91 131 };
92 132
133 + const {
134 + palettes,
135 + preferred,
136 + css: paletteCss,
137 + isLoading: isLoadingPalettes,
138 + } = usePalettes();
139 + const onPalettes = Boolean(palettes?.length);
140 + const shownPalettes = useMemo(
141 + () => samplePalettes(palettes, preferred),
142 + [palettes, preferred],
143 + );
144 + const paletteFor = (index) =>
145 + onPalettes ? shownPalettes[index % shownPalettes.length] : undefined;
146 +
93 147 const { undoChange: undoColorAndFontsChange } = useVariationOverride({
94 148 css: !isAdmin && selectedColorAndFonts?.css,
95 149 duotoneTheme:
96 - !isAdmin && selectedColorAndFonts?.settings?.color?.duotone?.theme,
150 + !isAdmin &&
151 + !onPalettes &&
152 + selectedColorAndFonts?.settings?.color?.duotone?.theme,
97 153 });
98 154
155 + const { undoChange: undoPaletteChange } = usePaletteOverride({
156 + css: (!isAdmin && paletteCss?.[selectedPalette?.slug]) || '',
157 + duotoneTheme: (!isAdmin && paletteDuotone(selectedPalette)) || null,
158 + });
159 +
99 160 const { undoChange: undoVibesChange } = useSiteVibesOverride({
100 161 css: !isAdmin && selectedVibe?.css,
101 162 slug: !isAdmin && selectedVibe?.slug,
102 163 });
@@ -104,9 +165,10 @@
104 165 const { data: vibesData, isLoading: isLoadingVibes } =
105 166 useSiteVibesVariations();
106 167
107 168 const vibes = useMemo(() => {
108 - if (isLoadingVibes) return null;
169 + // Null for a theme with no block variations, which .css would throw on.
170 + if (isLoadingVibes || !vibesData?.css) return [];
109 171
110 172 const vibes = Object.entries(vibesData.css)
111 173 .filter(([slug]) => slug !== vibesData.currentVibe)
112 174 .map(([slug, css]) => ({
@@ -111,8 +173,13 @@
111 173 .filter(([slug]) => slug !== vibesData.currentVibe)
112 174 .map(([slug, css]) => ({
113 175 slug,
114 176 css: css?.replaceAll(slug, 'natural-1'),
177 + // Css only adds, so a cloned page keeps the applied vibe's slots.
178 + previewCss: `${vibesData.resets?.[slug] ?? ''}${css ?? ''}`.replaceAll(
179 + slug,
180 + 'natural-1',
181 + ),
115 182 }))
116 183 .sort(() => Math.random() - 0.5);
117 184
118 185 return [...vibes, ...vibes.slice(0, 3)];
@@ -138,9 +205,13 @@
138 205 const description =
139 206 heroSectionElement?.querySelector('p')?.textContent ?? null;
140 207 const cta =
141 208 heroSectionElement?.querySelector('.wp-block-button__link') ?? null;
142 - const images = [...(heroSectionElement?.querySelectorAll('img') ?? [])]
209 + const heroPatternName =
210 + heroSectionElement?.className?.match(
211 + /ext-hero-section ext-hero-section--(\S+)/,
212 + )?.[1] ?? null;
213 + const domImages = [...(heroSectionElement?.querySelectorAll('img') ?? [])]
143 214 .filter(
144 215 (img) =>
145 216 !img.src.includes('.svg') && !img.src.includes('data:image/svg+xml'),
146 217 )
@@ -152,34 +223,54 @@
152 223 return img.src;
153 224 }
154 225 });
155 226
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,
227 + const { siteId } = window.extSharedData;
228 + const stored = safeParseJson(
229 + localStorage.getItem(`extendify-launch-data-${siteId}`),
230 + );
231 + const launchState = useUserSelectionStore.getState();
232 + const storedSiteImages =
233 + stored?.state?.siteImages ?? launchState?.siteImages?.siteImages ?? [];
234 + const siteProfile = stored?.state?.siteProfile ?? launchState?.siteProfile;
235 +
236 + (async () => {
237 + const siteImages = await resolveSiteImages({
238 + storedSiteImages,
239 + siteProfile,
240 + });
241 +
242 + apiFetch({
243 + path: '/extendify/v1/agent/site-design-variations',
244 + method: 'POST',
245 + data: {
246 + title,
247 + images: domImages,
248 + siteImages,
249 + postId: context?.postId,
250 + description,
251 + currentHeroPattern: heroPatternName,
252 + source: 'change-site-design-workflow',
253 + cta: {
254 + label: cta?.textContent,
255 + link: cta?.href,
256 + },
166 257 },
167 - },
168 - })
169 - .then((data) => {
170 - setHeroPatterns(data?.patterns?.flat() ?? []);
171 - setColorAndFontsVariations(
172 - [...(data?.colorAndFontsVariations ?? [])].sort(
173 - () => Math.random() - 0.5,
174 - ),
175 - );
258 + })
259 + .then((data) => {
260 + setHeroPatterns(data?.patterns?.flat() ?? []);
261 + setColorAndFontsVariations(
262 + [...(data?.colorAndFontsVariations ?? [])].sort(
263 + () => Math.random() - 0.5,
264 + ),
265 + );
176 266
177 - setBlockEditorStyles(data?.blockEditorSettings);
178 - })
179 - .finally(() => {
180 - setIsLoading(false);
181 - });
267 + setBlockEditorStyles(data?.blockEditorSettings);
268 + })
269 + .finally(() => {
270 + setIsLoading(false);
271 + });
272 + })();
182 273 }, []);
183 274
184 275 useEffect(() => {
185 276 if (blockEditorStyles) {
@@ -201,12 +292,21 @@
201 292
202 293 const undoChanges = () => {
203 294 undoHeroSectionChange();
204 295 undoColorAndFontsChange();
296 + undoPaletteChange();
205 297 undoVibesChange();
206 298 removeInjectedLinks();
299 + refreshBlockHighlight();
207 300 };
208 301
302 + const confirmed = useRef(false);
303 + useEffect(() => {
304 + return () => {
305 + if (!confirmed.current) undoChanges();
306 + };
307 + }, []);
308 +
209 309 const handleCancel = () => {
210 310 undoChanges();
211 311 onCancel();
212 312 };
@@ -214,8 +314,9 @@
214 314 const handleConfirm = async () => {
215 315 if (!selectedHeroPattern) return;
216 316
217 317 if (selectedHeroPattern.isCurrent) {
318 + confirmed.current = true;
218 319 onConfirm({
219 320 data: { postId: context?.postId },
220 321 shouldRefreshPage: false,
221 322 });
@@ -250,14 +351,18 @@
250 351 return parse(selectedHeroPattern.code)?.[0] || block;
251 352 }),
252 353 );
253 354
355 + confirmed.current = true;
254 356 onConfirm({
255 357 data: {
256 358 updatedPageBlocks,
257 359 postId,
258 360 vibeSlug: selectedVibe.slug,
259 - colorAndFontsVariation: selectedColorAndFonts,
361 + colorAndFontsVariation: onPalettes
362 + ? fontsOnlyVariation(selectedColorAndFonts)
363 + : selectedColorAndFonts,
364 + colorPalette: selectedPalette?.slug,
260 365 },
261 366 shouldRefreshPage: true,
262 367 });
263 368 } catch (error) {
@@ -266,18 +371,18 @@
266 371 setIsSaving(false);
267 372 }
268 373 };
269 374
270 - if (isLoading || isLoadingVibes) {
375 + if (isLoading || isLoadingVibes || isLoadingPalettes) {
271 376 return (
272 - <div className="flex justify-center flex-col gap-1">
273 - <Spinner />
377 + <div className="min-h-24 p-2 text-center text-sm">
378 + {__('Loading design options...', 'extendify-local')}
274 379 </div>
275 380 );
276 381 }
277 382
278 383 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">
384 + <div className="mb-4 ms-2 me-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50">
280 385 <div className="rounded-lg border-b border-gray-300 bg-white">
281 386 <div className="flex flex-col gap-4 p-3">
282 387 {currentHeroHtml && (
283 388 <DesignOption
@@ -287,8 +392,9 @@
287 392 onClick={() => {
288 393 setSelectedHeroPattern(currentDesignOption);
289 394 setSelectedColorAndFonts(null);
290 395 setSelectedVibe(null);
396 + setSelectedPalette(null);
291 397
292 398 undoChanges();
293 399 }}
294 400 />
@@ -300,11 +406,13 @@
300 406 isSelected={selectedHeroPattern?.id === heroPattern.id}
301 407 styles={{
302 408 linkStyles: heroPattern.linkStyles,
303 409 colorAndFontsVariations: colorAndFontsVariations[i].css,
304 - duotoneTheme:
305 - colorAndFontsVariations[i]?.settings?.color?.duotone?.theme,
306 - vibes: vibes[i]?.css,
410 + paletteCss: paletteCss?.[paletteFor(i)?.slug],
411 + duotoneTheme: onPalettes
412 + ? paletteDuotone(paletteFor(i))
413 + : colorAndFontsVariations[i]?.settings?.color?.duotone?.theme,
414 + vibes: vibes[i]?.previewCss,
307 415 blockSupportsCss: heroPattern.blockSupportsCss,
308 416 }}
309 417 onClick={() => {
310 418 setSelectedHeroPattern(heroPattern);
@@ -309,8 +417,9 @@
309 417 onClick={() => {
310 418 setSelectedHeroPattern(heroPattern);
311 419 setSelectedColorAndFonts(colorAndFontsVariations[i]);
312 420 setSelectedVibe(vibes[i]);
421 + setSelectedPalette(paletteFor(i));
313 422
314 423 if (!isAdmin) {
315 424 const blockSupportsCss = heroPattern.blockSupportsCss;
316 425 if (blockSupportsCss) {
@@ -325,8 +434,9 @@
325 434
326 435 injectLinkStyles(heroPattern.linkStyles);
327 436
328 437 updateHeroSection(heroPattern.renderedHtml);
438 + refreshBlockHighlight();
329 439 }
330 440 }}
331 441 />
332 442 ))}