Refactor core navigation to support grid-first root

This commit is contained in:
Sam Becker 2024-06-29 22:19:27 -05:00
parent 6ff4a72c20
commit 2ed96eb2f4
39 changed files with 319 additions and 198 deletions

View File

@ -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);

View File

@ -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<SetStateAction<string[]>>

View File

@ -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 (
<AdminTable>

View File

@ -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<string[]>([]);

41
src/app/feed/page.tsx Normal file
View File

@ -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<Metadata> {
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
? <PhotoFeedPage {...{ photos, photosCount }} />
: <PhotosEmptyState />
);
}

View File

@ -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 (

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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<Metadata> {
@ -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
? <PhotoGridPage
cacheKey={`page-${PATH_GRID}`}
photos={photos}
count={photosCount}
sidebar={<div className="sticky top-4 space-y-4 mt-[-4px]">
<PhotoGridSidebar {...{
tags,
cameras,
simulations,
photosCount,
}} />
</div>}
{...{ photos, photosCount, tags, cameras, simulations }}
/>
: <PhotosEmptyState />
);

View File

@ -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() {
<div className="mt-3">
<StaggeredOgPhotosInfinite
initialOffset={photos.length}
itemsPerPage={INFINITE_SCROLL_GRID_PHOTO_MULTIPLE}
itemsPerPage={INFINITE_SCROLL_GRID_MULTIPLE}
/>
</div>}
</>

View File

@ -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<Metadata> {
@ -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
? <div className="space-y-1">
<PhotosLarge {...{ photos }} />
{photosCount > photos.length &&
<PhotosLargeInfinite
initialOffset={INFINITE_SCROLL_LARGE_PHOTO_INITIAL}
itemsPerPage={INFINITE_SCROLL_LARGE_PHOTO_MULTIPLE}
/>}
</div>
? SHOW_GRID_FIRST
? <PhotoGridPage
{...{ photos, photosCount, tags, cameras, simulations }}
/>
: <PhotoFeedPage
{...{ photos, photosCount }}
/>
: <PhotosEmptyState />
);
}

View File

