PluginProbe
Extendify / 1.6.1
Extendify v1.6.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 0.7.0 All 126 releases
extendify / src / Library / pages / GridView.js

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

281 lines 10.3 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', 'extendify')}
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 ? // translators: %s is the layout category name
198 __(
199 'We couldn\'t find any layouts in the "%s" category.',
200 'extendify',
201 )
202 : // translators: %s is the pattern category name
203 __(
204 'We couldn\'t find any patterns in the "%s" category.',
205 'extendify',
206 ),
207 currentTax?.title ?? currentTax.slug,
208 )}
209 </h2>
210 </div>
211 )
212 }
213
214 return (
215 <>
216 {loading && (
217 <div className="-mt-2 flex h-full w-full items-center justify-center sm:mt-0">
218 <Spinner />
219 </div>
220 )}
221
222 <Grid type={currentType} templates={templates}>
223 {templates.map((template) => {
224 return (
225 <ImportTemplateBlock
226 maxHeight={
227 currentType === 'template' ? 520 : 'none'
228 }
229 key={template.id}
230 template={template}
231 />
232 )
233 })}
234 </Grid>
235
236 {nextPage.current && (
237 <>
238 <div className="mt-8">
239 <Spinner />
240 </div>
241 <div
242 className="relative flex flex-col items-end justify-end -top-1/4 h-4"
243 ref={loadMoreRef}
244 style={{ zIndex: -1 }}
245 />
246 </>
247 )}
248 </>
249 )
250 })
251
252 const Grid = ({ type, children }) => {
253 const sharedClasses = 'relative min-h-screen z-10 pb-40 pt-0.5'
254 switch (type) {
255 case 'template':
256 return (
257 <div
258 id="masonry-grid"
259 className={`grid gap-6 md:gap-8 lg:grid-cols-2 ${sharedClasses}`}>
260 {children}
261 </div>
262 )
263 }
264 const breakpointColumnsObj = {
265 default: 3,
266 1600: 2,
267 860: 1,
268 599: 2,
269 400: 1,
270 }
271 return (
272 <Masonry
273 id="masonry-grid"
274 breakpointCols={breakpointColumnsObj}
275 className={`-ml-6 flex w-auto px-0.5 md:-ml-8 ${sharedClasses}`}
276 columnClassName="pl-6 md:pl-8 bg-clip-padding space-y-6 md:space-y-8">
277 {children}
278 </Masonry>
279 )
280 }
281