Add infinite scroll to '/' and '/grid'

This commit is contained in:
Sam Becker 2023-09-10 11:24:22 -05:00
parent 17d37cfe5b
commit a1f01788ae
10 changed files with 155 additions and 65 deletions

View File

@ -1,29 +0,0 @@
import SiteGrid from '@/components/SiteGrid';
import PhotoGrid from '@/photo/PhotoGrid';
import { getPhotos } from '@/services/postgres';
export const runtime = 'edge';
const PHOTOS_PER_PAGE = 6;
export default async function GridPage(
{ params }: { params: Record<string, string> }
) {
const offset = parseInt(params.offset ?? '0');
const photos = await getPhotos(
undefined,
PHOTOS_PER_PAGE,
Number.isNaN(offset) ? 0 : offset,
);
return (
<SiteGrid
contentMain={<PhotoGrid
photos={photos}
offset={offset}
staggerOnFirstLoadOnly
/>}
/>
);
}

View File

@ -1,8 +1,9 @@
import MorePhotos from '@/components/MorePhotos';
import SiteGrid from '@/components/SiteGrid';
import { generateImageMetaForPhoto } from '@/photo';
import { generateImageMetaForPhoto, getPhotosLimitForQuery } from '@/photo';
import PhotoGrid from '@/photo/PhotoGrid';
import PhotosEmptyState from '@/photo/PhotosEmptyState';
import { getPhotos } from '@/services/postgres';
import { getPhotos, getPhotosCount } from '@/services/postgres';
import { Metadata } from 'next';
export const runtime = 'edge';
@ -12,16 +13,30 @@ export async function generateMetadata(): Promise<Metadata> {
return generateImageMetaForPhoto(photos[0]);
}
export default async function GridPage() {
const photos = await getPhotos();
export default async function GridPage({
searchParams,
}: {
searchParams: { next: string };
}) {
const { offset, limit } = getPhotosLimitForQuery(searchParams.next);
const photos = await getPhotos(undefined, limit);
const count = await getPhotosCount();
const showMorePhotos = count > photos.length;
return (
photos.length > 0
? <SiteGrid
contentMain={<PhotoGrid
contentMain={<div className="space-y-4">
<PhotoGrid
photos={photos}
staggerOnFirstLoadOnly
/>}
/>
{showMorePhotos &&
<MorePhotos path={`/grid?next=${offset + 1}`} />}
</div>}
/>
: <PhotosEmptyState />
);

View File

@ -1,8 +1,10 @@
import AnimateItems from '@/components/AnimateItems';
import { generateImageMetaForPhoto } from '@/photo';
import MorePhotos from '@/components/MorePhotos';
import SiteGrid from '@/components/SiteGrid';
import { generateImageMetaForPhoto, getPhotosLimitForQuery } from '@/photo';
import PhotoLarge from '@/photo/PhotoLarge';
import PhotosEmptyState from '@/photo/PhotosEmptyState';
import { getPhotos } from '@/services/postgres';
import { getPhotos, getPhotosCount } from '@/services/postgres';
import { Metadata } from 'next';
export const runtime = 'edge';
@ -12,12 +14,23 @@ export async function generateMetadata(): Promise<Metadata> {
return generateImageMetaForPhoto(photos[0]);
}
export default async function HomePage() {
const photos = await getPhotos();
export default async function HomePage({
searchParams,
}: {
searchParams: { next: string };
}) {
const { offset, limit } = getPhotosLimitForQuery(searchParams.next);
const photos = await getPhotos(undefined, limit);
const count = await getPhotosCount();
const showMorePhotos = count > photos.length;
return (
photos.length > 0
? <AnimateItems
? <div className="space-y-4">
<AnimateItems
className="space-y-2"
duration={0.7}
staggerDelay={0.15}
@ -30,6 +43,11 @@ export default async function HomePage() {
priority={index <= 1}
/>)}
/>
{showMorePhotos &&
<SiteGrid
contentMain={<MorePhotos path={`?next=${offset + 1}`} />}
/>}
</div>
: <PhotosEmptyState />
);
}

View File

@ -6,7 +6,11 @@ import ThemeSwitcher from '@/site/ThemeSwitcher';
import { signOut } from 'next-auth/react';
import Link from 'next/link';
export default function Footer() {
export default function Footer({
showSignOut,
}: {
showSignOut?: boolean
}) {
return (
<SiteGrid
contentMain={<div className={cc(
@ -21,6 +25,7 @@ export default function Footer() {
>
Admin
</Link>
{showSignOut &&
<div
className={cc(
'cursor-pointer',
@ -29,7 +34,7 @@ export default function Footer() {
onClick={() => signOut()}
>
Sign out
</div>
</div>}
</div>
<ThemeSwitcher />
</div>}

View File

@ -0,0 +1,59 @@
'use client';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useRef, useTransition } from 'react';
import Spinner from './Spinner';
export default function MorePhotos({
path,
triggerOnView = true,
prefetch = true,
}: {
path: string
triggerOnView?: boolean
prefetch?: boolean
}) {
const router = useRouter();
if (prefetch) {
router.prefetch(path);
}
const [isPending, startTransition] = useTransition();
const buttonRef = useRef<HTMLButtonElement>(null);
const advance = useCallback(() => startTransition(() => {
router.push(path, { scroll: false });
}), [router, path]);
useEffect(() => {
const observer = new IntersectionObserver(e => {
if (triggerOnView && e[0].isIntersecting) {
advance();
}
}, {
root: null,
threshold: 0,
});
observer.observe(buttonRef.current!);
return () => observer.disconnect();
}, [triggerOnView, advance]);
return (
<button
ref={buttonRef}
className="block w-full subtle"
onClick={!triggerOnView ? advance : undefined}
disabled={triggerOnView || isPending}
>
{isPending
? <span className="relative inline-block translate-y-[3px]">
<Spinner size={16} />
</span>
: 'More photos'}
</button>
);
}

View File

@ -120,6 +120,18 @@ export const getNextPhoto = (photo: Photo, photos: Photo[]) => {
: undefined;
};
export const getPhotosLimitForQuery = (
query?: string,
photosPerRequest = 12,
) => {
const offsetInt = parseInt(query ?? '0');
const offset = (Number.isNaN(offsetInt) ? 0 : offsetInt);
return {
offset,
limit: photosPerRequest + offset * photosPerRequest,
};
};
export const generateImageMetaForPhoto = (photo?: Photo): Metadata => photo
? {
openGraph: {

View File

@ -186,6 +186,10 @@ export const getPhotos = async (
return photos;
};
export const getPhotosCount = async () => sql`
SELECT COUNT(*) FROM photos
`.then(({ rows }) => parseInt(rows[0].count, 10));
export const getPhoto = async (id: string): Promise<Photo | undefined> => {
// Check for photo id forwarding
// and convert short ids to uuids

View File

@ -6,7 +6,7 @@ import { cc } from '@/utility/css';
import SiteChecklistRow from './SiteChecklistRow';
import { FiCheckSquare, FiExternalLink } from 'react-icons/fi';
import { BiCopy, BiRefresh } from 'react-icons/bi';
import IconButton from '@/components/LoaderIcon';
import IconButton from '@/components/IconButton';
import { toast } from 'sonner';
import InfoBlock from '@/components/InfoBlock';

View File

@ -59,4 +59,10 @@
hover:border-gray-300 dark:hover:border-gray-600
hover:disabled:border-gray-200
}
button.subtle, .button.subtle {
@apply
disabled:shadow-none
disabled:bg-transparent dark:disabled:bg-transparent
disabled:border-gray-100 dark:disabled:border-gray-900
}
}