@@ -9,9 +9,9 @@ import { Badge } from "@/components/ui/badge";
9
9
import { Button } from "@/components/ui/button" ;
10
10
import { LayoutGrid , List , Search } from "lucide-react" ;
11
11
import { Table , TableBody , TableCell , TableRow } from "@/components/ui/table" ;
12
- import { FilterPopover } from "@/components/filter-popover" ;
12
+ import * as timeago from "timeago.js" ;
13
+ // import { FilterPopover } from "@/components/filter-popover";
13
14
14
- // Internal components
15
15
function ViewToggle ( {
16
16
view,
17
17
onChange,
@@ -52,8 +52,8 @@ function ViewToggle({
52
52
function RecipeFilters ( {
53
53
search,
54
54
onSearchChange,
55
- selectedCategories,
56
- onCategoriesChange,
55
+ // selectedCategories,
56
+ // onCategoriesChange,
57
57
} : {
58
58
search : string ;
59
59
onSearchChange : ( value : string ) => void ;
@@ -62,20 +62,20 @@ function RecipeFilters({
62
62
} ) {
63
63
return (
64
64
< div className = "flex flex-row gap-2 flex-wrap items-start justify-between" >
65
- < div className = "relative w-1 /3" >
65
+ < div className = "relative w-2 /3" >
66
66
< Search className = "absolute left-2 top-3 h-4 w-4 text-muted-foreground" />
67
67
< Input
68
68
type = "search"
69
- placeholder = "Search by keywords..."
69
+ placeholder = "Search by title, description, or keywords..."
70
70
className = "font-aeonikFono text-md pl-8"
71
71
value = { search }
72
72
onChange = { ( e ) => onSearchChange ( e . target . value ) }
73
73
/>
74
74
</ div >
75
- < FilterPopover
75
+ { /* <FilterPopover
76
76
selectedCategories={selectedCategories}
77
77
onCategoriesChange={onCategoriesChange}
78
- />
78
+ /> */ }
79
79
</ div >
80
80
) ;
81
81
}
@@ -89,8 +89,8 @@ function CookbookContent({ initialRecipes, recipeCards }: CookbookProps) {
89
89
const router = useRouter ( ) ;
90
90
const searchParams = useSearchParams ( ) ;
91
91
92
- const ITEMS_PER_PAGE = 10 ;
93
- const [ currentPage , _ ] = useState ( 1 ) ;
92
+ const ITEMS_PER_PAGE = 9 ;
93
+ const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
94
94
95
95
const [ view , setView ] = useState < "grid" | "list" > ( ( ) => {
96
96
return ( searchParams . get ( "view" ) as "grid" | "list" ) || "list" ;
@@ -125,15 +125,16 @@ function CookbookContent({ initialRecipes, recipeCards }: CookbookProps) {
125
125
} ;
126
126
127
127
const handleCategoriesChange = ( categories : string [ ] ) => {
128
+ setCurrentPage ( 1 ) ;
128
129
setSelectedCategories ( categories ) ;
129
130
updateURL ( view , categories ) ;
130
131
} ;
131
132
132
133
const handleSearchChange = ( value : string ) => {
134
+ setCurrentPage ( 1 ) ;
133
135
setSearch ( value ) ;
134
136
} ;
135
137
136
- // Create a map of recipe IDs to their corresponding rendered cards
137
138
const recipeCardMap = useMemo ( ( ) => {
138
139
return initialRecipes . reduce (
139
140
( map , recipe , index ) => {
@@ -145,50 +146,75 @@ function CookbookContent({ initialRecipes, recipeCards }: CookbookProps) {
145
146
} , [ initialRecipes , recipeCards ] ) ;
146
147
147
148
const filteredRecipeCards = useMemo ( ( ) => {
148
- // First sort by date
149
149
const sortedRecipes = [ ...initialRecipes ] . sort ( ( a , b ) => {
150
150
return new Date ( b . date ) . getTime ( ) - new Date ( a . date ) . getTime ( ) ;
151
151
} ) ;
152
152
153
- // Then apply filters
153
+ // Filter all recipes without pagination
154
154
const filteredRecipes = sortedRecipes . filter ( ( recipe ) => {
155
- const searchText = search . toLowerCase ( ) ;
155
+ const searchText = search . toLowerCase ( ) . trim ( ) ;
156
+
157
+ if ( ! searchText ) {
158
+ return true ;
159
+ }
160
+
161
+ const titleMatch = recipe . title . toLowerCase ( ) . includes ( searchText ) ;
162
+ const descriptionMatch = recipe . description
163
+ . toLowerCase ( )
164
+ . includes ( searchText ) ;
165
+ const categoryMatch = recipe . categories . some ( ( category ) =>
166
+ category . toLowerCase ( ) . includes ( searchText )
167
+ ) ;
168
+ const tagMatch = recipe . tags . some ( ( tag ) =>
169
+ tag . toLowerCase ( ) . includes ( searchText )
170
+ ) ;
171
+
156
172
const matchesSearch =
157
- recipe . title . toLowerCase ( ) . includes ( searchText ) ||
158
- recipe . description . toLowerCase ( ) . includes ( searchText ) ||
159
- recipe . categories . some ( ( category ) =>
160
- category . toLowerCase ( ) . includes ( searchText )
161
- ) ||
162
- recipe . tags . some ( ( tag ) => tag . toLowerCase ( ) . includes ( searchText ) ) ;
173
+ titleMatch || descriptionMatch || categoryMatch || tagMatch ;
163
174
164
175
const matchesCategories =
165
176
selectedCategories . length === 0 ||
166
177
recipe . categories . some ( ( category ) =>
167
178
selectedCategories . includes ( category . toLowerCase ( ) )
168
179
) ;
169
180
170
- return matchesSearch && matchesCategories ;
181
+ const shouldInclude = matchesSearch && matchesCategories ;
182
+
183
+ return shouldInclude ;
171
184
} ) ;
172
185
173
186
const startIndex = 0 ;
174
187
const endIndex = currentPage * ITEMS_PER_PAGE ;
175
188
176
- return filteredRecipes
177
- . slice ( startIndex , endIndex )
178
- . map ( ( recipe ) => recipeCardMap [ recipe . id ] ) ;
189
+ const displayedRecipes = filteredRecipes . slice ( startIndex , endIndex ) ;
190
+
191
+ return displayedRecipes . map ( ( recipe ) => ( {
192
+ recipe,
193
+ card : recipeCardMap [ recipe . id ] ,
194
+ } ) ) ;
179
195
} , [ search , selectedCategories , initialRecipes , recipeCardMap , currentPage ] ) ;
180
196
181
- // Add total pages calculation
182
197
const totalPages = useMemo ( ( ) => {
183
198
const filteredLength = initialRecipes . filter ( ( recipe ) => {
184
- const searchText = search . toLowerCase ( ) ;
199
+ const searchText = search . toLowerCase ( ) . trim ( ) ;
200
+
201
+ if ( ! searchText ) {
202
+ return true ;
203
+ }
204
+
205
+ const titleMatch = recipe . title . toLowerCase ( ) . includes ( searchText ) ;
206
+ const descriptionMatch = recipe . description
207
+ . toLowerCase ( )
208
+ . includes ( searchText ) ;
209
+ const categoryMatch = recipe . categories . some ( ( category ) =>
210
+ category . toLowerCase ( ) . includes ( searchText )
211
+ ) ;
212
+ const tagMatch = recipe . tags . some ( ( tag ) =>
213
+ tag . toLowerCase ( ) . includes ( searchText )
214
+ ) ;
215
+
185
216
const matchesSearch =
186
- recipe . title . toLowerCase ( ) . includes ( searchText ) ||
187
- recipe . description . toLowerCase ( ) . includes ( searchText ) ||
188
- recipe . categories . some ( ( category ) =>
189
- category . toLowerCase ( ) . includes ( searchText )
190
- ) ||
191
- recipe . tags . some ( ( tag ) => tag . toLowerCase ( ) . includes ( searchText ) ) ;
217
+ titleMatch || descriptionMatch || categoryMatch || tagMatch ;
192
218
193
219
const matchesCategories =
194
220
selectedCategories . length === 0 ||
@@ -206,14 +232,30 @@ function CookbookContent({ initialRecipes, recipeCards }: CookbookProps) {
206
232
207
233
const lastItemRef = useRef < HTMLTableRowElement > ( null ) ;
208
234
235
+ const [ hasScrolled , setHasScrolled ] = useState ( false ) ;
236
+
237
+ useEffect ( ( ) => {
238
+ const handleScroll = ( ) => {
239
+ if ( ! hasScrolled ) {
240
+ setHasScrolled ( true ) ;
241
+ }
242
+ } ;
243
+
244
+ window . addEventListener ( "scroll" , handleScroll ) ;
245
+ return ( ) => window . removeEventListener ( "scroll" , handleScroll ) ;
246
+ } , [ hasScrolled ] ) ;
247
+
209
248
useEffect ( ( ) => {
210
249
const observer = new IntersectionObserver (
211
250
( entries ) => {
212
251
const lastEntry = entries [ 0 ] ;
213
- if ( lastEntry . isIntersecting && ! isLoading ) {
214
- // Check if we have more pages to load
252
+ if ( lastEntry . isIntersecting && ! isLoading && hasScrolled ) {
215
253
if ( currentPage < totalPages ) {
216
254
setIsLoading ( true ) ;
255
+ setTimeout ( ( ) => {
256
+ setCurrentPage ( ( prev ) => prev + 1 ) ;
257
+ setIsLoading ( false ) ;
258
+ } , 500 ) ;
217
259
}
218
260
}
219
261
} ,
@@ -230,7 +272,7 @@ function CookbookContent({ initialRecipes, recipeCards }: CookbookProps) {
230
272
observer . unobserve ( currentRef ) ;
231
273
}
232
274
} ;
233
- } , [ currentPage , totalPages , isLoading ] ) ;
275
+ } , [ currentPage , totalPages , isLoading , hasScrolled ] ) ;
234
276
235
277
return (
236
278
< div className = "max-w-5xl mx-auto" >
@@ -239,8 +281,8 @@ function CookbookContent({ initialRecipes, recipeCards }: CookbookProps) {
239
281
< div className = "space-y-1" >
240
282
< h1 className = "text-4xl font-semibold" > Cookbook</ h1 >
241
283
< p className = "text-lg text-muted-foreground w-full" >
242
- Explore ready-to-use code recipes for building applications on
243
- Stacks.
284
+ Explore common blockchain recipes with ready-to-copy examples for
285
+ building applications on Stacks and Bitcoin .
244
286
</ p >
245
287
</ div >
246
288
< ViewToggle view = { view } onChange = { handleViewChange } />
@@ -257,8 +299,7 @@ function CookbookContent({ initialRecipes, recipeCards }: CookbookProps) {
257
299
{ view === "list" ? (
258
300
< Table >
259
301
< TableBody >
260
- { filteredRecipeCards . map ( ( recipeCard , index ) => {
261
- const recipe = initialRecipes [ index ] ;
302
+ { filteredRecipeCards . map ( ( { recipe, card } , index ) => {
262
303
const isLastItem = index === filteredRecipeCards . length - 1 ;
263
304
264
305
return (
@@ -282,16 +323,19 @@ function CookbookContent({ initialRecipes, recipeCards }: CookbookProps) {
282
323
) ) }
283
324
</ div >
284
325
</ TableCell >
326
+ < TableCell className = "text-muted-foreground font-aeonikFono" >
327
+ { timeago . format ( new Date ( recipe . date ) ) }
328
+ </ TableCell >
285
329
</ TableRow >
286
330
) ;
287
331
} ) }
288
332
</ TableBody >
289
333
</ Table >
290
334
) : (
291
335
< div className = "grid grid-cols-1 md:grid-cols-2 gap-6" >
292
- { filteredRecipeCards . map ( ( card , index ) => (
336
+ { filteredRecipeCards . map ( ( { recipe , card } , index ) => (
293
337
< div
294
- key = { index }
338
+ key = { recipe . id }
295
339
ref = {
296
340
index === filteredRecipeCards . length - 1
297
341
? lastItemRef
0 commit comments