PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 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 All 128 releases
← All changes | src/AutoLaunch/LaunchPage.jsx +219 -166 3.1.4 → 3.2.2 View file →
@@ -1,24 +1,37 @@
1 -import { ExtendifyCodeConnector } from '@auto-launch/components/ExtendifyCodeConnector';
2 -import { Launch } from '@auto-launch/components/Launch';
1 +import { CreatingSite } from '@auto-launch/components/CreatingSite';
2 +import { DescriptionGathering } from '@auto-launch/components/DescriptionGathering';
3 3 import { LaunchUpdate } from '@auto-launch/components/LaunchUpdate';
4 4 import { Logo } from '@auto-launch/components/Logo';
5 -import { MigrateChoice } from '@auto-launch/components/MigrateChoice';
6 -import { MovingGradient } from '@auto-launch/components/MovingGradients';
7 -import { NeedsTheme } from '@auto-launch/components/NeedsTheme';
8 -import { RestartLaunchModal } from '@auto-launch/components/RestartLaunchModal';
9 -import { ViewportPulse } from '@auto-launch/components/ViewportPulse';
5 +import {
6 + ModalHeading,
7 + ModalLink,
8 + ModalText,
9 +} from '@auto-launch/components/Modal';
10 +import { ShaderBackground } from '@auto-launch/components/ShaderBackground';
11 +import { activeLogo } from '@auto-launch/functions/design';
10 12 import { getAbTest } from '@auto-launch/functions/getAbTest';
11 13 import { preLaunchFunctions } from '@auto-launch/functions/setup';
12 14 import { updateOption } from '@auto-launch/functions/wp';
15 +import { useExtendifyCodeConnector } from '@auto-launch/hooks/useExtendifyCodeConnector';
16 +import { useInstallRequiredPlugins } from '@auto-launch/hooks/useInstallRequiredPlugins';
17 +import { useMigrateChoice } from '@auto-launch/hooks/useMigrateChoice';
18 +import { useNeedsTheme } from '@auto-launch/hooks/useNeedsTheme';
19 +import { useRestartLaunch } from '@auto-launch/hooks/useRestartLaunch';
13 20 import { useLaunchDataStore } from '@auto-launch/state/launch-data';
21 +import { launchStrings } from '@auto-launch/strings';
22 +import {
23 + activeChoiceTemplate,
24 + activeDescriptionTemplate,
25 + activeModalTemplate,
26 +} from '@auto-launch/templates';
27 +import { ActionButton, SecondaryButton } from '@auto-launch/templates/button';
28 +import { Heading, PageLink } from '@auto-launch/templates/heading';
14 29 import { digest } from '@shared/api/digest';
15 30 import { registerCoreBlocks } from '@wordpress/block-library';
16 31 import { getBlockTypes } from '@wordpress/blocks';
17 32 import { useSelect } from '@wordpress/data';
18 -import { useEffect, useRef, useState } from '@wordpress/element';
19 -import { __ } from '@wordpress/i18n';
20 -import { chevronLeft, Icon } from '@wordpress/icons';
33 +import { useEffect, useState } from '@wordpress/element';
21 34 import classNames from 'classnames';
22 35 import { AnimatePresence, motion } from 'framer-motion';
23 36 import { checkIn, reportRestApiStatus } from './functions/insights';
24 37
@@ -60,10 +73,23 @@
60 73 getAbTest('AutoLaunch.MigrateScreen').variant === 'B';
61 74 const [choosingMigration, setChoosingMigration] = useState(inMigrateVariant);
62 75 const showMigrateChoice = !skipDescription && choosingMigration;
63 76
64 - const containerRef = useRef(null);
77 + // Deleting the old pages first leaves the user on a theme Launch cannot build on.
78 + const blocker = launchUpdateNeeded
79 + ? 'update'
80 + : needsTheme
81 + ? 'theme'
82 + : needsToReset
83 + ? 'reset'
84 + : null;
65 85
86 + // Only the describe screen commits to a build; every other screen can still leave.
87 + useInstallRequiredPlugins({
88 + enabled:
89 + !blocker && !skipDescription && !showConnector && !showMigrateChoice,
90 + });
91 +
66 92 useEffect(() => {
67 93 if (launchUpdateNeeded) return;
68 94 if (launchUpdateGaveUp) {
69 95 digest({
@@ -75,9 +101,9 @@
75 101 },
76 102 });
77 103 }
78 104 // translators: Launch is a noun.
79 - document.title = __('Launch - AI-Powered Web Creation', 'extendify-local');
105 + document.title = launchStrings().documentTitle;
80 106 updateOption('extendify_launch_loaded', new Date().toISOString());
81 107 // We load core blocks so we can parse them
82 108 if (getBlockTypes().length === 0) registerCoreBlocks();
83 109
@@ -85,183 +111,210 @@
85 111 checkIn({ stage: 'launch_page' });
86 112 reportRestApiStatus();
87 113 }, [launchUpdateNeeded, launchUpdateGaveUp]);
88 114
89 - if (launchUpdateNeeded) {
90 - return (
91 - <Wrapper>
92 - <div className="bg-white w-full max-w-xl rounded-lg border border-design-main/60 relative z-10">
93 - <LaunchUpdate
94 - pluginUpdateNeeded={pluginUpdateNeeded}
95 - themeUpdateNeeded={themeUpdateNeeded}
96 - pluginUpdateViaRest={pluginUpdateViaRest}
97 - pluginSlug={window.extLaunchData?.pluginSlug}
98 - attempt={launchUpdateAttempt}
99 - />
100 - </div>
101 - </Wrapper>
102 - );
103 - }
115 + const modal =
116 + blocker === 'update' ? (
117 + <UpdateScreen
118 + pluginUpdateNeeded={pluginUpdateNeeded}
119 + themeUpdateNeeded={themeUpdateNeeded}
120 + pluginUpdateViaRest={pluginUpdateViaRest}
121 + pluginSlug={window.extLaunchData?.pluginSlug}
122 + attempt={launchUpdateAttempt}
123 + />
124 + ) : blocker === 'theme' ? (
125 + <NeedsThemeScreen />
126 + ) : blocker === 'reset' ? (
127 + <RestartScreen pages={oldPages} />
128 + ) : null;
129 + // Site creation must not start behind a modal the user has not cleared.
130 + const screen = modal
131 + ? 'description'
132 + : showConnector
133 + ? 'connector'
134 + : showMigrateChoice
135 + ? 'migrate'
136 + : skipDescription
137 + ? 'creating'
138 + : 'description';
104 139
105 - if (needsTheme) {
106 - return (
107 - <Wrapper>
108 - <div className="bg-white w-full max-w-3xl rounded-lg border border-design-main/60 relative z-10">
109 - <NeedsTheme />
110 - </div>
111 - </Wrapper>
112 - );
113 - }
140 + return (
141 + <Viewport modal={modal} screen={screen}>
142 + {screen === 'creating' ? (
143 + <CreatingSite />
144 + ) : screen === 'connector' ? (
145 + <ConnectorScreen
146 + onProceed={() => setData('go', true)}
147 + footer={
148 + <BackLink
149 + onClick={() => setData('showExtendifyCodeScreen', false)}
150 + />
151 + }
152 + />
153 + ) : screen === 'migrate' ? (
154 + <MigrateScreen
155 + onBuildNew={() => setChoosingMigration(false)}
156 + footer={showExitLink ? <ExitLink /> : null}
157 + />
158 + ) : (
159 + <DescriptionPage
160 + heading={<TheTitle />}
161 + footer={showExitLink ? <ExitLink /> : null}
162 + >
163 + {/* Matches the migrate screen's height, so choosing Build does not jump. */}
164 + <div className={classNames({ 'md:h-72.75': inMigrateVariant })}>
165 + <div className="mx-auto w-full">
166 + <DescriptionGathering autoFocus={!modal} />
167 + </div>
168 + </div>
169 + </DescriptionPage>
170 + )}
171 + </Viewport>
172 + );
173 +};
114 174
115 - if (needsToReset) {
116 - return (
117 - <Wrapper>
118 - <div className="w-full max-w-2xl rounded-3xl border bg-gray-100/80 backdrop-blur-2xl shadow-md relative z-10 border-gray-300">
119 - <RestartLaunchModal pages={oldPages} />
120 - </div>
121 - </Wrapper>
122 - );
123 - }
124 -
175 +const NeedsThemeScreen = () => {
176 + const { title, body, action } = useNeedsTheme();
125 177 return (
126 - <Wrapper
127 - footer={
128 - showConnector ? (
129 - <BackLink onClick={() => setData('showExtendifyCodeScreen', false)} />
130 - ) : showExitLink ? (
131 - <ExitLink />
132 - ) : null
133 - }
134 - >
135 - <AnimatePresence mode="wait" initial={false}>
136 - <TheTitle
137 - key={showMigrateChoice ? 'migrate' : 'description'}
138 - // The connector renders its own heading, so hide the shared one.
139 - hide={skipDescription || showConnector}
140 - migrate={showMigrateChoice}
141 - />
142 - </AnimatePresence>
143 - <div
144 - ref={containerRef}
145 - className={classNames('w-full relative z-10', {
146 - 'max-w-3xl': inMigrateVariant || showConnector,
147 - 'max-w-2xl': !inMigrateVariant && !showConnector,
148 - 'md:h-72.75': inMigrateVariant && !skipDescription,
149 - })}
150 - >
151 - <AnimatePresence mode="wait">
152 - {showConnector && (
153 - <ExtendifyCodeConnector
154 - key="extendify-code"
155 - onProceed={() => setData('go', true)}
156 - />
157 - )}
158 - {!showConnector && showMigrateChoice && (
159 - <MigrateChoice
160 - key="migrate-choice"
161 - onBuildNew={() => setChoosingMigration(false)}
162 - />
163 - )}
164 - {!showConnector && !showMigrateChoice && (
165 - <Launch
166 - key={skipDescription ? 'description-launch' : 'creating-launch'}
167 - skipDescription={skipDescription}
168 - lastHeight={containerRef.current?.offsetHeight}
169 - />
170 - )}
171 - </AnimatePresence>
172 - </div>
173 - </Wrapper>
178 + <ModalPage
179 + heading={<ModalHeading title={title} />}
180 + body={<ModalText>{body}</ModalText>}
181 + actions={<ModalLink href={action.href} label={action.label} />}
182 + />
174 183 );
175 184 };
176 185
177 -const Wrapper = ({ children, footer }) => {
178 - const { pulse } = useLaunchDataStore();
186 +const UpdateScreen = (props) => <LaunchUpdate {...props} />;
179 187
188 +const RestartScreen = ({ pages }) => {
189 + const { title, body, note, exit, confirm } = useRestartLaunch({ pages });
180 190 return (
181 - <div style={{ zIndex: 99999 + 1 }} className="fixed inset-0 bg-white">
182 - <div className="relative h-dvh bg-banner-main text-banner-text text-base overflow-y-auto">
183 - <div className="relative z-10 min-h-dvh w-full flex flex-col items-center justify-between p-6">
184 - <div className="w-full flex flex-col items-center gap-5 md:gap-8 m-auto">
185 - <div className="mb-4">
186 - <Logo />
187 - </div>
188 - {children}
189 - </div>
190 - {footer && <div className="w-full pt-8 shrink-0">{footer}</div>}
191 - </div>
192 - </div>
193 - <MovingGradient />
194 - {pulse ? <ViewportPulse /> : null}
195 - </div>
191 + <ModalPage
192 + left
193 + heading={<ModalHeading title={title} />}
194 + body={
195 + <>
196 + <ModalText>{body}</ModalText>
197 + <ModalText strong>{note}</ModalText>
198 + </>
199 + }
200 + actions={
201 + <>
202 + <SecondaryButton
203 + disabled={confirm.processing}
204 + label={exit.label}
205 + onClick={exit.onClick}
206 + />
207 + <ActionButton {...confirm} />
208 + </>
209 + }
210 + />
196 211 );
197 212 };
198 213
199 -const ExitLink = () => {
214 +const ConnectorScreen = ({ onProceed, footer }) => {
215 + const { title, subtitle, options } = useExtendifyCodeConnector({ onProceed });
200 216 return (
201 - <a
202 - className="inline-flex items-center gap-0.5 text-sm text-banner-text opacity-70 hover:opacity-100 transition-opacity"
203 - href={window.extSharedData.adminUrl}
204 - onClick={() => checkIn({ stage: 'exit_to_wp_admin' })}
205 - >
206 - <Icon fill="currentColor" icon={chevronLeft} size={20} />
207 - {__('WP Admin Dashboard', 'extendify-local')}
208 - </a>
217 + <ChoicePage
218 + heading={title && <Heading subtitle={subtitle} title={title} />}
219 + options={options}
220 + footer={footer}
221 + />
209 222 );
210 223 };
211 224
212 -const BackLink = ({ onClick }) => {
225 +const MigrateScreen = ({ onBuildNew, footer }) => {
226 + const { title, options } = useMigrateChoice({ onBuildNew });
213 227 return (
214 - <button
215 - type="button"
216 - onClick={onClick}
217 - className="inline-flex items-center gap-0.5 border-0 bg-transparent cursor-pointer text-sm text-banner-text opacity-70 hover:opacity-100 transition-opacity"
218 - >
219 - <Icon fill="currentColor" icon={chevronLeft} size={20} />
220 - {__('Back', 'extendify-local')}
221 - </button>
228 + <ChoicePage
229 + heading={<Heading title={title} />}
230 + options={options}
231 + footer={footer}
232 + />
222 233 );
223 234 };
224 235
225 -const TheTitle = ({ hide, migrate }) => {
226 - const useOldHeader =
227 - getAbTest('AutoLaunch.HeaderParagraphOld').variant === 'B';
228 - if (hide) return null;
236 +const Viewport = ({ children, screen, modal }) => (
237 + // Inline inset: utilities are !important here and would win over it.
238 + <div
239 + style={{ zIndex: 99999 + 1, top: 0, right: 0, bottom: 0, left: 0 }}
240 + className="group fixed bg-ui-page"
241 + >
242 + <AnimatePresence mode="wait" initial={false}>
243 + <motion.div
244 + key={screen}
245 + className="absolute inset-0"
246 + // React 18 drops a boolean inert; only a string reaches the DOM.
247 + inert={modal ? '' : undefined}
248 + initial={{ opacity: 0 }}
249 + animate={{ opacity: 1 }}
250 + exit={{ opacity: 0 }}
251 + transition={{ duration: 0.25 }}
252 + >
253 + {children}
254 + </motion.div>
255 + </AnimatePresence>
256 + {modal}
257 + <ShaderBackground />
258 + </div>
259 +);
229 260
230 - const headingClass =
231 - 'text-xl md:text-2xl text-pretty text-banner-text font-semibold p-0 m-0 text-center';
232 - const transition = {
233 - animate: { opacity: 1 },
234 - exit: { opacity: 0 },
235 - transition: { duration: 0.4 },
261 +const DescriptionPage = ({ children, footer, heading }) => {
262 + const Page = activeDescriptionTemplate();
263 + const { partnerLogo, partnerName } = window.extSharedData ?? {};
264 + // A template cannot tell a heading that renders null from one that draws.
265 + const has = {
266 + heading: Boolean(heading),
267 + footer: Boolean(footer),
236 268 };
269 + const parts = {
270 + logo: <Logo name={partnerName} src={activeLogo() ?? partnerLogo} />,
271 + heading,
272 + content: children,
273 + footer,
274 + };
237 275
238 - if (migrate) {
239 - return (
240 - <motion.h2 className={headingClass} {...transition}>
241 - {__('How would you like to start your website?', 'extendify-local')}
242 - </motion.h2>
243 - );
244 - }
276 + return <Page parts={parts} has={has} />;
277 +};
245 278
246 - if (useOldHeader) {
247 - return (
248 - <motion.div className="flex flex-col items-center gap-2" {...transition}>
249 - <h2 className={headingClass}>
250 - {__('Tell Us About Your Website', 'extendify-local')}
251 - </h2>
252 - <p className="text-sm md:text-base text-pretty text-banner-text opacity-70 p-0 m-0 text-center max-w-xl">
253 - {__(
254 - "Share your vision, and we'll craft a website that's perfectly tailored to your needs, ready to launch in no time.",
255 - 'extendify-local',
256 - )}
257 - </p>
258 - </motion.div>
259 - );
260 - }
279 +const ModalPage = ({ heading, body, actions, left }) => {
280 + const Page = activeModalTemplate();
281 + const has = {
282 + heading: Boolean(heading),
283 + actions: Boolean(actions),
284 + card: true,
285 + left: Boolean(left),
286 + };
261 287
262 - return (
263 - <motion.h2 className={headingClass} {...transition}>
264 - {__('Describe the website you want to build', 'extendify-local')}
265 - </motion.h2>
266 - );
288 + return <Page parts={{ heading, body, actions }} has={has} />;
267 289 };
290 +
291 +const ChoicePage = ({ heading, options, footer }) => {
292 + const Page = activeChoiceTemplate();
293 + const { partnerLogo, partnerName } = window.extSharedData ?? {};
294 + const has = {
295 + heading: Boolean(heading),
296 + footer: Boolean(footer),
297 + };
298 + const parts = {
299 + logo: <Logo name={partnerName} src={activeLogo() ?? partnerLogo} />,
300 + heading,
301 + options,
302 + footer,
303 + };
304 +
305 + return <Page parts={parts} has={has} />;
306 +};
307 +
308 +const ExitLink = () => (
309 + <PageLink
310 + href={window.extSharedData.adminUrl}
311 + label={launchStrings().exitLink}
312 + onClick={() => checkIn({ stage: 'exit_to_wp_admin' })}
313 + />
314 +);
315 +
316 +const BackLink = ({ onClick }) => (
317 + <PageLink label={launchStrings().back} onClick={onClick} />
318 +);
319 +
320 +const TheTitle = () => <Heading title={launchStrings().heading} />;