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/AutoLaunch/LaunchPage.jsx +219 -166 3.1.5 → trunk 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} />;