PluginProbe
Extendify / 2.2.0
Extendify v2.2.0
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 0.7.0 All 126 releases
extendify / src / Launch / components / PageControl.jsx

PageControl.jsx in Extendify 2.2.0, at src/Launch/components/PageControl.jsx

408 lines 11.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import {
2 useEffect,
3 useLayoutEffect,
4 useRef,
5 useState,
6 useMemo,
7 } from '@wordpress/element';
8 import { __, isRTL } from '@wordpress/i18n';
9 import {
10 convertToValidParamsArray,
11 mapToneValuesToObjects,
12 } from '@shared/utils/convert-to-valid-params';
13 import { getUrlParameter } from '@shared/utils/get-url-parameter';
14 import { TONES } from '@launch/components/BusinessInformation/Tones';
15 import { NavigationButton } from '@launch/components/NavigationButton';
16 import {
17 PagesSelect,
18 state as pagesSelectState,
19 } from '@launch/pages/PagesSelect';
20 import {
21 SiteStructure,
22 state as siteStructureState,
23 } from '@launch/pages/SiteStructure';
24 import { useGlobalStore } from '@launch/state/Global';
25 import { usePagesStore } from '@launch/state/Pages';
26 import { useUserSelectionStore } from '@launch/state/user-selections';
27 import { RightCaret, LeftCaret } from '@launch/svg';
28
29 // This is a bit hacky for faster development.
30 // We should refactor to include custom flows for each objective
31 // And allow the router to switch paths along the way
32 const PagesPageData = {
33 component: PagesSelect,
34 state: pagesSelectState,
35 };
36 const StructurePageData = {
37 component: SiteStructure,
38 state: siteStructureState,
39 };
40
41 const objectives = ['business', 'ecommerce', 'blog', 'landing-page', 'other'];
42 const structures = ['single-page', 'multi-page'];
43 const ALLOWED_SKIP = ['questions', 'info', 'layout', 'pages'];
44 const ALLOWED_TONES = TONES.map((tone) => tone?.value);
45
46 export const PageControl = () => {
47 const {
48 currentPageIndex,
49 addPage,
50 removePage,
51 pages,
52 getPageState,
53 previousPage,
54 replaceHistory,
55 addPreselectedPage,
56 getCurrentPageSlug,
57 } = usePagesStore();
58 const {
59 siteStructure,
60 siteObjective,
61 setSiteObjective,
62 setSiteStructure,
63 setUrlParameters,
64 setSiteInformation,
65 setBusinessInformation,
66 businessInformation,
67 siteQA,
68 } = useUserSelectionStore();
69
70 const titleUrlParameter = getUrlParameter('title', false);
71 const descriptionUrlParameter = getUrlParameter('description', false);
72 const siteObjectiveParam = getUrlParameter('objective', false);
73 const siteStructureParam = getUrlParameter('structure', false);
74 const siteToneRaw = getUrlParameter('tone', false);
75 const siteSkipRaw = getUrlParameter('skip', false);
76 const siteToneParam = useMemo(
77 () =>
78 mapToneValuesToObjects(
79 convertToValidParamsArray(siteToneRaw, ALLOWED_TONES),
80 TONES,
81 ),
82 [siteToneRaw],
83 );
84 const siteSkipParam = useMemo(
85 () => convertToValidParamsArray(siteSkipRaw, ALLOWED_SKIP),
86 [siteSkipRaw],
87 );
88 const removeStructurePage = useRef(false);
89 const toneAppliedOnce = useRef(false);
90 const showSiteQuestions = window.extSharedData?.showSiteQuestions ?? false;
91 const isValidSiteToneParam =
92 Array.isArray(siteToneParam) && siteToneParam?.length > 0;
93
94 useLayoutEffect(() => {
95 setUrlParameters({
96 title: titleUrlParameter,
97 description: descriptionUrlParameter,
98 objective: siteObjectiveParam,
99 structure: siteStructureParam,
100 tone: isValidSiteToneParam ? siteToneParam : null,
101 skip:
102 Array.isArray(siteSkipParam) && siteSkipParam?.length
103 ? siteSkipParam
104 : null,
105 });
106
107 // If we later add more structures, consider having predefined paths
108 if (siteStructure === 'multi-page') {
109 addPage('page-select', PagesPageData, 'layout');
110 }
111 // If the site objective is not 'landing-page', add the site structure page
112 if (siteObjective !== 'landing-page') {
113 addPage('site-structure', StructurePageData, 'site-prep');
114 }
115
116 // Landing pages are single-page structure, so we need to remove both page-select and site-structure pages
117 if (siteObjective === 'landing-page' && siteStructure === 'single-page') {
118 removePage('page-select');
119 removePage('site-structure');
120 }
121 // For any single-page structure (regardless of objective), remove the page-select page
122 if (siteStructure === 'single-page') {
123 removePage('page-select');
124 }
125 // If a valid objective parameter is in the URL, set it as the site objective and skip the objective selection page
126 if (siteObjectiveParam && objectives.includes(siteObjectiveParam)) {
127 setSiteObjective(siteObjectiveParam);
128 addPreselectedPage('website-objective');
129 removePage('website-objective');
130 }
131
132 // If a structure parameter is in the URL, and it's valid, set the structure and skip the structure page
133 if (siteStructureParam && structures.includes(siteStructureParam)) {
134 setSiteStructure(siteStructureParam);
135 addPreselectedPage('site-structure');
136 removeStructurePage.current = true;
137 }
138
139 if (showSiteQuestions) {
140 removePage('site-structure');
141 }
142
143 if (isValidSiteToneParam && !toneAppliedOnce.current) {
144 const mergedTones = [
145 ...(businessInformation?.tones || []),
146 ...siteToneParam,
147 ];
148
149 const uniqueTones = mergedTones.reduce((acc, tone) => {
150 if (!acc.some((t) => t.value === tone.value)) {
151 acc.push(tone);
152 }
153 return acc;
154 }, []);
155
156 setBusinessInformation('tones', uniqueTones);
157 toneAppliedOnce.current = true;
158 }
159
160 if (siteSkipParam.includes('questions')) {
161 if (!siteStructureParam) {
162 const siteStructureFromQuestions = siteQA?.questions.find(
163 (item) => item?.id === 'pages',
164 )?.answerAI;
165
166 const mappedStructure = {
167 'multiple-pages': 'multi-page',
168 'one-page': 'single-page',
169 };
170
171 if (
172 siteStructureFromQuestions &&
173 mappedStructure?.[siteStructureFromQuestions]
174 ) {
175 setSiteStructure(mappedStructure[siteStructureFromQuestions]);
176 }
177 }
178
179 addPreselectedPage('site-questions');
180 removePage('site-questions');
181 }
182
183 if (siteSkipParam.includes('info') && titleUrlParameter) {
184 setSiteInformation('title', titleUrlParameter);
185
186 if (descriptionUrlParameter) {
187 setBusinessInformation('description', descriptionUrlParameter);
188 }
189
190 addPreselectedPage('site-information');
191 removePage('site-information');
192 }
193
194 if (siteSkipParam.includes('pages') && siteStructure === 'multi-page') {
195 addPreselectedPage('page-select');
196 removePage('page-select');
197 }
198 }, [
199 setSiteObjective,
200 setSiteStructure,
201 siteStructure,
202 siteObjective,
203 addPage,
204 removePage,
205 descriptionUrlParameter,
206 titleUrlParameter,
207 siteObjectiveParam,
208 siteStructureParam,
209 siteToneParam,
210 siteSkipParam,
211 addPreselectedPage,
212 showSiteQuestions,
213 setUrlParameters,
214 setSiteInformation,
215 setBusinessInformation,
216 businessInformation,
217 isValidSiteToneParam,
218 siteQA,
219 ]);
220
221 useEffect(() => {
222 if (removeStructurePage.current) removePage('site-structure');
223 }, [removePage]);
224
225 useEffect(() => {
226 const replaceStateHistory = () => {
227 history.state === null && replaceHistory(currentPageIndex);
228 };
229 window.addEventListener('load', replaceStateHistory);
230
231 const popstate = () => {
232 const page = currentPageIndex - 1;
233 if (page === -1) history.go(-1);
234 previousPage();
235 };
236 window.addEventListener('popstate', popstate);
237 return () => {
238 window.removeEventListener('popstate', popstate);
239 };
240 }, [previousPage, currentPageIndex, replaceHistory]);
241
242 const pagesList = Array.from(pages.entries());
243 // Some pages act as a notice or loading message and move on their own
244 if (!getPageState(pagesList[currentPageIndex][0])?.useNav) return null;
245
246 const skipInfoStepEnabled =
247 siteSkipParam.includes('info') && titleUrlParameter;
248 const skipInfoAndQuestionsEnabled =
249 skipInfoStepEnabled && siteSkipParam.includes('questions');
250 const currentPageSlug = getCurrentPageSlug();
251 const forceFirstpage =
252 (skipInfoStepEnabled && currentPageSlug === 'site-questions') ||
253 (skipInfoAndQuestionsEnabled && currentPageSlug === 'layout');
254
255 return (
256 <div className="z-10 w-full flex-none border-t border-gray-100 bg-white px-6 py-5 shadow-surface md:px-12 md:py-6">
257 <div className="flex justify-between">
258 <span className="flex-1 self-start">
259 <PrevButton forceFirstpage={forceFirstpage} />
260 </span>
261 <span className="hidden grow items-center justify-center md:flex">
262 <Steps />
263 </span>
264 <span className="flex flex-1 justify-end">
265 <NextButton />
266 </span>
267 </div>
268 </div>
269 );
270 };
271
272 const Steps = () => {
273 const { currentPageIndex, pages, getPageState } = usePagesStore();
274 const totalPages = usePagesStore((state) => state.count());
275 const pagesList = Array.from(pages.entries());
276
277 return (
278 <div
279 className="flex"
280 role="progressbar"
281 aria-valuenow={currentPageIndex}
282 aria-valuemin="0"
283 aria-valuetext={pagesList[currentPageIndex][1].state.getState().title}
284 aria-valuemax={totalPages - 1}>
285 {pagesList.map(([page], index) => {
286 const bgColor =
287 index < currentPageIndex ? 'bg-design-main' : 'bg-gray-200';
288 if (!getPageState(page)?.useNav) return null;
289 return (
290 <div key={page} className="flex items-center">
291 {(index !== currentPageIndex && (
292 <div className={`${bgColor} h-2.5 w-2.5 rounded-full`} />
293 )) || (
294 <div className="flex h-4 w-4 items-center justify-center rounded-full bg-design-main">
295 <div className="h-1.5 w-1.5 rounded-full bg-white/80" />
296 </div>
297 )}
298 {index < totalPages - 1 && (
299 <div className={`${bgColor} h-0.5 w-16`} />
300 )}
301 </div>
302 );
303 })}
304 </div>
305 );
306 };
307
308 const PrevButton = ({ forceFirstpage = false }) => {
309 const { previousPage, currentPageIndex } = usePagesStore();
310 const onFirstPage = currentPageIndex === 0 || forceFirstpage;
311
312 if (onFirstPage) {
313 return (
314 <NavigationButton
315 onClick={() =>
316 (window.location.href = `${window.extSharedData.adminUrl}admin.php?page=extendify-assist`)
317 }
318 id="extendify-exit-launch-button"
319 className="border-gray-200 bg-white text-design-main hover:bg-gray-50 focus:bg-gray-50">
320 <>
321 {isRTL() ? (
322 <RightCaret className="mt-px h-5 w-5" />
323 ) : (
324 <LeftCaret className="mt-px h-5 w-5" />
325 )}
326
327 <span>{__('WP Admin Dashboard', 'extendify-local')}</span>
328 </>
329 </NavigationButton>
330 );
331 }
332
333 return (
334 <NavigationButton
335 onClick={previousPage}
336 data-test="back-button"
337 className="border-gray-200 bg-white text-design-main hover:bg-gray-50 focus:bg-gray-50">
338 <>
339 {isRTL() ? (
340 <RightCaret className="mt-px h-5 w-5" />
341 ) : (
342 <LeftCaret className="mt-px h-5 w-5" />
343 )}
344 <span>{__('Back', 'extendify-local')}</span>
345 </>
346 </NavigationButton>
347 );
348 };
349
350 const NextButton = () => {
351 const { nextPage, currentPageIndex, pages } = usePagesStore();
352 const totalPages = usePagesStore((state) => state.count());
353 const onLastPage = currentPageIndex === totalPages - 1;
354 const currentPageKey = Array.from(pages.keys())[currentPageIndex];
355 const pageState = pages.get(currentPageKey).state;
356 const [canProgress, setCanProgress] = useState(false);
357 const [canSkip, setCanSkip] = useState(false);
358
359 const nextPageOrComplete = () => {
360 if (onLastPage) {
361 useGlobalStore.setState({ generating: true });
362 } else {
363 nextPage();
364 }
365 };
366
367 useEffect(() => {
368 const { ready, canSkip } = pageState?.getState() || {};
369 setCanSkip(canSkip ?? false);
370 setCanProgress(ready ?? false);
371 return pageState.subscribe((s) => {
372 setCanSkip(s.canSkip);
373 setCanProgress(s.ready);
374 });
375 }, [pageState, currentPageIndex]);
376
377 return canSkip ? (
378 <NavigationButton
379 onClick={() => nextPageOrComplete()}
380 data-test="back-button"
381 className="mr-2 border-gray-200 bg-white text-design-main hover:bg-gray-50 focus:bg-gray-50">
382 <>
383 {__('Skip', 'extendify-local')}
384 {isRTL() ? (
385 <LeftCaret className="mt-px h-5 w-5" />
386 ) : (
387 <RightCaret className="mt-px h-5 w-5" />
388 )}
389 </>
390 </NavigationButton>
391 ) : (
392 <NavigationButton
393 onClick={nextPageOrComplete}
394 disabled={!canProgress}
395 className="border-design-main bg-design-main text-design-text"
396 data-test="next-button">
397 <>
398 {__('Next', 'extendify-local')}
399 {isRTL() ? (
400 <LeftCaret className="mt-px h-5 w-5" />
401 ) : (
402 <RightCaret className="mt-px h-5 w-5" />
403 )}
404 </>
405 </NavigationButton>
406 );
407 };
408