diff --git a/src/app/(isr)/grid/[offset]/page.tsx b/src/app/(isr)/grid/[offset]/page.tsx deleted file mode 100644 index 97d67e5f..00000000 --- a/src/app/(isr)/grid/[offset]/page.tsx +++ /dev/null @@ -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 } -) { - const offset = parseInt(params.offset ?? '0'); - - const photos = await getPhotos( - undefined, - PHOTOS_PER_PAGE, - Number.isNaN(offset) ? 0 : offset, - ); - - return ( - } - /> - ); -} diff --git a/src/app/(isr)/grid/page.tsx b/src/app/(isr)/grid/page.tsx index 96695b1b..d1946e6d 100644 --- a/src/app/(isr)/grid/page.tsx +++ b/src/app/(isr)/grid/page.tsx @@ -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 { 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 ? } + contentMain={
+ + {showMorePhotos && + } +
} /> : ); diff --git a/src/app/(isr)/page.tsx b/src/app/(isr)/page.tsx index f158503e..3736e778 100644 --- a/src/app/(isr)/page.tsx +++ b/src/app/(isr)/page.tsx @@ -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 { 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 - ? - )} - /> + ?
+ + )} + /> + {showMorePhotos && + } + />} +
: ); } diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index d2589fc8..1d6f3001 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -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 ( Admin -
signOut()} - > - Sign out -
+ {showSignOut && +
signOut()} + > + Sign out +
} } diff --git a/src/components/LoaderIcon.tsx b/src/components/IconButton.tsx similarity index 100% rename from src/components/LoaderIcon.tsx rename to src/components/IconButton.tsx diff --git a/src/components/MorePhotos.tsx b/src/components/MorePhotos.tsx new file mode 100644 index 00000000..136c4317 --- /dev/null +++ b/src/components/MorePhotos.tsx @@ -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(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 ( + + ); +} diff --git a/src/photo/index.ts b/src/photo/index.ts index 4e458804..b595911e 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -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: { diff --git a/src/services/postgres.ts b/src/services/postgres.ts index 97f38e81..7998d5c5 100644 --- a/src/services/postgres.ts +++ b/src/services/postgres.ts @@ -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 => { // Check for photo id forwarding // and convert short ids to uuids diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx index 279f4744..6934b186 100644 --- a/src/site/SiteChecklistClient.tsx +++ b/src/site/SiteChecklistClient.tsx @@ -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'; diff --git a/src/site/globals.css b/src/site/globals.css index b2002f91..48c6758f 100644 --- a/src/site/globals.css +++ b/src/site/globals.css @@ -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 + } }