Skip to content

Commit f43c5ad

Browse files
committed
Use paginated app list
1 parent 4d3382a commit f43c5ad

File tree

2 files changed

+201
-167
lines changed

2 files changed

+201
-167
lines changed
Lines changed: 166 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from "react";
1+
import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
22
import {
33
Box,
44
Chip,
@@ -10,135 +10,164 @@ import {
1010
} from "@mui/material";
1111
import SearchIcon from "@mui/icons-material/Search";
1212
import capitalize from "lodash/capitalize";
13-
import { useRecoilValue } from "recoil";
14-
import { axios } from "../../data/axios";
13+
import { useRecoilValue, useRecoilState, useRecoilValueLoadable } from "recoil";
1514
import {
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

93148
export 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

Comments
 (0)