PluginProbe
Extendify / 0.9.2
Extendify v0.9.2
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 / Library / pages / GridView.js

GridView.js in Extendify 0.9.2, at src/Library/pages/GridView.js

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