PluginProbe
Extendify / 0.5.0
Extendify v0.5.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.5.0, at src/pages/GridView.js

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