Skip to content

Commit

Permalink
feat: infinite scroll
Browse files Browse the repository at this point in the history
  • Loading branch information
RUNFUNRUN committed Sep 1, 2024
1 parent 7daa377 commit 80ca82d
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 27 deletions.
Binary file modified bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@tanstack/react-query": "^5.53.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"framer-motion": "^11.3.16",
Expand All @@ -43,6 +44,7 @@
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.52.1",
"react-intersection-observer": "^9.13.0",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
Expand Down
74 changes: 74 additions & 0 deletions src/app/(default)/community/_components/infinite-scroll-art.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use client';

import { ArtCards, SkeletonCards } from '@/components/art-cards';
import type { ArtsResponse, Resolution } from '@/types';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
import { useInView } from 'react-intersection-observer';

export const InfiniteScrollArt = ({
resolution,
}: { resolution: Resolution }) => {
const { ref, inView } = useInView();

const fetchArts = async ({
pageParam,
}: { pageParam: number }): Promise<ArtsResponse> => {
const seartchParams = new URLSearchParams();
seartchParams.set('cursor', pageParam.toString());
seartchParams.set('width', resolution === 'fullhd' ? '26' : '27');

const res = await fetch(`/api/arts/?${seartchParams}`);
return res.json();
};

const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['arts'],
queryFn: fetchArts,
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.next,
});

useEffect(() => {
if (inView) {
fetchNextPage();
}
}, [fetchNextPage, inView]);

return status === 'pending' ? (
<SkeletonCards />
) : status === 'error' ? (
<p className='text-center'>Error: {error.message}</p>
) : (
<>
{data.pages.map((page, i) => (
<ArtCards arts={page.data ?? []} key={i.toString()} />
))}
<div className='text-center mt-8'>
<button
type='button'
ref={ref}
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load Newer'
: 'Nothing more to load'}
</button>
</div>
<div className='text-center mt-8'>
{isFetching && !isFetchingNextPage ? 'Fetching...' : null}
</div>
</>
);
};
14 changes: 14 additions & 0 deletions src/app/(default)/community/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { ReactNode } from 'react';

const queryClient = new QueryClient();

const Layout = ({ children }: { children: ReactNode }) => {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};

export default Layout;
27 changes: 3 additions & 24 deletions src/app/(default)/community/page.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,10 @@
import { prisma } from '@/client';
import { ArtCards, SkeletonCards } from '@/components/art-cards';
import type { Metadata } from 'next';
import { Suspense } from 'react';

export const dynamic = 'force-dynamic';

const ArtsList = async () => {
const arts = await prisma.art.findMany({
orderBy: {
createdAt: 'desc',
},
});
return <ArtCards arts={arts} />;
};
import { InfiniteScrollArt } from './_components/infinite-scroll-art';

const Page = () => {
return (
<div className='container mb-20'>
<div className='flex flex-col gap-4 my-4'>
<Suspense
fallback={Array(3)
.fill(null)
.map((i) => <SkeletonCards key={i} />)}
>
<ArtsList />
</Suspense>
</div>
<div className='container mt-4 mb-20'>
<InfiniteScrollArt resolution='fullhd' />
</div>
);
};
Expand Down
47 changes: 47 additions & 0 deletions src/app/api/arts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { prisma } from '@/client';
import { type NextRequest, NextResponse } from 'next/server';

export const GET = async (req: NextRequest) => {
const pageSize = 20;

const searchParams = req.nextUrl.searchParams;
const cursor = Number.parseInt(searchParams.get('cursor') ?? '0');
const width = Number.parseInt(searchParams.get('width') ?? '26');

if (width !== 26 && width !== 27) {
return NextResponse.json({ success: false }, { status: 400 });
}

if (Number.isNaN(cursor)) {
return NextResponse.json({ success: false }, { status: 400 });
}

try {
const artsCount = await prisma.art.count({
where: {
width,
},
});

if (cursor > artsCount) {
return NextResponse.json({ success: false }, { status: 404 });
}

const arts = await prisma.art.findMany({
orderBy: {
createdAt: 'desc',
},
where: {
width,
},
skip: cursor,
take: pageSize,
});

const next = cursor + pageSize < artsCount ? cursor + pageSize : null;

return NextResponse.json({ success: true, data: arts, next });
} catch {
return NextResponse.json({ success: false }, { status: 500 });
}
};
12 changes: 10 additions & 2 deletions src/components/art-cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Skeleton } from './ui/skeleton';

const ArtCard = ({ art }: { art: Art }) => {
const asciiData = unflattenArray(art.body);
const date = formatDate(art.createdAt);
const date = formatDate(new Date(art.createdAt));

return (
<Card
Expand Down Expand Up @@ -69,5 +69,13 @@ export const ArtCards = ({ arts }: { arts: Art[] }) => {
};

export const SkeletonCards = () => {
return <Skeleton className='w-[586px] h-[330px] mx-auto' />;
return (
<div className='flex flex-col gap-4'>
{Array(3)
.fill(0)
.map((_, i) => (
<Skeleton className='w-[586px] h-[330px] mx-auto' key={i} />
))}
</div>
);
};
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Art } from '@prisma/client';
import type { z } from 'zod';
import type { heightSchema } from './schemas';

Expand Down Expand Up @@ -25,3 +26,9 @@ export type ShareArtResponse = {
slug?: string;
error?: unknown;
};

export type ArtsResponse = {
succsess: boolean;
data?: Array<Art>;
next?: number;
};
9 changes: 8 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,14 @@ export const formatDate = (date: Date): string => {
const locale = getUserLocale();
const timeZone = getUserTimeZone();

return date.toLocaleDateString(locale, {
return date.toLocaleString(locale, {
timeZone: timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true,
});
};

0 comments on commit 80ca82d

Please sign in to comment.