Add infinite scroll to '/' and '/grid'
This commit is contained in:
parent
17d37cfe5b
commit
a1f01788ae
@ -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
|
||||
/>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
photos={photos}
|
||||
staggerOnFirstLoadOnly
|
||||
/>}
|
||||
contentMain={<div className="space-y-4">
|
||||
<PhotoGrid
|
||||
photos={photos}
|
||||
staggerOnFirstLoadOnly
|
||||
/>
|
||||
{showMorePhotos &&
|
||||
<MorePhotos path={`/grid?next=${offset + 1}`} />}
|
||||
</div>}
|
||||
/>
|
||||
: <PhotosEmptyState />
|
||||
);
|
||||
|
||||
@ -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,24 +14,40 @@ 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
|
||||
className="space-y-2"
|
||||
duration={0.7}
|
||||
staggerDelay={0.15}
|
||||
distanceOffset={0}
|
||||
staggerOnFirstLoadOnly
|
||||
items={photos.map((photo, index) =>
|
||||
<PhotoLarge
|
||||
key={photo.id}
|
||||
photo={photo}
|
||||
priority={index <= 1}
|
||||
/>)}
|
||||
/>
|
||||
? <div className="space-y-4">
|
||||
<AnimateItems
|
||||
className="space-y-2"
|
||||
duration={0.7}
|
||||
staggerDelay={0.15}
|
||||
distanceOffset={0}
|
||||
staggerOnFirstLoadOnly
|
||||
items={photos.map((photo, index) =>
|
||||
<PhotoLarge
|
||||
key={photo.id}
|
||||
photo={photo}
|
||||
priority={index <= 1}
|
||||
/>)}
|
||||
/>
|
||||
{showMorePhotos &&
|
||||
<SiteGrid
|
||||
contentMain={<MorePhotos path={`?next=${offset + 1}`} />}
|
||||
/>}
|
||||
</div>
|
||||
: <PhotosEmptyState />
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,15 +25,16 @@ export default function Footer() {
|
||||
>
|
||||
Admin
|
||||
</Link>
|
||||
<div
|
||||
className={cc(
|
||||
'cursor-pointer',
|
||||
'hover:text-gray-600 dark:hover:text-gray-400',
|
||||
)}
|
||||
onClick={() => signOut()}
|
||||
>
|
||||
Sign out
|
||||
</div>
|
||||
{showSignOut &&
|
||||
<div
|
||||
className={cc(
|
||||
'cursor-pointer',
|
||||
'hover:text-gray-600 dark:hover:text-gray-400',
|
||||
)}
|
||||
onClick={() => signOut()}
|
||||
>
|
||||
Sign out
|
||||
</div>}
|
||||
</div>
|
||||
<ThemeSwitcher />
|
||||
</div>}
|
||||
|
||||
59
src/components/MorePhotos.tsx
Normal file
59
src/components/MorePhotos.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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: {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user