PluginProbe
Extendify / 0.6.0
Extendify v0.6.0
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 / pages / GridView.js

GridView.js in Extendify 0.6.0, at src/pages/GridView.js

275 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { Spinner, Button } from '@wordpress/components'
2 import {
3 useEffect,
4 useState,
5 useCallback,
6 useRef,
7 memo,
8 } from '@wordpress/element'
9 import { __, sprintf } from '@wordpress/i18n'
10 import { cloneDeep } from 'lodash'
11 import { useInView } from 'react-intersection-observer'
12 import Masonry from 'react-masonry-css'
13 import { Templates as TemplatesApi } from '@extendify/api/Templates'
14 import { ImportTemplateBlock } from '@extendify/components/ImportTemplateBlock'
15 import { useIsMounted } from '@extendify/hooks/helpers'
16 import { useTestGroup } from '@extendify/hooks/useTestGroup'
17 import { useGlobalStore } from '@extendify/state/GlobalState'
18 import { useTaxonomyStore } from '@extendify/state/Taxonomies'
19 import { useTemplatesStore } from '@extendify/state/Templates'
20
21 export const GridView = memo(function GridView() {
22 const isMounted = useIsMounted()
23 const templates = useTemplatesStore((state) => state.templates)
24 const appendTemplates = useTemplatesStore((state) => state.appendTemplates)
25 const [serverError, setServerError] = useState('')
26 const [nothingFound, setNothingFound] = useState(false)
27 const [loading, setLoading] = useState(false)
28 const [loadMoreRef, inView] = useInView()
29 const searchParamsRaw = useTemplatesStore((state) => state.searchParams)
30 const currentType = useGlobalStore((state) => state.currentType)
31 const resetTemplates = useTemplatesStore((state) => state.resetTemplates)
32 const open = useGlobalStore((state) => state.open)
33 const taxonomies = useTaxonomyStore((state) => state.taxonomies)
34 const updateType = useTemplatesStore((state) => state.updateType)
35 const updateTaxonomies = useTemplatesStore(
36 (state) => state.updateTaxonomies,
37 )
38
39 // Store the next page in case we have pagination
40 const nextPage = useRef(useTemplatesStore.getState().nextPage)
41 const searchParams = useRef(useTemplatesStore.getState().searchParams)
42 const taxonomyType =
43 searchParams.current.type === 'pattern' ? 'patternType' : 'layoutType'
44 const currentTax = searchParams.current.taxonomies[taxonomyType]
45 const defaultOrAlt = useTestGroup('default-or-alt-sitetype', ['A', 'B'])
46
47 // Subscribing to the store will keep these values updates synchronously
48 useEffect(() => {
49 return useTemplatesStore.subscribe(
50 (state) => state.nextPage,
51 (n) => (nextPage.current = n),
52 )
53 }, [])
54 useEffect(() => {
55 return useTemplatesStore.subscribe(
56 (state) => state.searchParams,
57 (s) => (searchParams.current = s),
58 )
59 }, [])
60
61 // Fetch the templates then add them to the current state
62 const fetchTemplates = useCallback(() => {
63 if (!defaultOrAlt) {
64 return
65 }
66 setServerError('')
67 setNothingFound(false)
68 const defaultError = __(
69 'Unknown error occured. Check browser console or contact support.',
70 'extendify',
71 )
72 const args = { offset: nextPage.current }
73 // AB test the default or defaultAlt site type
74 const defaultSiteType =
75 defaultOrAlt === 'A' ? { slug: 'default' } : { slug: 'defaultAlt' }
76 const siteType = searchParams.current.taxonomies?.siteType?.slug?.length
77 ? searchParams.current.taxonomies.siteType
78 : defaultSiteType
79 // End AB test - otherwise use { slug: 'default' } when empty
80 const params = cloneDeep(searchParams.current)
81 params.taxonomies.siteType = siteType
82 TemplatesApi.get(params, args)
83 .then((response) => {
84 if (!isMounted.current) return
85 if (response?.error?.length) {
86 setServerError(response?.error)
87 return
88 }
89 if (response?.records?.length <= 0) {
90 setNothingFound(true)
91 return
92 }
93 if (
94 searchParamsRaw === searchParams.current &&
95 response?.records?.length
96 ) {
97 useTemplatesStore.setState({
98 nextPage: response?.offset ?? '',
99 })
100 appendTemplates(response.records)
101 setLoading(false)
102 }
103 })
104 .catch((error) => {
105 if (!isMounted.current) return
106 console.error(error)
107 setServerError(defaultError)
108 })
109 }, [appendTemplates, isMounted, searchParamsRaw, defaultOrAlt])
110
111 useEffect(() => {
112 if (templates?.length === 0) {
113 setLoading(true)
114 return
115 }
116 }, [templates?.length, searchParamsRaw])
117
118 useEffect(() => {
119 // This will check the URL for a pattern type and set that and remove it
120 // TODO: possibly refactor this if we exapnd it to support layouts
121 if (!open || !taxonomies?.patternType?.length) return
122 const search = new URLSearchParams(window.location.search)
123 if (!search.has('ext-patternType')) return
124 const term = search.get('ext-patternType')
125 // Delete it right away
126 search.delete('ext-patternType')
127 window.history.replaceState(
128 null,
129 null,
130 window.location.pathname + '?' + search.toString(),
131 )
132 // Search the slug in patternTypes
133 const tax = taxonomies.patternType.find((t) => t.slug === term)
134 if (!tax) return
135 updateTaxonomies({ patternType: tax })
136 updateType('pattern')
137 }, [open, taxonomies, updateType, updateTaxonomies])
138
139 // This is the main driver for loading templates
140 // This loads the initial batch of templates. But if we don't yet have taxonomies.
141 // There's also an option to skip loading on first mount
142 useEffect(() => {
143 if (!Object.keys(searchParams.current?.taxonomies)?.length) {
144 return
145 }
146
147 if (useTemplatesStore.getState().skipNextFetch) {
148 // This is useful if the templates are fetched already and
149 // the library moves to/from another state that re-renders
150 // The point is to keep the logic close to the list. That may change someday
151 useTemplatesStore.setState({
152 skipNextFetch: false,
153 })
154 return
155 }
156 fetchTemplates()
157 return () => resetTemplates()
158 }, [fetchTemplates, searchParams, resetTemplates])
159
160 // Fetches when the load more is in view
161 useEffect(() => {
162 nextPage.current && inView && fetchTemplates()
163 }, [inView, fetchTemplates, templates])
164
165 if (serverError.length) {
166 return (
167 <div className="text-left">
168 <h2 className="text-left">{__('Server error', 'extendify')}</h2>
169 <code
170 className="mb-4 block max-w-xl p-4"
171 style={{ minHeight: '10rem' }}>
172 {serverError}
173 </code>
174 <Button
175 isTertiary
176 onClick={() => resetTemplates() && fetchTemplates()}>
177 {__('Press here to reload')}
178 </Button>
179 </div>
180 )
181 }
182
183 if (nothingFound) {
184 return (
185 <div className="-mt-2 flex h-full w-full items-center justify-center sm:mt-0">
186 <h2 className="text-sm font-normal text-extendify-gray">
187 {sprintf(
188 searchParams.current.type === 'template'
189 ? __(
190 'We couldn\'t find any layouts in the "%s" category.',
191 'extendify',
192 )
193 : __(
194 'We couldn\'t find any patterns in the "%s" category.',
195 'extendify',
196 ),
197 currentTax?.title ?? currentTax.slug,
198 )}
199 </h2>
200 </div>
201 )
202 }
203
204 return (
205 <>
206 {loading && (
207 <div className="-mt-2 flex h-full w-full items-center justify-center sm:mt-0">
208 <Spinner />
209 </div>
210 )}
211
212 <Grid type={currentType} templates={templates}>
213 {templates.map((template) => {
214 return (
215 <ImportTemplateBlock
216 maxHeight={
217 currentType === 'template' ? 520 : 'none'
218 }
219 key={template.id}
220 template={template}
221 />
222 )
223 })}
224 </Grid>
225
226 {nextPage.current && (
227 <>
228 <div className="my-20">
229 <Spinner />
230 </div>
231 {/* This is a large div that, when in view, will trigger more patterns to load */}
232 <div
233 className="relative flex -translate-y-full transform flex-col items-end justify-end"
234 ref={loadMoreRef}
235 style={{
236 zIndex: -1,
237 marginBottom: '-100%',
238 height:
239 currentType === 'template' ? '150vh' : '75vh',
240 }}
241 />
242 </>
243 )}
244 </>
245 )
246 })
247
248 const Grid = ({ type, children }) => {
249 const sharedClasses = 'relative min-h-screen z-10 pb-40 pt-0.5'
250 switch (type) {
251 case 'template':
252 return (
253 <div
254 className={`grid gap-6 md:gap-8 lg:grid-cols-2 ${sharedClasses}`}>
255 {children}
256 </div>
257 )
258 }
259 const breakpointColumnsObj = {
260 default: 3,
261 1600: 2,
262 860: 1,
263 599: 2,
264 400: 1,
265 }
266 return (
267 <Masonry
268 breakpointCols={breakpointColumnsObj}
269 className={`-ml-6 flex w-auto px-0.5 md:-ml-8 ${sharedClasses}`}
270 columnClassName="pl-6 md:pl-8 bg-clip-padding space-y-6 md:space-y-8">
271 {children}
272 </Masonry>
273 )
274 }
275