Skip to content

Commit 778b168

Browse files
committed
feat(main): add infinite scroll!
1 parent 186de3d commit 778b168

File tree

13 files changed

+285
-37
lines changed

13 files changed

+285
-37
lines changed

apps/strapi/src/api/front-page-cards/controllers/front-page-cards.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
/**
22
* A set of functions called "actions" for `front-page-cards`
33
*/
4+
import { Controller, GenericController } from '@strapi/strapi/lib/core-api/controller'
45
import { Knex } from 'knex'
56
import { query } from './query'
67

7-
export default {
8-
async frontPageCards(ctx, next) {
8+
const CardController: GenericController = {
9+
async frontPageCards(ctx) {
10+
const queryParams = ctx.query
11+
const { limit = '100', offset = '0' } = queryParams
12+
13+
const parsedLimit = parseInt(Array.isArray(limit) ? limit[0] : limit, 10)
14+
const parsedOffset = parseInt(Array.isArray(offset) ? offset[0] : offset, 10)
15+
916
try {
1017
const db = strapi.db as unknown
1118
// @ts-expect-error shut up
@@ -15,7 +22,12 @@ export default {
1522
// select title, excerpt, url from (select title, excerpt, id, (select file_id from files_related_morphs where (related_type = 'api::blog-post.blog-post' and related_id = blog_posts.id)) as file_id from blog_posts) as sorted_blog join files on files.id = sorted_blog.file_id;
1623
let data
1724
try {
18-
data = await knex.select('*').fromRaw(query).orderBy('published', 'desc').limit(100)
25+
data = await knex
26+
.select('*')
27+
.fromRaw(query)
28+
.orderBy('published', 'desc')
29+
.limit(parsedLimit)
30+
.offset(parsedOffset)
1931
} catch (err) {
2032
console.error(err)
2133
ctx.badRequest('Post report controller error', { moreDetails: err })
@@ -28,6 +40,7 @@ export default {
2840
},
2941
}
3042

43+
export default CardController
3144
// `((select p.id as id, null as guid, 'post' as type, p.title, p.excerpt, p.slug as url, p.slug as identifier, to_jsonb(array_agg(distinct concat(coalesce(tm.first_name, ba.first_name),' ', coalesce(tm.last_name, ba.last_name)))) as authors, coalesce(p.publish_date, p.published_at) as published, to_jsonb(array_agg(bt.title)) as tags, jsonb_build_object('url',f.url,'width', f.width, 'height', f.height, 'alt', f.alternative_text, 'caption', f.caption) as image from blog_posts p left outer join files_related_morphs fp on p.id = fp.related_id LEFT OUTER JOIN files f on f.id=fp.file_id LEFT OUTER JOIN blog_posts_blog_authors_links pal on p.id = pal.blog_post_id FULL JOIN blog_authors ba on pal.blog_author_id = ba.id LEFT OUTER JOIN blog_posts_blog_tags_links ptl on p.id = ptl.blog_post_id LEFT OUTER JOIN tags bt ON ptl.tag_id = bt.id LEFT OUTER JOIN blog_posts_team_members_links pml on p.id = pml.blog_post_id LEFT OUTER JOIN team_members tm ON pml.team_member_id = tm.id LEFT OUTER JOIN blog_posts_category_links pcl on p.id = pcl.blog_post_id LEFT OUTER JOIN categories c ON pcl.category_id = c.id where (fp.related_type is null or fp.related_type = 'api::blog-post.blog-post') group by p.id, guid, type, p.title, p.excerpt, p.slug, p.publish_date, p.published_at, image)
3245
// UNION
3346
// (select j.id as id, j.guid as guid, 'article' as type, j.title as title, j.abstract as excerpt, j.url as url, j.doi as identifier, j.authors, coalesce(j.published, j.published_at) as published, j.keywords as tags, jsonb_build_object('url',f.url,'width', f.width, 'height', f.height, 'alt', f.alternative_text, 'caption', f.caption) as image from jote_articles j left outer join files_related_morphs fp on j.id = fp.related_id left outer join files f on f.id=fp.file_id LEFT OUTER JOIN jote_articles_jote_article_category_links jcl on j.id = jcl.jote_article_id LEFT OUTER JOIN jote_article_categories jc ON jcl.jote_article_category_id = jc.id where (fp.related_type is null or fp.related_type = 'api::jote-article.jote-article')

apps/strapi/src/extensions/documentation/documentation/1.0.0/full_documentation.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"name": "Apache 2.0",
1515
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
1616
},
17-
"x-generation-date": "2023-01-12T14:20:48.277Z"
17+
"x-generation-date": "2023-01-12T18:35:16.801Z"
1818
},
1919
"x-strapi-config": {
2020
"path": "/documentation",

apps/strapi/tsconfig.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@
1717
},
1818
"include": ["./", "src/**/*.json"],
1919
"exclude": [
20-
"**/node_modules/**",
21-
"**/build/**",
22-
"**/dist/**",
23-
"**/.cache/**",
24-
"**/.tmp/**",
20+
"./**/node_modules/**",
21+
"./**/build/**",
22+
"./**/dist/**",
23+
"./**/.cache/**",
24+
"./**/.tmp/**",
2525
"src/admin/",
26-
"**/*.test.ts",
27-
"src/plugins/**"
26+
"./**/*.test.ts",
27+
"src/plugins/**",
28+
"src/extensions/documentation/**"
2829
]
2930
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
'use client'
2+
import Image from 'next/image'
3+
import React from 'react'
4+
import { trpc } from '../utils/trpcClient'
5+
6+
export function useIsIntersecting<TElement extends HTMLElement>() {
7+
// to prevents runtime crash in IE, let's mark it true right away
8+
const [isIntersecting, setIsIntersecting] = React.useState(false)
9+
10+
const ref = React.useRef<TElement>(null)
11+
12+
React.useEffect(() => {
13+
if (!ref.current) {
14+
return
15+
}
16+
const observer = new IntersectionObserver(([entry]) => setIsIntersecting(entry.isIntersecting))
17+
observer.observe(ref.current)
18+
return () => {
19+
observer.disconnect()
20+
}
21+
}, [])
22+
return [isIntersecting, ref] as const
23+
}
24+
25+
export function InfiniteCards() {
26+
const query = trpc.cards.infiniteCards.useInfiniteQuery(
27+
{},
28+
{
29+
getNextPageParam(lastPage) {
30+
console.log(lastPage)
31+
return lastPage.nextCursor
32+
},
33+
},
34+
)
35+
const [isLoadMoreVisible, ref] = useIsIntersecting<HTMLDivElement>()
36+
37+
const fetchNextPageRef = React.useRef(query.fetchNextPage)
38+
fetchNextPageRef.current = query.fetchNextPage
39+
40+
React.useEffect(() => {
41+
if (isLoadMoreVisible && query.hasNextPage && !query.isFetching) {
42+
fetchNextPageRef.current()
43+
}
44+
}, [isLoadMoreVisible, query.hasNextPage, query.isFetching])
45+
46+
console.log(query.data)
47+
return (
48+
<>
49+
{query.data?.pages.map((page) => {
50+
return page.cards.map((card) => {
51+
return (
52+
<article key={card.id} className="relative col-span-1 h-80 border border-black">
53+
<span className="absolute top-4 right-4 flex h-8 w-8 items-center justify-center border border-black bg-white font-bold">
54+
{card.type === 'article' ? 'J' : 'B'}
55+
</span>
56+
{card?.image?.url ? (
57+
<Image
58+
src={card.image.url}
59+
alt={card.image.alt ?? ''}
60+
width={card.image.width}
61+
height={card.image.height}
62+
className="h-full max-h-40 object-cover"
63+
/>
64+
) : (
65+
<div className="flex h-40 items-center justify-center bg-orange-500">
66+
<span className="text-2xl text-black">{card.title[0]}</span>
67+
</div>
68+
)}
69+
<div className="p-4">
70+
<h2 className="my-2 text-xl font-bold leading-tight tracking-tighter">
71+
{card.title}
72+
</h2>
73+
<h2>{card.category}</h2>
74+
</div>
75+
</article>
76+
)
77+
})
78+
})}
79+
80+
<div ref={ref}>
81+
{query.isFetchingNextPage ? (
82+
<span>Loading</span>
83+
) : (
84+
<button
85+
disabled={!query.hasNextPage}
86+
onClick={() => {
87+
query.fetchNextPage()
88+
}}
89+
className={'w-full cursor-pointer p-4' + (!query.hasNextPage ? ' opacity-50' : '')}
90+
>
91+
{query.hasNextPage ? 'Load more' : 'You loaded everything'}
92+
</button>
93+
)}
94+
</div>
95+
</>
96+
)
97+
}

apps/trialanderror.org/src/app/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { getAllCards } from '../server/mixed'
22
import Image from 'next/image'
3+
import { ClientProvider } from '../utils/trpcClient'
4+
import { InfiniteCards } from './InfiniteCards'
35

46
export default async function Page() {
5-
const cards = await getAllCards()
7+
const cards = await getAllCards({ limit: 9 })
8+
69
return (
710
<div className="grid gap-6 p-10 sm:grid-cols-2 lg:grid-cols-3 ">
811
{cards.map((card) => (
@@ -29,6 +32,9 @@ export default async function Page() {
2932
</div>
3033
</article>
3134
))}
35+
<ClientProvider>
36+
<InfiniteCards />
37+
</ClientProvider>
3238
</div>
3339
)
3440
}
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { createTRPCRouter } from "./trpc";
2-
import { exampleRouter } from "./routers/example";
1+
import { createTRPCRouter } from './trpc'
2+
import { exampleRouter } from './routers/example'
3+
import { cardRouter } from './routers/frontPageCards'
34

45
/**
56
* This is the primary router for your server.
@@ -8,7 +9,8 @@ import { exampleRouter } from "./routers/example";
89
*/
910
export const appRouter = createTRPCRouter({
1011
example: exampleRouter,
11-
});
12+
cards: cardRouter,
13+
})
1214

1315
// export type definition of API
14-
export type AppRouter = typeof appRouter;
16+
export type AppRouter = typeof appRouter
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { z } from 'zod'
2+
import { getAllCards } from '../../mixed'
3+
4+
import { createTRPCRouter, publicProcedure, protectedProcedure } from '../trpc'
5+
6+
export const cardRouter = createTRPCRouter({
7+
infiniteCards: publicProcedure
8+
.input(
9+
z.object({
10+
limit: z.number().optional().default(9),
11+
cursor: z.number().optional().default(0),
12+
}),
13+
)
14+
.query(async ({ input }) => {
15+
const cards = await getAllCards({ limit: input.limit + 1, offset: input.cursor })
16+
17+
let nextCursor: number | undefined = undefined
18+
19+
if (cards.length > input.limit) {
20+
const lastCard = cards.pop()
21+
22+
nextCursor = input.cursor + input.limit
23+
}
24+
25+
return {
26+
cards,
27+
nextCursor,
28+
}
29+
}),
30+
})

apps/trialanderror.org/src/server/mixed.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ interface Img {
2828
caption?: string
2929
}
3030

31-
export async function getAllCards() {
31+
export async function getAllCards({ limit = 100, offset = 0 } = {}) {
3232
try {
3333
return (
34-
await fetch(`${env.STRAPI_ENDPOINT}/front-page-cards`, {
34+
await fetch(`${env.STRAPI_ENDPOINT}/front-page-cards?limit=${limit}&offset=${offset}`, {
3535
headers: {
3636
Authorization: `Bearer ${env.STRAPI_API_TOKEN}`,
3737
},

apps/trialanderror.org/src/utils/api.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,17 @@
55
*
66
* We also create a few inference helpers for input and output types
77
*/
8-
import { httpBatchLink, loggerLink } from "@trpc/client";
9-
import { createTRPCNext } from "@trpc/next";
10-
import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
11-
import superjson from "superjson";
12-
13-
import { type AppRouter } from "../server/api/root";
8+
import { httpBatchLink, loggerLink } from '@trpc/client'
9+
import { createTRPCNext } from '@trpc/next'
10+
import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server'
11+
import superjson from 'superjson'
12+
import { type AppRouter } from '../server/api/root'
1413

1514
const getBaseUrl = () => {
16-
if (typeof window !== "undefined") return ""; // browser should use relative url
17-
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
18-
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
19-
};
15+
if (typeof window !== 'undefined') return '' // browser should use relative url
16+
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}` // SSR should use vercel url
17+
return `http://localhost:${process.env.PORT ?? 3000}` // dev SSR should use localhost
18+
}
2019

2120
/**
2221
* A set of typesafe react-query hooks for your tRPC API
@@ -37,29 +36,29 @@ export const api = createTRPCNext<AppRouter>({
3736
links: [
3837
loggerLink({
3938
enabled: (opts) =>
40-
process.env.NODE_ENV === "development" ||
41-
(opts.direction === "down" && opts.result instanceof Error),
39+
process.env.NODE_ENV === 'development' ||
40+
(opts.direction === 'down' && opts.result instanceof Error),
4241
}),
4342
httpBatchLink({
4443
url: `${getBaseUrl()}/api/trpc`,
4544
}),
4645
],
47-
};
46+
}
4847
},
4948
/**
5049
* Whether tRPC should await queries when server rendering pages
5150
* @see https://trpc.io/docs/nextjs#ssr-boolean-default-false
5251
*/
5352
ssr: false,
54-
});
53+
})
5554

5655
/**
5756
* Inference helper for inputs
5857
* @example type HelloInput = RouterInputs['example']['hello']
5958
**/
60-
export type RouterInputs = inferRouterInputs<AppRouter>;
59+
export type RouterInputs = inferRouterInputs<AppRouter>
6160
/**
6261
* Inference helper for outputs
6362
* @example type HelloOutput = RouterOutputs['example']['hello']
6463
**/
65-
export type RouterOutputs = inferRouterOutputs<AppRouter>;
64+
export type RouterOutputs = inferRouterOutputs<AppRouter>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use client'
2+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
3+
import { httpBatchLink, loggerLink } from '@trpc/client'
4+
import { createTRPCReact } from '@trpc/react-query'
5+
import { useState } from 'react'
6+
import superjson from 'superjson'
7+
import type { AppRouter } from '../server/api/root'
8+
9+
export const trpc = createTRPCReact<AppRouter>({
10+
unstable_overrides: {
11+
useMutation: {
12+
async onSuccess(opts) {
13+
await opts.originalFn()
14+
await opts.queryClient.invalidateQueries()
15+
},
16+
},
17+
},
18+
})
19+
20+
function getBaseUrl() {
21+
if (typeof window !== 'undefined')
22+
// browser should use relative path
23+
return ''
24+
if (process.env.VERCEL_URL)
25+
// reference for vercel.com
26+
return `https://${process.env.VERCEL_URL}`
27+
if (process.env.RENDER_INTERNAL_HOSTNAME)
28+
// reference for render.com
29+
return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`
30+
// assume localhost
31+
return `http://localhost:${process.env.PORT ?? 3000}`
32+
}
33+
34+
export function ClientProvider(props: { children: React.ReactNode }) {
35+
const [queryClient] = useState(() => new QueryClient())
36+
const [trpcClient] = useState(() =>
37+
trpc.createClient({
38+
links: [
39+
loggerLink({
40+
enabled: () => true,
41+
}),
42+
httpBatchLink({
43+
url: `${getBaseUrl()}/api/trpc`,
44+
}),
45+
],
46+
transformer: superjson,
47+
}),
48+
)
49+
return (
50+
<trpc.Provider client={trpcClient} queryClient={queryClient}>
51+
<QueryClientProvider client={queryClient}>{props.children}</QueryClientProvider>
52+
</trpc.Provider>
53+
)
54+
}

0 commit comments

Comments
 (0)