From 2ed96eb2f43edfd8df0c2831ae3ebd3cd1dcb0f4 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 29 Jun 2024 22:19:27 -0500 Subject: [PATCH] Refactor core navigation to support grid-first root --- __tests__/path.test.ts | 12 ++- src/admin/AdminAddAllUploads.tsx | 4 +- src/admin/AdminTagTable.tsx | 4 +- src/admin/AdminUploadsClient.tsx | 4 +- src/app/feed/page.tsx | 41 ++++++++ src/app/film/[simulation]/page.tsx | 6 +- src/app/film/[simulation]/share/page.tsx | 4 +- src/app/focal/[focal]/page.tsx | 4 +- src/app/focal/[focal]/share/page.tsx | 4 +- src/app/grid/page.tsx | 23 ++--- src/app/og/all/page.tsx | 8 +- src/app/page.tsx | 35 ++++--- src/app/shot-on/[make]/[model]/page.tsx | 4 +- src/app/shot-on/[make]/[model]/share/page.tsx | 4 +- src/app/tag/[tag]/page.tsx | 4 +- src/app/tag/[tag]/share/page.tsx | 4 +- src/camera/CameraOverview.tsx | 4 +- src/components/cmdk/CommandKClient.tsx | 4 +- src/focal/FocalLengthOverview.tsx | 4 +- src/image-response/TemplateImageResponse.tsx | 4 +- src/photo/PhotoEditPageClient.tsx | 4 +- src/photo/PhotoFeedPage.tsx | 26 +++++ src/photo/PhotoGridContainer.tsx | 84 ++++++++++++++++ src/photo/PhotoGridInfinite.tsx | 4 +- src/photo/PhotoGridPage.tsx | 99 +++++-------------- src/photo/PhotoGridSidebar.tsx | 4 +- src/photo/UploadPageClient.tsx | 4 +- src/photo/cache.ts | 2 + src/photo/data.ts | 8 -- src/photo/db/query.ts | 6 +- src/photo/form/PhotoForm.tsx | 4 +- src/photo/index.ts | 12 +-- src/simulation/FilmSimulationOverview.tsx | 4 +- src/site/{IconFullFrame.tsx => IconFeed.tsx} | 2 +- src/site/Nav.tsx | 6 +- src/site/ViewSwitcher.tsx | 42 +++++--- src/site/paths.ts | 11 ++- src/tag/TagOverview.tsx | 4 +- src/tag/index.ts | 10 +- 39 files changed, 319 insertions(+), 198 deletions(-) create mode 100644 src/app/feed/page.tsx create mode 100644 src/photo/PhotoFeedPage.tsx create mode 100644 src/photo/PhotoGridContainer.tsx rename src/site/{IconFullFrame.tsx => IconFeed.tsx} (94%) diff --git a/__tests__/path.test.ts b/__tests__/path.test.ts index be19437a..7a9868b6 100644 --- a/__tests__/path.test.ts +++ b/__tests__/path.test.ts @@ -36,6 +36,7 @@ const SHARE = 'share'; const PATH_ROOT = '/'; const PATH_GRID = '/grid'; +const PATH_FEED = '/feed'; const PATH_ADMIN = '/admin/photos'; const PATH_OG = '/og'; const PATH_OG_ALL = `${PATH_OG}/all`; @@ -202,27 +203,28 @@ describe('Paths', () => { // Root expect(getEscapePath(PATH_ROOT)).toEqual(undefined); expect(getEscapePath(PATH_GRID)).toEqual(undefined); + expect(getEscapePath(PATH_FEED)).toEqual(undefined); expect(getEscapePath(PATH_ADMIN)).toEqual(undefined); // Photo - expect(getEscapePath(PATH_PHOTO)).toEqual(PATH_GRID); + expect(getEscapePath(PATH_PHOTO)).toEqual(PATH_ROOT); expect(getEscapePath(PATH_PHOTO_SHARE)).toEqual(PATH_PHOTO); // Tag - expect(getEscapePath(PATH_TAG)).toEqual(PATH_GRID); + expect(getEscapePath(PATH_TAG)).toEqual(PATH_ROOT); expect(getEscapePath(PATH_TAG_SHARE)).toEqual(PATH_TAG); expect(getEscapePath(PATH_TAG_PHOTO)).toEqual(PATH_TAG); expect(getEscapePath(PATH_TAG_PHOTO_SHARE)).toEqual(PATH_TAG_PHOTO); // Camera - expect(getEscapePath(PATH_CAMERA)).toEqual(PATH_GRID); + expect(getEscapePath(PATH_CAMERA)).toEqual(PATH_ROOT); expect(getEscapePath(PATH_CAMERA_SHARE)).toEqual(PATH_CAMERA); expect(getEscapePath(PATH_CAMERA_PHOTO)).toEqual(PATH_CAMERA); expect(getEscapePath(PATH_CAMERA_PHOTO_SHARE)).toEqual(PATH_CAMERA_PHOTO); // Film Simulation - expect(getEscapePath(PATH_FILM_SIMULATION)).toEqual(PATH_GRID); + expect(getEscapePath(PATH_FILM_SIMULATION)).toEqual(PATH_ROOT); expect(getEscapePath(PATH_FILM_SIMULATION_SHARE)).toEqual(PATH_FILM_SIMULATION); expect(getEscapePath(PATH_FILM_SIMULATION_PHOTO)).toEqual(PATH_FILM_SIMULATION); expect(getEscapePath(PATH_FILM_SIMULATION_PHOTO_SHARE)).toEqual(PATH_FILM_SIMULATION_PHOTO); // Focal Length - expect(getEscapePath(PATH_FOCAL_LENGTH)).toEqual(PATH_GRID); + expect(getEscapePath(PATH_FOCAL_LENGTH)).toEqual(PATH_ROOT); expect(getEscapePath(PATH_FOCAL_LENGTH_SHARE)).toEqual(PATH_FOCAL_LENGTH); expect(getEscapePath(PATH_FOCAL_LENGTH_PHOTO)).toEqual(PATH_FOCAL_LENGTH); expect(getEscapePath(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toEqual(PATH_FOCAL_LENGTH_PHOTO); diff --git a/src/admin/AdminAddAllUploads.tsx b/src/admin/AdminAddAllUploads.tsx index ac5dbd47..3810a115 100644 --- a/src/admin/AdminAddAllUploads.tsx +++ b/src/admin/AdminAddAllUploads.tsx @@ -7,7 +7,7 @@ import LoaderButton from '@/components/primitives/LoaderButton'; import { addAllUploadsAction } from '@/photo/actions'; import { PATH_ADMIN_PHOTOS } from '@/site/paths'; import { - TagsWithMeta, + Tags, convertTagsForForm, getValidationMessageForTags, } from '@/tag'; @@ -32,7 +32,7 @@ export default function AdminAddAllUploads({ setAddedUploadUrls, }: { storageUrls: string[] - uniqueTags?: TagsWithMeta + uniqueTags?: Tags isAdding: boolean setIsAdding: (isAdding: boolean) => void setAddedUploadUrls?: Dispatch> diff --git a/src/admin/AdminTagTable.tsx b/src/admin/AdminTagTable.tsx index 7628ff7e..e5eb7a1f 100644 --- a/src/admin/AdminTagTable.tsx +++ b/src/admin/AdminTagTable.tsx @@ -4,7 +4,7 @@ import AdminTable from '@/admin/AdminTable'; import { Fragment } from 'react'; import DeleteButton from '@/admin/DeleteButton'; import { photoQuantityText } from '@/photo'; -import { TagsWithMeta, formatTag, sortTagsObject } from '@/tag'; +import { Tags, formatTag, sortTagsObject } from '@/tag'; import EditButton from '@/admin/EditButton'; import { pathForAdminTagEdit } from '@/site/paths'; import { clsx } from 'clsx/lite'; @@ -13,7 +13,7 @@ import AdminTagBadge from './AdminTagBadge'; export default function AdminTagTable({ tags, }: { - tags: TagsWithMeta + tags: Tags }) { return ( diff --git a/src/admin/AdminUploadsClient.tsx b/src/admin/AdminUploadsClient.tsx index 8ac65884..f93b2cac 100644 --- a/src/admin/AdminUploadsClient.tsx +++ b/src/admin/AdminUploadsClient.tsx @@ -4,7 +4,7 @@ import { StorageListResponse } from '@/services/storage'; import AdminAddAllUploads from './AdminAddAllUploads'; import AdminUploadsTable from './AdminUploadsTable'; import { useState } from 'react'; -import { TagsWithMeta } from '@/tag'; +import { Tags } from '@/tag'; export default function AdminUploadsClient({ title, @@ -13,7 +13,7 @@ export default function AdminUploadsClient({ }: { title?: string urls: StorageListResponse - uniqueTags?: TagsWithMeta + uniqueTags?: Tags }) { const [isAdding, setIsAdding] = useState(false); const [addedUploadUrls, setAddedUploadUrls] = useState([]); diff --git a/src/app/feed/page.tsx b/src/app/feed/page.tsx new file mode 100644 index 00000000..98e6724d --- /dev/null +++ b/src/app/feed/page.tsx @@ -0,0 +1,41 @@ +import { + INFINITE_SCROLL_FEED_INITIAL, + generateOgImageMetaForPhotos, +} from '@/photo'; +import PhotosEmptyState from '@/photo/PhotosEmptyState'; +import { Metadata } from 'next/types'; +import { cache } from 'react'; +import { getPhotos, getPhotosMeta } from '@/photo/db/query'; +import PhotoFeedPage from '@/photo/PhotoFeedPage'; + +export const dynamic = 'force-static'; +export const maxDuration = 60; + +const getPhotosCached = cache(() => getPhotos({ + limit: INFINITE_SCROLL_FEED_INITIAL, +})); + +export async function generateMetadata(): Promise { + const photos = await getPhotosCached() + .catch(() => []); + return generateOgImageMetaForPhotos(photos); +} + +export default async function FeedPage() { + const [ + photos, + photosCount, + ] = await Promise.all([ + getPhotosCached() + .catch(() => []), + getPhotosMeta() + .then(({ count }) => count) + .catch(() => 0), + ]); + + return ( + photos.length > 0 + ? + : + ); +} diff --git a/src/app/film/[simulation]/page.tsx b/src/app/film/[simulation]/page.tsx index d770a01a..a9814a6e 100644 --- a/src/app/film/[simulation]/page.tsx +++ b/src/app/film/[simulation]/page.tsx @@ -1,4 +1,4 @@ -import { INFINITE_SCROLL_GRID_PHOTO_INITIAL } from '@/photo'; +import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo'; import { FilmSimulation, generateMetaForFilmSimulation } from '@/simulation'; import FilmSimulationOverview from '@/simulation/FilmSimulationOverview'; import { getPhotosFilmSimulationDataCached } from '@/simulation/data'; @@ -20,7 +20,7 @@ export async function generateMetadata({ { count, dateRange }, ] = await getPhotosFilmSimulationDataCachedCached({ simulation, - limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, + limit: INFINITE_SCROLL_GRID_INITIAL, }); const { @@ -55,7 +55,7 @@ export default async function FilmSimulationPage({ { count, dateRange }, ] = await getPhotosFilmSimulationDataCachedCached({ simulation, - limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, + limit: INFINITE_SCROLL_GRID_INITIAL, }); return ( diff --git a/src/app/film/[simulation]/share/page.tsx b/src/app/film/[simulation]/share/page.tsx index 5f72b3c4..8af9a08c 100644 --- a/src/app/film/[simulation]/share/page.tsx +++ b/src/app/film/[simulation]/share/page.tsx @@ -1,4 +1,4 @@ -import { INFINITE_SCROLL_GRID_PHOTO_INITIAL } from '@/photo'; +import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo'; import { FilmSimulation, generateMetaForFilmSimulation } from '@/simulation'; import FilmSimulationOverview from '@/simulation/FilmSimulationOverview'; import FilmSimulationShareModal from '@/simulation/FilmSimulationShareModal'; @@ -9,7 +9,7 @@ import { cache } from 'react'; const getPhotosFilmSimulationDataCachedCached = cache((simulation: FilmSimulation) => getPhotosFilmSimulationDataCached({ simulation, - limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, + limit: INFINITE_SCROLL_GRID_INITIAL, })); interface FilmSimulationProps { diff --git a/src/app/focal/[focal]/page.tsx b/src/app/focal/[focal]/page.tsx index 82948194..4cd09c83 100644 --- a/src/app/focal/[focal]/page.tsx +++ b/src/app/focal/[focal]/page.tsx @@ -1,7 +1,7 @@ import { generateMetaForFocalLength, getFocalLengthFromString } from '@/focal'; import FocalLengthOverview from '@/focal/FocalLengthOverview'; import { getPhotosFocalLengthDataCached } from '@/focal/data'; -import { INFINITE_SCROLL_GRID_PHOTO_INITIAL } from '@/photo'; +import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo'; import { PATH_ROOT } from '@/site/paths'; import type { Metadata } from 'next'; import { redirect } from 'next/navigation'; @@ -10,7 +10,7 @@ import { cache } from 'react'; const getPhotosFocalDataCachedCached = cache((focal: number) => getPhotosFocalLengthDataCached({ focal, - limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, + limit: INFINITE_SCROLL_GRID_INITIAL, })); interface FocalLengthProps { diff --git a/src/app/focal/[focal]/share/page.tsx b/src/app/focal/[focal]/share/page.tsx index 01d7aead..10633b3e 100644 --- a/src/app/focal/[focal]/share/page.tsx +++ b/src/app/focal/[focal]/share/page.tsx @@ -2,14 +2,14 @@ import { generateMetaForFocalLength, getFocalLengthFromString } from '@/focal'; import FocalLengthOverview from '@/focal/FocalLengthOverview'; import FocalLengthShareModal from '@/focal/FocalLengthShareModal'; import { getPhotosFocalLengthDataCached } from '@/focal/data'; -import { INFINITE_SCROLL_GRID_PHOTO_INITIAL } from '@/photo'; +import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo'; import type { Metadata } from 'next'; import { cache } from 'react'; const getPhotosFocalLengthDataCachedCached = cache((focal: number) => getPhotosFocalLengthDataCached({ focal, - limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, + limit: INFINITE_SCROLL_GRID_INITIAL, })); interface FocalLengthProps { diff --git a/src/app/grid/page.tsx b/src/app/grid/page.tsx index 0b0cb4ab..4c732f58 100644 --- a/src/app/grid/page.tsx +++ b/src/app/grid/page.tsx @@ -1,20 +1,18 @@ import { - INFINITE_SCROLL_GRID_PHOTO_INITIAL, + INFINITE_SCROLL_GRID_INITIAL, generateOgImageMetaForPhotos, } from '@/photo'; import PhotosEmptyState from '@/photo/PhotosEmptyState'; import { Metadata } from 'next/types'; -import PhotoGridSidebar from '@/photo/PhotoGridSidebar'; import { getPhotoSidebarData } from '@/photo/data'; -import { getPhotos } from '@/photo/db/query'; +import { getPhotos, getPhotosMeta } from '@/photo/db/query'; import { cache } from 'react'; import PhotoGridPage from '@/photo/PhotoGridPage'; -import { PATH_GRID } from '@/site/paths'; export const dynamic = 'force-static'; const getPhotosCached = cache(() => getPhotos({ - limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, + limit: INFINITE_SCROLL_GRID_INITIAL, })); export async function generateMetadata(): Promise { @@ -33,23 +31,16 @@ export default async function GridPage() { ] = await Promise.all([ getPhotosCached() .catch(() => []), + getPhotosMeta() + .then(({ count }) => count) + .catch(() => 0), ...getPhotoSidebarData(), ]); return ( photos.length > 0 ? - - } + {...{ photos, photosCount, tags, cameras, simulations }} /> : ); diff --git a/src/app/og/all/page.tsx b/src/app/og/all/page.tsx index 56306e24..55791c14 100644 --- a/src/app/og/all/page.tsx +++ b/src/app/og/all/page.tsx @@ -1,6 +1,6 @@ import { - INFINITE_SCROLL_GRID_PHOTO_INITIAL, - INFINITE_SCROLL_GRID_PHOTO_MULTIPLE, + INFINITE_SCROLL_GRID_INITIAL, + INFINITE_SCROLL_GRID_MULTIPLE, } from '@/photo'; import { getPhotosCached } from '@/photo/cache'; import { getPhotosMeta } from '@/photo/db/query'; @@ -12,7 +12,7 @@ export default async function OGPage() { photos, count, ] = await Promise.all([ - getPhotosCached({ limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL }) + getPhotosCached({ limit: INFINITE_SCROLL_GRID_INITIAL }) .catch(() => []), getPhotosMeta() .then(({ count }) => count) @@ -26,7 +26,7 @@ export default async function OGPage() {
} diff --git a/src/app/page.tsx b/src/app/page.tsx index de3bde77..9a3b83e1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,20 +1,24 @@ import { - INFINITE_SCROLL_LARGE_PHOTO_INITIAL, - INFINITE_SCROLL_LARGE_PHOTO_MULTIPLE, + INFINITE_SCROLL_FEED_INITIAL, + INFINITE_SCROLL_GRID_INITIAL, generateOgImageMetaForPhotos, } from '@/photo'; import PhotosEmptyState from '@/photo/PhotosEmptyState'; import { Metadata } from 'next/types'; -import PhotosLarge from '@/photo/PhotosLarge'; import { cache } from 'react'; import { getPhotos, getPhotosMeta } from '@/photo/db/query'; -import PhotosLargeInfinite from '@/photo/PhotosLargeInfinite'; +import { SHOW_GRID_FIRST } from '@/site/config'; +import { getPhotoSidebarData } from '@/photo/data'; +import PhotoGridPage from '@/photo/PhotoGridPage'; +import PhotoFeedPage from '@/photo/PhotoFeedPage'; export const dynamic = 'force-static'; export const maxDuration = 60; const getPhotosCached = cache(() => getPhotos({ - limit: INFINITE_SCROLL_LARGE_PHOTO_INITIAL, + limit: SHOW_GRID_FIRST + ? INFINITE_SCROLL_GRID_INITIAL + : INFINITE_SCROLL_FEED_INITIAL, })); export async function generateMetadata(): Promise { @@ -27,24 +31,29 @@ export default async function HomePage() { const [ photos, photosCount, + tags, + cameras, + simulations, ] = await Promise.all([ getPhotosCached() .catch(() => []), getPhotosMeta() .then(({ count }) => count) .catch(() => 0), + ...(SHOW_GRID_FIRST + ? getPhotoSidebarData() + : [[], [], []]), ]); return ( photos.length > 0 - ?
- - {photosCount > photos.length && - } -
+ ? SHOW_GRID_FIRST + ? + : : ); } diff --git a/src/app/shot-on/[make]/[model]/page.tsx b/src/app/shot-on/[make]/[model]/page.tsx index ee065416..ca611e1b 100644 --- a/src/app/shot-on/[make]/[model]/page.tsx +++ b/src/app/shot-on/[make]/[model]/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from 'next/types'; import { CameraProps } from '@/camera'; import { generateMetaForCamera } from '@/camera/meta'; -import { INFINITE_SCROLL_GRID_PHOTO_INITIAL } from '@/photo'; +import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo'; import { getPhotosCameraDataCached } from '@/camera/data'; import CameraOverview from '@/camera/CameraOverview'; import { cache } from 'react'; @@ -12,7 +12,7 @@ const getPhotosCameraDataCachedCached = cache(( ) => getPhotosCameraDataCached( make, model, - INFINITE_SCROLL_GRID_PHOTO_INITIAL, + INFINITE_SCROLL_GRID_INITIAL, )); export async function generateMetadata({ diff --git a/src/app/shot-on/[make]/[model]/share/page.tsx b/src/app/shot-on/[make]/[model]/share/page.tsx index 37c52345..a2dfe450 100644 --- a/src/app/shot-on/[make]/[model]/share/page.tsx +++ b/src/app/shot-on/[make]/[model]/share/page.tsx @@ -2,7 +2,7 @@ import { CameraProps } from '@/camera'; import CameraShareModal from '@/camera/CameraShareModal'; import { generateMetaForCamera } from '@/camera/meta'; import { Metadata } from 'next/types'; -import { INFINITE_SCROLL_GRID_PHOTO_INITIAL } from '@/photo'; +import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo'; import { getPhotosCameraDataCached } from '@/camera/data'; import CameraOverview from '@/camera/CameraOverview'; import { cache } from 'react'; @@ -13,7 +13,7 @@ const getPhotosCameraDataCachedCached = cache(( ) => getPhotosCameraDataCached( make, model, - INFINITE_SCROLL_GRID_PHOTO_INITIAL, + INFINITE_SCROLL_GRID_INITIAL, )); export async function generateMetadata({ diff --git a/src/app/tag/[tag]/page.tsx b/src/app/tag/[tag]/page.tsx index 39f1de9f..8411a07d 100644 --- a/src/app/tag/[tag]/page.tsx +++ b/src/app/tag/[tag]/page.tsx @@ -1,4 +1,4 @@ -import { INFINITE_SCROLL_GRID_PHOTO_INITIAL } from '@/photo'; +import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo'; import { PATH_ROOT } from '@/site/paths'; import { generateMetaForTag } from '@/tag'; import TagOverview from '@/tag/TagOverview'; @@ -8,7 +8,7 @@ import { redirect } from 'next/navigation'; import { cache } from 'react'; const getPhotosTagDataCachedCached = cache((tag: string) => - getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL})); + getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_INITIAL})); interface TagProps { params: { tag: string } diff --git a/src/app/tag/[tag]/share/page.tsx b/src/app/tag/[tag]/share/page.tsx index 3cacbb43..7ed8d9b2 100644 --- a/src/app/tag/[tag]/share/page.tsx +++ b/src/app/tag/[tag]/share/page.tsx @@ -1,4 +1,4 @@ -import { INFINITE_SCROLL_GRID_PHOTO_INITIAL } from '@/photo'; +import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo'; import { generateMetaForTag } from '@/tag'; import TagOverview from '@/tag/TagOverview'; import TagShareModal from '@/tag/TagShareModal'; @@ -7,7 +7,7 @@ import type { Metadata } from 'next'; import { cache } from 'react'; const getPhotosTagDataCachedCached = cache((tag: string) => - getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL })); + getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_INITIAL })); interface TagProps { params: { tag: string } diff --git a/src/camera/CameraOverview.tsx b/src/camera/CameraOverview.tsx index 7a094871..2293996a 100644 --- a/src/camera/CameraOverview.tsx +++ b/src/camera/CameraOverview.tsx @@ -1,7 +1,7 @@ import { Photo, PhotoDateRange } from '@/photo'; import { Camera, createCameraKey } from '.'; import CameraHeader from './CameraHeader'; -import PhotoGridPage from '@/photo/PhotoGridPage'; +import PhotoGridContainer from '@/photo/PhotoGridContainer'; export default function CameraOverview({ camera, @@ -17,7 +17,7 @@ export default function CameraOverview({ animateOnFirstLoadOnly?: boolean, }) { return ( - - +
+ + {photosCount > photos.length && + } +
+ ); +} diff --git a/src/photo/PhotoGridContainer.tsx b/src/photo/PhotoGridContainer.tsx new file mode 100644 index 00000000..97c608b1 --- /dev/null +++ b/src/photo/PhotoGridContainer.tsx @@ -0,0 +1,84 @@ +'use client'; + +import SiteGrid from '@/components/SiteGrid'; +import { Photo } from '.'; +import PhotoGrid from './PhotoGrid'; +import PhotoGridInfinite from './PhotoGridInfinite'; +import { Camera } from '@/camera'; +import { clsx } from 'clsx/lite'; +import AnimateItems from '@/components/AnimateItems'; +import { FilmSimulation } from '@/simulation'; +import { useCallback, useState } from 'react'; + +export default function PhotoGridContainer({ + cacheKey, + photos, + count, + tag, + camera, + simulation, + focal, + animateOnFirstLoadOnly, + header, + sidebar, +}: { + cacheKey: string + photos: Photo[] + count: number + tag?: string + camera?: Camera + simulation?: FilmSimulation + focal?: number + animateOnFirstLoadOnly?: boolean + header?: JSX.Element + sidebar?: JSX.Element +}) { + const [ + shouldAnimateDynamicItems, + setShouldAnimateDynamicItems, + ] = useState(false); + + const onAnimationComplete = useCallback(() => + setShouldAnimateDynamicItems(true), []); + + const initialOffset = photos.length; + + return ( + + {header && + } +
+ + {count > initialOffset && + } +
+ } + contentSide={sidebar} + sideHiddenOnMobile + /> + ); +} diff --git a/src/photo/PhotoGridInfinite.tsx b/src/photo/PhotoGridInfinite.tsx index 0b9a1428..b9d3eae0 100644 --- a/src/photo/PhotoGridInfinite.tsx +++ b/src/photo/PhotoGridInfinite.tsx @@ -1,7 +1,7 @@ 'use client'; import { Camera } from '@/camera'; -import { INFINITE_SCROLL_GRID_PHOTO_MULTIPLE } from '.'; +import { INFINITE_SCROLL_GRID_MULTIPLE } from '.'; import InfinitePhotoScroll from './InfinitePhotoScroll'; import PhotoGrid from './PhotoGrid'; import { FilmSimulation } from '@/simulation'; @@ -29,7 +29,7 @@ export default function PhotoGridInfinite({ - setShouldAnimateDynamicItems(true), []); - - const initialOffset = photos.length; - return ( - - {header && - } -
- - {count > initialOffset && - } -
+ + } - contentSide={sidebar} - sideHiddenOnMobile /> ); } diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index 0199ff63..018ca15b 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -7,7 +7,7 @@ import PhotoTag from '@/tag/PhotoTag'; import { FaTag } from 'react-icons/fa'; import { IoMdCamera } from 'react-icons/io'; import { PhotoDateRange, dateRangeForPhotos, photoQuantityText } from '.'; -import { TAG_FAVS, TAG_HIDDEN, TagsWithMeta, addHiddenToTags } from '@/tag'; +import { TAG_FAVS, TAG_HIDDEN, Tags, addHiddenToTags } from '@/tag'; import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation'; import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon'; import { FilmSimulations, sortFilmSimulationsWithCount } from '@/simulation'; @@ -23,7 +23,7 @@ export default function PhotoGridSidebar({ photosCount, photosDateRange, }: { - tags: TagsWithMeta + tags: Tags cameras: Cameras simulations: FilmSimulations photosCount: number diff --git a/src/photo/UploadPageClient.tsx b/src/photo/UploadPageClient.tsx index ad579dd7..27e612d8 100644 --- a/src/photo/UploadPageClient.tsx +++ b/src/photo/UploadPageClient.tsx @@ -4,7 +4,7 @@ import AdminChildPage from '@/components/AdminChildPage'; import { PATH_ADMIN_UPLOADS } from '@/site/paths'; import { PhotoFormData, generateTakenAtFields } from './form'; import PhotoForm from './form/PhotoForm'; -import { TagsWithMeta } from '@/tag'; +import { Tags } from '@/tag'; import usePhotoFormParent from './form/usePhotoFormParent'; import AiButton from './ai/AiButton'; import { AiAutoGeneratedField } from './ai'; @@ -21,7 +21,7 @@ export default function UploadPageClient({ }: { blobId?: string photoFormExif: Partial - uniqueTags: TagsWithMeta + uniqueTags: Tags hasAiTextGeneration?: boolean textFieldsToAutoGenerate?: AiAutoGeneratedField[], imageThumbnailBase64?: string diff --git a/src/photo/cache.ts b/src/photo/cache.ts index fc21653f..714ccb51 100644 --- a/src/photo/cache.ts +++ b/src/photo/cache.ts @@ -24,6 +24,7 @@ import { PATHS_ADMIN, PATHS_TO_CACHE, PATH_ADMIN, + PATH_FEED, PATH_GRID, PATH_ROOT, PREFIX_CAMERA, @@ -126,6 +127,7 @@ export const revalidatePhoto = (photoId: string) => { revalidatePath(pathForPhoto({ photo: photoId }), 'layout'); revalidatePath(PATH_ROOT, 'layout'); revalidatePath(PATH_GRID, 'layout'); + revalidatePath(PATH_FEED, 'layout'); revalidatePath(PREFIX_TAG, 'layout'); revalidatePath(PREFIX_CAMERA, 'layout'); revalidatePath(PREFIX_FILM_SIMULATION, 'layout'); diff --git a/src/photo/data.ts b/src/photo/data.ts index 3c008ef9..fa674b4a 100644 --- a/src/photo/data.ts +++ b/src/photo/data.ts @@ -1,11 +1,9 @@ import { - getPhotosMetaCached, getUniqueCamerasCached, getUniqueFilmSimulationsCached, getUniqueTagsCached, } from '@/photo/cache'; import { - getPhotosMeta, getUniqueCameras, getUniqueFilmSimulations, getUniqueTags, @@ -14,9 +12,6 @@ import { SHOW_FILM_SIMULATIONS } from '@/site/config'; import { sortTagsObject } from '@/tag'; export const getPhotoSidebarData = () => [ - getPhotosMeta() - .then(({ count }) => count) - .catch(() => 0), getUniqueTags().then(sortTagsObject).catch(() => []), getUniqueCameras().catch(() => []), SHOW_FILM_SIMULATIONS @@ -25,9 +20,6 @@ export const getPhotoSidebarData = () => [ ] as const; export const getPhotoSidebarDataCached = () => [ - getPhotosMetaCached() - .then(({ count }) => count) - .catch(() => 0), getUniqueTagsCached().then(sortTagsObject), getUniqueCamerasCached(), SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [], diff --git a/src/photo/db/query.ts b/src/photo/db/query.ts index c2150c80..1c171580 100644 --- a/src/photo/db/query.ts +++ b/src/photo/db/query.ts @@ -12,7 +12,7 @@ import { PhotoDateRange, } from '@/photo'; import { Cameras, createCameraKey } from '@/camera'; -import { TagsWithMeta } from '@/tag'; +import { Tags } from '@/tag'; import { FilmSimulation, FilmSimulations } from '@/simulation'; import { SHOULD_DEBUG_SQL } from '@/site/config'; import { @@ -261,7 +261,7 @@ export const getUniqueTags = async () => WHERE hidden IS NOT TRUE GROUP BY tag ORDER BY tag ASC - `.then(({ rows }): TagsWithMeta => rows.map(({ tag, count }) => ({ + `.then(({ rows }): Tags => rows.map(({ tag, count }) => ({ tag: tag as string, count: parseInt(count, 10), }))) @@ -273,7 +273,7 @@ export const getUniqueTagsHidden = async () => FROM photos GROUP BY tag ORDER BY tag ASC - `.then(({ rows }): TagsWithMeta => rows.map(({ tag, count }) => ({ + `.then(({ rows }): Tags => rows.map(({ tag, count }) => ({ tag: tag as string, count: parseInt(count, 10), }))) diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index 26333b01..139fb388 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -19,7 +19,7 @@ import { PATH_ADMIN_PHOTOS, PATH_ADMIN_UPLOADS } from '@/site/paths'; import { toastSuccess, toastWarning } from '@/toast'; import { getDimensionsFromSize } from '@/utility/size'; import ImageWithFallback from '@/components/image/ImageWithFallback'; -import { TagsWithMeta, convertTagsForForm } from '@/tag'; +import { Tags, convertTagsForForm } from '@/tag'; import { AiContent } from '../ai/useAiImageQueries'; import AiButton from '../ai/AiButton'; import Spinner from '@/components/Spinner'; @@ -49,7 +49,7 @@ export default function PhotoForm({ initialPhotoForm: Partial updatedExifData?: Partial updatedBlurData?: string - uniqueTags?: TagsWithMeta + uniqueTags?: Tags aiContent?: AiContent shouldStripGpsData?: boolean onTitleChange?: (updatedTitle: string) => void diff --git a/src/photo/index.ts b/src/photo/index.ts index 5c0ef7fe..2d861363 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -16,17 +16,17 @@ import type { Metadata } from 'next'; export const OUTDATED_THRESHOLD = new Date('2024-06-16'); -// INFINITE SCROLL: LARGE PHOTOS -export const INFINITE_SCROLL_LARGE_PHOTO_INITIAL = +// INFINITE SCROLL: FEED +export const INFINITE_SCROLL_FEED_INITIAL = process.env.NODE_ENV === 'development' ? 2 : 12; -export const INFINITE_SCROLL_LARGE_PHOTO_MULTIPLE = +export const INFINITE_SCROLL_FEED_MULTIPLE = process.env.NODE_ENV === 'development' ? 2 : 24; -// INFINITE SCROLL: GRID PHOTOS -export const INFINITE_SCROLL_GRID_PHOTO_INITIAL = HIGH_DENSITY_GRID +// INFINITE SCROLL: GRID +export const INFINITE_SCROLL_GRID_INITIAL = HIGH_DENSITY_GRID ? process.env.NODE_ENV === 'development' ? 12 : 24 : process.env.NODE_ENV === 'development' ? 12 : 24; -export const INFINITE_SCROLL_GRID_PHOTO_MULTIPLE = HIGH_DENSITY_GRID +export const INFINITE_SCROLL_GRID_MULTIPLE = HIGH_DENSITY_GRID ? process.env.NODE_ENV === 'development' ? 12 : 48 : process.env.NODE_ENV === 'development' ? 12 : 48; diff --git a/src/simulation/FilmSimulationOverview.tsx b/src/simulation/FilmSimulationOverview.tsx index 37fa71fe..dfac4fae 100644 --- a/src/simulation/FilmSimulationOverview.tsx +++ b/src/simulation/FilmSimulationOverview.tsx @@ -1,7 +1,7 @@ import { Photo, PhotoDateRange } from '@/photo'; import FilmSimulationHeader from './FilmSimulationHeader'; import { FilmSimulation } from '.'; -import PhotoGridPage from '@/photo/PhotoGridPage'; +import PhotoGridContainer from '@/photo/PhotoGridContainer'; export default function FilmSimulationOverview({ simulation, @@ -17,7 +17,7 @@ export default function FilmSimulationOverview({ animateOnFirstLoadOnly?: boolean, }) { return ( - { if (pathname === PATH_ROOT) { - return 'full-frame'; + return SHOW_GRID_FIRST ? 'grid' : 'feed'; } else if (isPathGrid(pathname)) { return 'grid'; + } else if (isPathFeed(pathname)) { + return 'feed'; } else if (isPathProtected(pathname)) { return 'admin'; } diff --git a/src/site/ViewSwitcher.tsx b/src/site/ViewSwitcher.tsx index 6d4b2566..bd0352fe 100644 --- a/src/site/ViewSwitcher.tsx +++ b/src/site/ViewSwitcher.tsx @@ -1,13 +1,19 @@ import Switcher from '@/components/Switcher'; import SwitcherItem from '@/components/SwitcherItem'; -import IconFullFrame from '@/site/IconFullFrame'; +import IconFeed from '@/site/IconFeed'; import IconGrid from '@/site/IconGrid'; -import { PATH_ADMIN_PHOTOS, PATH_GRID } from '@/site/paths'; +import { + PATH_ADMIN_PHOTOS, + PATH_FEED, + PATH_GRID, + PATH_ROOT, +} from '@/site/paths'; import { BiLockAlt } from 'react-icons/bi'; import IconSearch from './IconSearch'; import { useAppState } from '@/state/AppState'; +import { SHOW_GRID_FIRST } from './config'; -export type SwitcherSelection = 'full-frame' | 'grid' | 'sets' | 'admin'; +export type SwitcherSelection = 'feed' | 'grid' | 'admin'; export default function ViewSwitcher({ currentSelection, @@ -18,21 +24,27 @@ export default function ViewSwitcher({ }) { const { setIsCommandKOpen } = useAppState(); + const renderItemFeed = () => + } + href={SHOW_GRID_FIRST ? PATH_FEED : PATH_ROOT} + active={currentSelection === 'feed'} + noPadding + />; + + const renderItemGrid = () => + } + href={SHOW_GRID_FIRST ? PATH_ROOT : PATH_GRID} + active={currentSelection === 'grid'} + noPadding + />; + return (
- } - href="/" - active={currentSelection === 'full-frame'} - noPadding - /> - } - href={PATH_GRID} - active={currentSelection === 'grid'} - noPadding - /> + {SHOW_GRID_FIRST ? renderItemGrid() : renderItemFeed()} + {SHOW_GRID_FIRST ? renderItemFeed() : renderItemGrid()} {showAdmin && } diff --git a/src/site/paths.ts b/src/site/paths.ts index 47d82f27..53053912 100644 --- a/src/site/paths.ts +++ b/src/site/paths.ts @@ -8,6 +8,7 @@ import { TAG_HIDDEN } from '@/tag'; // Core paths export const PATH_ROOT = '/'; export const PATH_GRID = '/grid'; +export const PATH_FEED = '/feed'; export const PATH_ADMIN = '/admin'; export const PATH_API = '/api'; export const PATH_SIGN_IN = '/sign-in'; @@ -37,8 +38,8 @@ export const PATH_ADMIN_CONFIGURATION = `${PATH_ADMIN}/configuration`; export const PATH_ADMIN_BASELINE = `${PATH_ADMIN}/baseline`; // Debug paths -export const PATH_OG_ALL = `${PATH_OG}/all`; -export const PATH_OG_SAMPLE = `${PATH_OG}/sample`; +export const PATH_OG_ALL = `${PATH_OG}/all`; +export const PATH_OG_SAMPLE = `${PATH_OG}/sample`; // API paths export const PATH_API_STORAGE = `${PATH_API}/storage`; @@ -60,6 +61,7 @@ export const PATHS_ADMIN = [ export const PATHS_TO_CACHE = [ PATH_ROOT, PATH_GRID, + PATH_FEED, PATH_OG, PATH_PHOTO_DYNAMIC, PATH_TAG_DYNAMIC, @@ -252,6 +254,9 @@ export const checkPathPrefix = (pathname = '', prefix: string) => export const isPathGrid = (pathname?: string) => checkPathPrefix(pathname, PATH_GRID); +export const isPathFeed = (pathname?: string) => + checkPathPrefix(pathname, PATH_FEED); + export const isPathSignIn = (pathname?: string) => checkPathPrefix(pathname, PATH_SIGN_IN); @@ -334,7 +339,7 @@ export const getEscapePath = (pathname?: string) => { (simulation && isPathFilmSimulation(pathname)) || (focal && isPathFocalLength(pathname)) ) { - return PATH_GRID; + return PATH_ROOT; } else if (photoId && isPathTagPhotoShare(pathname)) { return pathForPhoto({ photo: photoId, tag }); } else if (photoId && isPathCameraPhotoShare(pathname)) { diff --git a/src/tag/TagOverview.tsx b/src/tag/TagOverview.tsx index 044f0cd1..dbe5bbcf 100644 --- a/src/tag/TagOverview.tsx +++ b/src/tag/TagOverview.tsx @@ -1,6 +1,6 @@ import { Photo, PhotoDateRange } from '@/photo'; import TagHeader from './TagHeader'; -import PhotoGridPage from '@/photo/PhotoGridPage'; +import PhotoGridContainer from '@/photo/PhotoGridContainer'; export default function TagOverview({ tag, @@ -16,7 +16,7 @@ export default function TagOverview({ animateOnFirstLoadOnly?: boolean, }) { return ( - isTagFavs(a) ? -1 : a.localeCompare(b)); export const sortTagsObject = ( - tags: TagsWithMeta, + tags: Tags, tagToHide?: string, ) => tags .filter(({ tag }) => tag!== tagToHide) @@ -66,7 +66,7 @@ export const sortTagsObject = ( export const sortTagsWithoutFavs = (tags: string[]) => sortTags(tags, TAG_FAVS); -export const sortTagsObjectWithoutFavs = (tags: TagsWithMeta) => +export const sortTagsObjectWithoutFavs = (tags: Tags) => sortTagsObject(tags, TAG_FAVS); export const descriptionForTaggedPhotos = ( @@ -105,7 +105,7 @@ export const isPathFavs = (pathname?: string) => export const isTagHidden = (tag: string) => tag.toLowerCase() === TAG_HIDDEN; -export const addHiddenToTags = (tags: TagsWithMeta, hiddenPhotosCount = 0) => { +export const addHiddenToTags = (tags: Tags, hiddenPhotosCount = 0) => { if (hiddenPhotosCount > 0) { return tags .filter(({ tag }) => tag === TAG_FAVS) @@ -116,7 +116,7 @@ export const addHiddenToTags = (tags: TagsWithMeta, hiddenPhotosCount = 0) => { } }; -export const convertTagsForForm = (tags: TagsWithMeta = []) => +export const convertTagsForForm = (tags: Tags = []) => sortTagsObjectWithoutFavs(tags) .map(({ tag, count }) => ({ value: tag,