1- import { useEffect , useState } from "react" ;
1+ import { forwardRef , useCallback , useEffect , useRef , useState } from "react" ;
22import {
33 Box ,
44 Chip ,
@@ -10,135 +10,164 @@ import {
1010} from "@mui/material" ;
1111import SearchIcon from "@mui/icons-material/Search" ;
1212import capitalize from "lodash/capitalize" ;
13- import { useRecoilValue } from "recoil" ;
14- import { axios } from "../../data/axios" ;
13+ import { useRecoilValue , useRecoilState , useRecoilValueLoadable } from "recoil" ;
1514import {
16- appsByStoreCategoryState ,
15+ appsPageState ,
1716 storeCategoriesSlugState ,
17+ fetchAppsFromStore ,
1818} from "../../data/atoms" ;
1919
20- function AppEntry ( { app } ) {
21- return (
20+ const AppEntry = forwardRef ( ( { app } , ref ) => (
21+ < Box
22+ ref = { ref }
23+ sx = { {
24+ border : "1px solid #e0e0e0" ,
25+ borderRadius : "4px" ,
26+ backgroundColor : "#f8f8f8" ,
27+ p : 1 ,
28+ m : 1 ,
29+ display : "flex" ,
30+ alignItems : "center" ,
31+ cursor : "pointer" ,
32+ textAlign : "left" ,
33+ ":hover" : {
34+ backgroundColor : "#edeff7" ,
35+ borderColor : "#d0d0d0" ,
36+ borderRadius : "4px" ,
37+ boxShadow : "0 0 0 1px #d0d0d0" ,
38+ } ,
39+ } }
40+ onClick = { ( ) => {
41+ window . location . href = `/a/${ app . slug } ` ;
42+ } }
43+ >
2244 < Box
2345 sx = { {
24- border : "1px solid #e0e0e0" ,
25- borderRadius : "4px" ,
26- backgroundColor : "#f8f8f8" ,
27- p : 1 ,
28- m : 1 ,
2946 display : "flex" ,
3047 alignItems : "center" ,
31- cursor : "pointer" ,
3248 textAlign : "left" ,
33- ":hover" : {
34- backgroundColor : "#edeff7" ,
35- borderColor : "#d0d0d0" ,
36- borderRadius : "4px" ,
37- boxShadow : "0 0 0 1px #d0d0d0" ,
38- } ,
39- } }
40- onClick = { ( ) => {
41- window . location . href = `/a/${ app . slug } ` ;
49+ pl : 1 ,
50+ pb : 1 ,
4251 } }
4352 >
44- < Box
45- sx = { {
46- display : "flex" ,
47- alignItems : "center" ,
48- textAlign : "left" ,
49- pl : 1 ,
50- pb : 1 ,
53+ < img
54+ src = { app . icon128 }
55+ alt = { app . name }
56+ style = { {
57+ width : 70 ,
58+ height : 70 ,
59+ margin : "1em 0.5em" ,
60+ borderRadius : "0.2em" ,
61+ alignSelf : "start" ,
5162 } }
52- >
53- < img
54- src = { app . icon128 }
55- alt = { app . name }
56- style = { {
57- width : 70 ,
58- height : 70 ,
59- margin : "1em 0.5em" ,
60- borderRadius : "0.2em" ,
61- alignSelf : "start" ,
62- } }
63- />
64- < Box sx = { { padding : 2 } } >
65- < Typography
66- component = "div"
67- color = "text.primary"
68- sx = { { fontSize : 18 , fontWeight : 600 } }
69- >
70- { app . name }
71- </ Typography >
72- < Typography variant = "body2" color = "text.secondary" >
73- { app . description ?. length > 200
74- ? `${ app . description . substring ( 0 , 200 ) } ...`
75- : app . description }
76- </ Typography >
77- < Box sx = { { mt : 1 , mb : 1 } } >
78- { app . categories &&
79- app . categories . map ( ( category ) => (
80- < Chip
81- label = { capitalize ( category ) }
82- size = "small"
83- key = { category }
84- />
85- ) ) }
86- </ Box >
63+ />
64+ < Box sx = { { padding : 2 } } >
65+ < Typography
66+ component = "div"
67+ color = "text.primary"
68+ sx = { { fontSize : 18 , fontWeight : 600 } }
69+ >
70+ { app . name }
71+ </ Typography >
72+ < Typography variant = "body2" color = "text.secondary" >
73+ { app . description ?. length > 200
74+ ? `${ app . description . substring ( 0 , 200 ) } ...`
75+ : app . description }
76+ </ Typography >
77+ < Box sx = { { mt : 1 , mb : 1 } } >
78+ { app . categories &&
79+ app . categories . map ( ( category ) => (
80+ < Chip label = { capitalize ( category ) } size = "small" key = { category } />
81+ ) ) }
8782 </ Box >
8883 </ Box >
8984 </ Box >
85+ </ Box >
86+ ) ) ;
87+
88+ const AppList = ( { queryTerm } ) => {
89+ const loaderRef = useRef ( null ) ;
90+ const [ nextPage , setNextPage ] = useState ( null ) ;
91+ const appsLoadable = useRecoilValueLoadable (
92+ fetchAppsFromStore ( { queryTerm, nextPage } ) ,
9093 ) ;
91- }
94+ const [ appsData , setAppsData ] = useRecoilState ( appsPageState ( queryTerm ) ) ;
95+
96+ const appendFetchedApps = useCallback ( ( ) => {
97+ if ( appsLoadable . state !== "hasValue" ) return ;
98+
99+ setAppsData ( ( oldAppsData ) => {
100+ const newApps = appsLoadable . contents . apps . filter (
101+ ( app ) => ! oldAppsData . apps . some ( ( oldApp ) => oldApp . slug === app . slug ) ,
102+ ) ;
103+
104+ return {
105+ apps : [ ...oldAppsData . apps , ...newApps ] ,
106+ nextPage : appsLoadable . contents . nextPage ,
107+ } ;
108+ } ) ;
109+ } , [ appsLoadable . contents , setAppsData , appsLoadable . state ] ) ;
110+
111+ useEffect ( ( ) => {
112+ if ( loaderRef . current && appsLoadable . state === "hasValue" ) {
113+ setAppsData ( appsLoadable . contents ) ;
114+ const observer = new IntersectionObserver ( ( entries ) => {
115+ if ( entries [ 0 ] . isIntersecting && appsLoadable . contents . nextPage ) {
116+ appendFetchedApps ( ) ;
117+ setNextPage ( appsLoadable . contents . nextPage ) ;
118+ }
119+ } ) ;
120+ observer . observe ( loaderRef . current ) ;
121+ return ( ) => observer . disconnect ( ) ;
122+ }
123+ } , [ loaderRef , appsLoadable , appendFetchedApps , setAppsData ] ) ;
124+
125+ const apps = appsData ?. apps || [ ] ;
126+ return (
127+ < Box sx = { { overflowY : "auto" , flex : "1 1 auto" } } >
128+ { apps . length > 0 ? (
129+ apps . map ( ( app , index ) => (
130+ < AppEntry
131+ app = { app }
132+ key = { app . slug }
133+ ref = { index + 1 === apps . length ? loaderRef : null }
134+ />
135+ ) )
136+ ) : (
137+ < Box ref = { loaderRef } />
138+ ) }
139+ { appsLoadable . state === "loading" && (
140+ < Box >
141+ < CircularProgress />
142+ </ Box >
143+ ) }
144+ </ Box >
145+ ) ;
146+ } ;
92147
93148export default function Search ( { appSlug } ) {
94149 const [ categoryFilter , setCategoryFilter ] = useState (
95150 appSlug ? "recommended" : "featured" ,
96151 ) ;
152+ const [ queryTerm , setQueryTerm ] = useState (
153+ appSlug
154+ ? `categories/recommended/${ appSlug } /apps`
155+ : "categories/featured/apps" ,
156+ ) ;
97157 const defaultCategories = useRecoilValue ( storeCategoriesSlugState ) ;
98158 const [ appCategories , setAppCategories ] = useState ( defaultCategories ) ;
99159 const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
100- const [ searching , setSearching ] = useState ( false ) ;
101- const [ apps , setApps ] = useState ( [ ] ) ;
102- const appsByStoreCategory = useRecoilValue (
103- appsByStoreCategoryState (
104- categoryFilter . toLowerCase ( ) . startsWith ( "recommended" )
105- ? `recommended/${ appSlug } `
106- : categoryFilter . toLowerCase ( ) ,
107- ) ,
108- ) ;
109160
110161 useEffect ( ( ) => {
111162 if ( categoryFilter && searchTerm === "" ) {
112- setApps ( appsByStoreCategory ) ;
113- setAppCategories ( defaultCategories ) ;
114- }
115- } , [ appsByStoreCategory , defaultCategories , searchTerm , categoryFilter ] ) ;
116-
117- const searchApps = ( term ) => {
118- setSearching ( true ) ;
119- if ( term === "" ) {
120- setApps ( appsByStoreCategory ) ;
121163 setAppCategories ( defaultCategories ) ;
122- setSearching ( false ) ;
123- } else {
124- axios ( )
125- . get ( `/api/store/search?query=${ term } ` )
126- . then ( ( response ) => {
127- setApps ( response . data ?. results || [ ] ) ;
128-
129- const categories = response . data ?. results
130- ?. map ( ( app ) => app ?. categories )
131- . flat ( ) ;
132- setAppCategories ( [ ...new Set ( categories ) ] ) ;
133- } )
134- . catch ( ( error ) => {
135- console . error ( error ) ;
136- } )
137- . finally ( ( ) => {
138- setSearching ( false ) ;
139- } ) ;
164+ setQueryTerm (
165+ categoryFilter . toLowerCase ( ) . startsWith ( "recommended" )
166+ ? `categories/recommended/${ appSlug } /apps`
167+ : `categories/${ categoryFilter . toLowerCase ( ) } /apps` ,
168+ ) ;
140169 }
141- } ;
170+ } , [ defaultCategories , searchTerm , categoryFilter , appSlug ] ) ;
142171
143172 return (
144173 < Box
@@ -152,7 +181,8 @@ export default function Search({ appSlug }) {
152181 sx = { { p : "2px 4px" , display : "flex" , alignItems : "center" } }
153182 onSubmit = { ( e ) => {
154183 e . preventDefault ( ) ;
155- searchApps ( searchTerm ) ;
184+ // searchApps(searchTerm);
185+ setQueryTerm ( `search?query=${ searchTerm } ` ) ;
156186 } }
157187 >
158188 < InputBase
@@ -168,61 +198,45 @@ export default function Search({ appSlug }) {
168198 type = "button"
169199 sx = { { p : "10px" } }
170200 aria-label = "search"
171- onClick = { ( ) => searchApps ( searchTerm ) }
201+ onClick = { ( ) => setQueryTerm ( `search?query= ${ searchTerm } ` ) }
172202 >
173203 < SearchIcon />
174204 </ IconButton >
175205 </ Paper >
176- { searching && (
177- < Box sx = { { textAlign : "center" , mt : 2 } } >
178- < CircularProgress />
179- </ Box >
180- ) }
181206
182- { ! searching && (
183- < >
184- < Box sx = { { textAlign : "left" , mt : 1 } } >
185- { appCategories . map ( ( category ) => (
186- < Chip
187- key = { category }
188- label = { capitalize ( category ) }
189- size = "small"
190- variant = {
191- categoryFilter . toLowerCase ( ) === category . toLowerCase ( ) ||
192- ( categoryFilter . startsWith ( "recommended" ) &&
193- category . toLowerCase ( ) . startsWith ( "recommended" ) )
194- ? "filled"
195- : "outlined"
196- }
197- sx = { {
198- cursor : "pointer" ,
199- m : 0.5 ,
200- border :
201- categoryFilter . toLowerCase ( ) === category . toLowerCase ( )
202- ? "1px solid #b0b0b0"
203- : "1px solid #e0e0e0" ,
204- } }
205- onClick = { ( ) =>
206- setCategoryFilter (
207- category . toLowerCase ( ) . startsWith ( "recommended" )
208- ? `recommended/${ appSlug } `
209- : category . toLowerCase ( ) ,
210- )
211- }
212- />
213- ) ) }
214- </ Box >
215- { apps . length === 0 && (
216- < Box sx = { { textAlign : "center" , mt : 2 } } >
217- < p > No apps found</ p >
218- </ Box >
219- ) }
220- < Box sx = { { overflowY : "auto" , flex : "1 1 auto" } } >
221- { apps . length > 0 &&
222- apps . map ( ( app ) => < AppEntry app = { app } key = { app . slug } /> ) }
223- </ Box >
224- </ >
225- ) }
207+ < Box sx = { { textAlign : "left" , mt : 1 } } >
208+ { appCategories . map ( ( category ) => (
209+ < Chip
210+ key = { category }
211+ label = { capitalize ( category ) }
212+ size = "small"
213+ variant = {
214+ categoryFilter . toLowerCase ( ) === category . toLowerCase ( ) ||
215+ ( categoryFilter . startsWith ( "recommended" ) &&
216+ category . toLowerCase ( ) . startsWith ( "recommended" ) )
217+ ? "filled"
218+ : "outlined"
219+ }
220+ sx = { {
221+ cursor : "pointer" ,
222+ m : 0.5 ,
223+ border :
224+ categoryFilter . toLowerCase ( ) === category . toLowerCase ( )
225+ ? "1px solid #b0b0b0"
226+ : "1px solid #e0e0e0" ,
227+ } }
228+ onClick = { ( ) => {
229+ setCategoryFilter ( category ) ;
230+ setQueryTerm (
231+ category . toLowerCase ( ) . startsWith ( "recommended" )
232+ ? `categories/recommended/${ appSlug } /apps`
233+ : `categories/${ category . toLowerCase ( ) } /apps` ,
234+ ) ;
235+ } }
236+ />
237+ ) ) }
238+ </ Box >
239+ < AppList queryTerm = { queryTerm } appSlug = { appSlug } />
226240 </ Box >
227241 ) ;
228242}
0 commit comments