PluginProbe
Extendify / 3.1.6
Extendify v3.1.6
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
extendify / src / Launch / pages / HomeSelect.jsx

HomeSelect.jsx in Extendify 3.1.6, at src/Launch/pages/HomeSelect.jsx

211 lines 6.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { getThemeVariations } from '@launch/api/WPApi';
2 import { LoadingIndicator } from '@launch/components/LoadingIndicator';
3 import { SmallPreview } from '@launch/components/SmallPreview';
4 import { Title } from '@launch/components/Title';
5 import { useFetch } from '@launch/hooks/useFetch';
6 import { useHomeLayouts } from '@launch/hooks/useHomeLayouts';
7 import { useIsMountedLayout } from '@launch/hooks/useIsMounted';
8 import { PageLayout } from '@launch/layouts/PageLayout';
9 import { pageState } from '@launch/state/factory';
10 import { usePagesSelectionStore } from '@launch/state/pages-selections';
11 import { useUserSelectionStore } from '@launch/state/user-selections';
12 import { Checkmark } from '@launch/svg';
13 import { deepMerge } from '@shared/lib/utils';
14 import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
15 import { __ } from '@wordpress/i18n';
16 import classNames from 'classnames';
17 import { AnimatePresence, motion } from 'framer-motion';
18
19 export const state = pageState('Layout', () => ({
20 ready: false,
21 canSkip: false,
22 useNav: true,
23 onRemove: () => {},
24 }));
25
26 export const HomeSelect = () => {
27 const { loading, homeLayouts } = useHomeLayouts();
28
29 return (
30 <PageLayout>
31 <div className="grow overflow-y-auto px-6 py-8 md:p-12 3xl:p-16">
32 <Title
33 title={__('Pick a design for your website', 'extendify-local')}
34 description={__('You can personalize this later.', 'extendify-local')}
35 />
36 <div className="relative mx-auto w-full max-w-6xl">
37 {loading ? (
38 <LoadingIndicator />
39 ) : (
40 <DesignSelector homeLayouts={homeLayouts} />
41 )}
42 </div>
43 </div>
44 </PageLayout>
45 );
46 };
47
48 const DesignSelector = ({ homeLayouts }) => {
49 const { data: variations } = useFetch('variations', getThemeVariations);
50 const isMounted = useIsMountedLayout();
51 const [styles, setStyles] = useState([]);
52 const { setStyle, style: currentStyle } = usePagesSelectionStore();
53
54 const { siteInformation, siteObjective, setVariation, variation } =
55 useUserSelectionStore();
56
57 const onSelect = useCallback(
58 (style) => {
59 setStyle(style);
60 setVariation(style?.variation);
61 },
62 [setStyle, setVariation],
63 );
64 const wrapperRef = useRef();
65 const once = useRef(false);
66
67 useEffect(() => {
68 state.setState({ ready: !!variation?.title });
69 }, [variation]);
70
71 useEffect(() => {
72 if (!homeLayouts || !variations) return;
73 if (styles.length) return;
74 setStyle(null);
75 setVariation(null);
76 (async () => {
77 const slicedEntries = Array.from(homeLayouts.entries());
78 for (const [index, style] of slicedEntries) {
79 if (!isMounted.current) return;
80
81 const { fonts, colorPalette } = style.siteStyle;
82
83 // Select the variation matching the color palette suggested
84 // by the AI. If none matches, return a random variation.
85 let variation =
86 variations.find(({ slug }) => slug === colorPalette) ??
87 variations[index % variations.length];
88
89 // If a variation matched color palette, replace the fonts with
90 // those suggested by the AI.
91 if (variation.slug === colorPalette && fonts) {
92 variation = deepMerge(variation, {
93 styles: {
94 elements: {
95 heading: {
96 typography: {
97 fontFamily: `var(--wp--preset--font-family--${fonts.heading.slug})`,
98 },
99 },
100 },
101 typography: {
102 fontFamily: `var(--wp--preset--font-family--${fonts.body.slug})`,
103 },
104 },
105 settings: {
106 typography: {
107 fontFamilies: {
108 // If font doesn't have a host, it means it's already installed
109 // with the theme. We only need to add the ones that are not.
110 custom: [fonts.heading, fonts.body].filter(
111 (font) => !!font.host,
112 ),
113 },
114 },
115 },
116 });
117 }
118
119 setStyles((styles) => [
120 ...styles,
121 {
122 ...style,
123 variation,
124 },
125 ]);
126
127 // Delay between 350ms and 1s to make it less rigid
128 const random = Math.floor(Math.random() * (1000 - 150 + 1)) + 150;
129 await new Promise((resolve) => setTimeout(resolve, random));
130 }
131 })();
132 }, [
133 homeLayouts,
134 isMounted,
135 variations,
136 styles.length,
137 setStyle,
138 setVariation,
139 ]);
140
141 useEffect(() => {
142 if (!currentStyle || !styles || once.current) return;
143 const currentButton = wrapperRef.current?.querySelector(
144 `#layout-style-${currentStyle.slug} [role="button"]`,
145 );
146 if (!currentButton) return;
147 once.current = true;
148 currentButton.focus();
149 }, [currentStyle, styles]);
150
151 return (
152 <div
153 className="grid gap-8 md:grid-cols-2 lg:grid-cols-3"
154 data-test="layout-preview-wrapper"
155 ref={wrapperRef}
156 >
157 {styles?.map((style) => (
158 <div className="relative" key={style.id}>
159 <AnimatePresence>
160 <motion.div
161 initial={{ opacity: 0 }}
162 animate={{ opacity: 1 }}
163 duration={0.7}
164 className={classNames(
165 'relative cursor-pointer overflow-hidden rounded-sm border border-gray-200 ring-offset-2 ring-offset-white focus-within:outline-hidden focus-within:ring-4 focus-within:ring-design-main focus-within:ring-offset-2 focus-within:ring-offset-white hover:outline-hidden hover:ring-4',
166 {
167 'ring-4 ring-design-main ring-offset-2 ring-offset-white hover:ring-design-main':
168 currentStyle?.id === style.id,
169 'hover:ring-gray-300': currentStyle?.id !== style.id,
170 },
171 )}
172 style={{ aspectRatio: '1.55' }}
173 >
174 <SmallPreview
175 style={style}
176 showNav={siteObjective !== 'landing-page'}
177 siteTitle={siteInformation.title}
178 onSelect={onSelect}
179 selected={currentStyle?.id === style.id}
180 />
181 </motion.div>
182 </AnimatePresence>
183 <span aria-hidden="true">
184 {currentStyle?.id === style.id ? (
185 <Checkmark className="absolute right-0 top-0 z-50 m-2 h-6 w-6 -translate-y-5 translate-x-5 rounded-full bg-design-main text-design-text" />
186 ) : null}
187 </span>
188 </div>
189 ))}
190 {homeLayouts?.slice(styles?.length).map((_, i) => (
191 <AnimatePresence key={i}>
192 <motion.div
193 initial={{ opacity: 1 }}
194 animate={{ opacity: 1 }}
195 exit={{ opacity: 0 }}
196 duration={0.7}
197 className="relative bg-gray-50"
198 style={{
199 aspectRatio: '1.55',
200 backgroundImage:
201 'linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.5) 50%, rgba(255,255,255,0) 100%)',
202 backgroundSize: '600% 600%',
203 animation: 'extendify-loading-skeleton 10s ease-in-out infinite',
204 }}
205 />
206 </AnimatePresence>
207 ))}
208 </div>
209 );
210 };
211