PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
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 3.1.5, at src/Launch/components/PageControl.jsx

405 lines 11.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { TONES } from '@launch/components/BusinessInformation/Tones';
2 import { NavigationButton } from '@launch/components/NavigationButton';
3 import {
4 PagesSelect,
5 state as pagesSelectState,
6 } from '@launch/pages/PagesSelect';
7 import {
8 SiteStructure,
9 state as siteStructureState,
10 } from '@launch/pages/SiteStructure';
11 import { useGlobalStore } from '@launch/state/Global';
12 import { usePagesStore } from '@launch/state/Pages';
13 import { useUserSelectionStore } from '@launch/state/user-selections';
14 import { LeftCaret, RightCaret } from '@launch/svg';
15 import {
16 convertToValidParamsArray,
17 mapToneValuesToObjects,
18 } from '@shared/utils/convert-to-valid-params';
19 import { getUrlParameter } from '@shared/utils/get-url-parameter';
20 import {
21 useEffect,
22 useLayoutEffect,
23 useMemo,
24 useRef,
25 useState,
26 } from '@wordpress/element';
27 import { __, isRTL } from '@wordpress/i18n';
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 text-base">
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 >
286 {pagesList.map(([page], index) => {
287 const bgColor =
288 index < currentPageIndex ? 'bg-design-main' : 'bg-gray-200';
289 if (!getPageState(page)?.useNav) return null;
290 return (
291 <div key={page} className="flex items-center">
292 {(index !== currentPageIndex && (
293 <div className={`${bgColor} h-2.5 w-2.5 rounded-full`} />
294 )) || (
295 <div className="flex h-4 w-4 items-center justify-center rounded-full bg-design-main">
296 <div className="h-1.5 w-1.5 rounded-full bg-white/80" />
297 </div>
298 )}
299 {index < totalPages - 1 && (
300 <div className={`${bgColor} h-0.5 w-16`} />
301 )}
302 </div>
303 );
304 })}
305 </div>
306 );
307 };
308
309 const PrevButton = ({ forceFirstpage = false }) => {
310 const { previousPage, currentPageIndex } = usePagesStore();
311 const onFirstPage = currentPageIndex === 0 || forceFirstpage;
312
313 if (onFirstPage) {
314 return (
315 <NavigationButton
316 onClick={() => {
317 window.location.href = `${window.extSharedData.adminUrl}admin.php?page=extendify-assist`;
318 }}
319 id="extendify-exit-launch-button"
320 className="border-gray-200 bg-white text-design-main hover:bg-gray-50 focus:bg-gray-50"
321 >
322 {isRTL() ? (
323 <RightCaret className="mt-px h-5 w-5" />
324 ) : (
325 <LeftCaret className="mt-px h-5 w-5" />
326 )}
327
328 <span>{__('WP Admin Dashboard', 'extendify-local')}</span>
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 </NavigationButton>
346 );
347 };
348
349 const NextButton = () => {
350 const { nextPage, currentPageIndex, pages } = usePagesStore();
351 const totalPages = usePagesStore((state) => state.count());
352 const onLastPage = currentPageIndex === totalPages - 1;
353 const currentPageKey = Array.from(pages.keys())[currentPageIndex];
354 const pageState = pages.get(currentPageKey).state;
355 const [canProgress, setCanProgress] = useState(false);
356 const [canSkip, setCanSkip] = useState(false);
357
358 const nextPageOrComplete = () => {
359 if (onLastPage) {
360 useGlobalStore.setState({ generating: true });
361 } else {
362 nextPage();
363 }
364 };
365
366 useEffect(() => {
367 const { ready, canSkip } = pageState?.getState() || {};
368 setCanSkip(canSkip ?? false);
369 setCanProgress(ready ?? false);
370 return pageState.subscribe((s) => {
371 setCanSkip(s.canSkip);
372 setCanProgress(s.ready);
373 });
374 }, [pageState, currentPageIndex]);
375
376 return canSkip ? (
377 <NavigationButton
378 onClick={() => nextPageOrComplete()}
379 data-test="back-button"
380 className="mr-2 border-gray-200 bg-white text-design-main hover:bg-gray-50 focus:bg-gray-50"
381 >
382 {__('Skip', 'extendify-local')}
383 {isRTL() ? (
384 <LeftCaret className="mt-px h-5 w-5" />
385 ) : (
386 <RightCaret className="mt-px h-5 w-5" />
387 )}
388 </NavigationButton>
389 ) : (
390 <NavigationButton
391 onClick={nextPageOrComplete}
392 disabled={!canProgress}
393 className="border-design-main bg-design-main text-design-text"
394 data-test="next-button"
395 >
396 {__('Next', 'extendify-local')}
397 {isRTL() ? (
398 <LeftCaret className="mt-px h-5 w-5" />
399 ) : (
400 <RightCaret className="mt-px h-5 w-5" />
401 )}
402 </NavigationButton>
403 );
404 };
405