@ -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({

View File

@ -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({

View File

@ -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 }

View File

@ -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 }

View File

@ -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 (
<PhotoGridPage {...{
<PhotoGridContainer {...{
cacheKey: `camera-${createCameraKey(camera)}`,
photos,
count,

View File

@ -38,7 +38,7 @@ import { getKeywordsForPhoto, titleForPhoto } from '@/photo';
import PhotoDate from '@/photo/PhotoDate';
import PhotoSmall from '@/photo/PhotoSmall';
import { FaCheck } from 'react-icons/fa6';
import { TagsWithMeta, addHiddenToTags, formatTag } from '@/tag';
import { Tags, addHiddenToTags, formatTag } from '@/tag';
import { FaTag } from 'react-icons/fa';
import { formatCount, formatCountDescriptive } from '@/utility/string';
import CommandKItem from './CommandKItem';
@ -68,7 +68,7 @@ export default function CommandKClient({
showDebugTools,
footer,
}: {
tags: TagsWithMeta
tags: Tags
serverSections?: CommandKSection[]
showDebugTools?: boolean
footer?: string

View File

@ -1,5 +1,5 @@
import { Photo, PhotoDateRange } from '@/photo';
import PhotoGridPage from '@/photo/PhotoGridPage';
import PhotoGridContainer from '@/photo/PhotoGridContainer';
import FocalLengthHeader from './FocalLengthHeader';
export default function FocalLengthOverview({
@ -16,7 +16,7 @@ export default function FocalLengthOverview({
animateOnFirstLoadOnly?: boolean,
}) {
return (
<PhotoGridPage {...{
<PhotoGridContainer {...{
cacheKey: `focal-${focal}`,
photos,
count,

View File

@ -1,5 +1,5 @@
import { Photo } from '../photo';
import IconFullFrame from '@/site/IconFullFrame';
import IconFeed from '@/site/IconFeed';
import IconGrid from '@/site/IconGrid';
import ImagePhotoGrid from './components/ImagePhotoGrid';
import { NextImageSize } from '@/services/next-image';
@ -66,7 +66,7 @@ export default function TemplateImageResponse({
color: '#333',
borderRight: '2px solid #333',
}}>
<IconFullFrame includeTitle={false} width={80} />
<IconFeed includeTitle={false} width={80} />
</div>
<div style={{
display: 'flex',

View File

@ -11,7 +11,7 @@ import PhotoForm from './form/PhotoForm';
import { useFormState } from 'react-dom';
import { areSimpleObjectsEqual } from '@/utility/object';
import { getExifDataAction } from './actions';
import { TagsWithMeta } from '@/tag';
import { Tags } from '@/tag';
import AiButton from './ai/AiButton';
import usePhotoFormParent from './form/usePhotoFormParent';
import ExifSyncButton from '@/admin/ExifSyncButton';
@ -24,7 +24,7 @@ export default function PhotoEditPageClient({
blurData,
}: {
photo: Photo
uniqueTags: TagsWithMeta
uniqueTags: Tags
hasAiTextGeneration: boolean
imageThumbnailBase64: string
blurData: string

View File

@ -0,0 +1,26 @@
import {
INFINITE_SCROLL_FEED_INITIAL,
INFINITE_SCROLL_FEED_MULTIPLE,
Photo,
} from '.';
import PhotosLarge from './PhotosLarge';
import PhotosLargeInfinite from './PhotosLargeInfinite';
export default function PhotoFeedPage({
photos,
photosCount,
}:{
photos: Photo[]
photosCount: number
}) {
return (
<div className="space-y-1">
<PhotosLarge {...{ photos }} />
{photosCount > photos.length &&
<PhotosLargeInfinite
initialOffset={INFINITE_SCROLL_FEED_INITIAL}
itemsPerPage={INFINITE_SCROLL_FEED_MULTIPLE}
/>}
</div>
);
}

View File

@ -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 (
<SiteGrid
contentMain={<div className={clsx(
header && 'space-y-8 mt-4',
)}>
{header &&
<AnimateItems
type="bottom"
items={[header]}
animateOnFirstLoadOnly
/>}
<div className="space-y-0.5 sm:space-y-1">
<PhotoGrid {...{
photos,
tag,
camera,
simulation,
focal,
animateOnFirstLoadOnly,
onAnimationComplete,
}} />
{count > initialOffset &&
<PhotoGridInfinite {...{
cacheKey,
initialOffset,
canStart: shouldAnimateDynamicItems,
tag,
camera,
simulation,
focal,
animateOnFirstLoadOnly,
}} />}
</div>
</div>}
contentSide={sidebar}
sideHiddenOnMobile
/>
);
}

View File

@ -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({
<InfinitePhotoScroll
cacheKey={cacheKey}
initialOffset={initialOffset}
itemsPerPage={INFINITE_SCROLL_GRID_PHOTO_MULTIPLE}
itemsPerPage={INFINITE_SCROLL_GRID_MULTIPLE}
tag={tag}
camera={camera}
simulation={simulation}

View File

@ -1,84 +1,37 @@
'use client';
import SiteGrid from '@/components/SiteGrid';
import { Tags } from '@/tag';
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';
import { Cameras } from '@/camera';
import { FilmSimulations } from '@/simulation';
import { PATH_GRID } from '@/site/paths';
import PhotoGridSidebar from './PhotoGridSidebar';
import PhotoGridContainer from './PhotoGridContainer';
export default function PhotoGridPage({
cacheKey,
photos,
count,
tag,
camera,
simulation,
focal,
animateOnFirstLoadOnly,
header,
sidebar,
photosCount,
tags,
cameras,
simulations,
}:{
cacheKey: string
photos: Photo[]
count: number
tag?: string
camera?: Camera
simulation?: FilmSimulation
focal?: number
animateOnFirstLoadOnly?: boolean
header?: JSX.Element
sidebar?: JSX.Element
photosCount: number
tags: Tags
cameras: Cameras
simulations: FilmSimulations
}) {
const [
shouldAnimateDynamicItems,
setShouldAnimateDynamicItems,
] = useState(false);
const onAnimationComplete = useCallback(() =>
setShouldAnimateDynamicItems(true), []);
const initialOffset = photos.length;
return (
<SiteGrid
contentMain={<div className={clsx(
header && 'space-y-8 mt-4',
)}>
{header &&
<AnimateItems
type="bottom"
items={[header]}
animateOnFirstLoadOnly
/>}
<div className="space-y-0.5 sm:space-y-1">
<PhotoGrid {...{
photos,
tag,
camera,
simulation,
focal,
animateOnFirstLoadOnly,
onAnimationComplete,
<PhotoGridContainer
cacheKey={`page-${PATH_GRID}`}
photos={photos}
count={photosCount}
sidebar={<div className="sticky top-4 space-y-4 mt-[-4px]">
<PhotoGridSidebar {...{
tags,
cameras,
simulations,
photosCount,
}} />
{count > initialOffset &&
<PhotoGridInfinite {...{
cacheKey,
initialOffset,
canStart: shouldAnimateDynamicItems,
tag,
camera,
simulation,
focal,
animateOnFirstLoadOnly,
}} />}
</div>
</div>}
contentSide={sidebar}
sideHiddenOnMobile
/>
);
}

View File

@ -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

View File

@ -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<PhotoFormData>
uniqueTags: TagsWithMeta
uniqueTags: Tags
hasAiTextGeneration?: boolean
textFieldsToAutoGenerate?: AiAutoGeneratedField[],
imageThumbnailBase64?: string

View File

@ -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');

View File

@ -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() : [],

View File

@ -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),
})))

View File

@ -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<PhotoFormData>
updatedExifData?: Partial<PhotoFormData>
updatedBlurData?: string
uniqueTags?: TagsWithMeta
uniqueTags?: Tags
aiContent?: AiContent
shouldStripGpsData?: boolean
onTitleChange?: (updatedTitle: string) => void

View File

@ -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;

View File

@ -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 (
<PhotoGridPage {...{
<PhotoGridContainer {...{
cacheKey: `simulation-${simulation}`,
photos,
count,

View File

@ -3,7 +3,7 @@
const INTRINSIC_WIDTH = 28;
const INTRINSIC_HEIGHT = 24;
export default function IconFullFrame({
export default function IconFeed({
width = INTRINSIC_WIDTH,
includeTitle = true,
}: {

View File

@ -8,12 +8,14 @@ import ViewSwitcher, { SwitcherSelection } from '@/site/ViewSwitcher';
import {
PATH_ROOT,
isPathAdmin,
isPathFeed,
isPathGrid,
isPathProtected,
isPathSignIn,
} from '@/site/paths';
import AnimateItems from '../components/AnimateItems';
import { useAppState } from '@/state/AppState';
import { SHOW_GRID_FIRST } from './config';
export default function Nav({
siteDomainOrTitle,
@ -36,9 +38,11 @@ export default function Nav({
const switcherSelectionForPath = (): SwitcherSelection | undefined => {
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';
}

View File

@ -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 = () =>
<SwitcherItem
icon={<IconFeed />}
href={SHOW_GRID_FIRST ? PATH_FEED : PATH_ROOT}
active={currentSelection === 'feed'}
noPadding
/>;
const renderItemGrid = () =>
<SwitcherItem
icon={<IconGrid />}
href={SHOW_GRID_FIRST ? PATH_ROOT : PATH_GRID}
active={currentSelection === 'grid'}
noPadding
/>;
return (
<div className="flex gap-1 sm:gap-2">
<Switcher>
<SwitcherItem
icon={<IconFullFrame />}
href="/"
active={currentSelection === 'full-frame'}
noPadding
/>
<SwitcherItem
icon={<IconGrid />}
href={PATH_GRID}
active={currentSelection === 'grid'}
noPadding
/>
{SHOW_GRID_FIRST ? renderItemGrid() : renderItemFeed()}
{SHOW_GRID_FIRST ? renderItemFeed() : renderItemGrid()}
{showAdmin &&
<SwitcherItem
icon={<BiLockAlt size={16} className="translate-y-[-0.5px]" />}

View File

@ -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';
@ -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)) {

View File

@ -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 (
<PhotoGridPage {...{
<PhotoGridContainer {...{
cacheKey: `tag-${tag}`,
photos,
count,

View File

@ -20,7 +20,7 @@ import {
export const TAG_FAVS = 'favs';
export const TAG_HIDDEN = 'hidden';
export type TagsWithMeta = {
export type Tags = {
tag: string
count: number
}[]
@ -57,7 +57,7 @@ export const sortTags = (
.sort((a, b) => 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,