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_ROOT = '/';
const PATH_GRID = '/grid'; const PATH_GRID = '/grid';
const PATH_FEED = '/feed';
const PATH_ADMIN = '/admin/photos'; const PATH_ADMIN = '/admin/photos';
const PATH_OG = '/og'; const PATH_OG = '/og';
const PATH_OG_ALL = `${PATH_OG}/all`; const PATH_OG_ALL = `${PATH_OG}/all`;
@ -202,27 +203,28 @@ describe('Paths', () => {
// Root // Root
expect(getEscapePath(PATH_ROOT)).toEqual(undefined); expect(getEscapePath(PATH_ROOT)).toEqual(undefined);
expect(getEscapePath(PATH_GRID)).toEqual(undefined); expect(getEscapePath(PATH_GRID)).toEqual(undefined);
expect(getEscapePath(PATH_FEED)).toEqual(undefined);
expect(getEscapePath(PATH_ADMIN)).toEqual(undefined); expect(getEscapePath(PATH_ADMIN)).toEqual(undefined);
// Photo // Photo
expect(getEscapePath(PATH_PHOTO)).toEqual(PATH_GRID); expect(getEscapePath(PATH_PHOTO)).toEqual(PATH_ROOT);
expect(getEscapePath(PATH_PHOTO_SHARE)).toEqual(PATH_PHOTO); expect(getEscapePath(PATH_PHOTO_SHARE)).toEqual(PATH_PHOTO);
// Tag // 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_SHARE)).toEqual(PATH_TAG);
expect(getEscapePath(PATH_TAG_PHOTO)).toEqual(PATH_TAG); expect(getEscapePath(PATH_TAG_PHOTO)).toEqual(PATH_TAG);
expect(getEscapePath(PATH_TAG_PHOTO_SHARE)).toEqual(PATH_TAG_PHOTO); expect(getEscapePath(PATH_TAG_PHOTO_SHARE)).toEqual(PATH_TAG_PHOTO);
// Camera // 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_SHARE)).toEqual(PATH_CAMERA);
expect(getEscapePath(PATH_CAMERA_PHOTO)).toEqual(PATH_CAMERA); expect(getEscapePath(PATH_CAMERA_PHOTO)).toEqual(PATH_CAMERA);
expect(getEscapePath(PATH_CAMERA_PHOTO_SHARE)).toEqual(PATH_CAMERA_PHOTO); expect(getEscapePath(PATH_CAMERA_PHOTO_SHARE)).toEqual(PATH_CAMERA_PHOTO);
// Film Simulation // 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_SHARE)).toEqual(PATH_FILM_SIMULATION);
expect(getEscapePath(PATH_FILM_SIMULATION_PHOTO)).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); expect(getEscapePath(PATH_FILM_SIMULATION_PHOTO_SHARE)).toEqual(PATH_FILM_SIMULATION_PHOTO);
// Focal Length // 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_SHARE)).toEqual(PATH_FOCAL_LENGTH);
expect(getEscapePath(PATH_FOCAL_LENGTH_PHOTO)).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); 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 { addAllUploadsAction } from '@/photo/actions';
import { PATH_ADMIN_PHOTOS } from '@/site/paths'; import { PATH_ADMIN_PHOTOS } from '@/site/paths';
import { import {
TagsWithMeta, Tags,
convertTagsForForm, convertTagsForForm,
getValidationMessageForTags, getValidationMessageForTags,
} from '@/tag'; } from '@/tag';
@ -32,7 +32,7 @@ export default function AdminAddAllUploads({
setAddedUploadUrls, setAddedUploadUrls,
}: { }: {
storageUrls: string[] storageUrls: string[]
uniqueTags?: TagsWithMeta uniqueTags?: Tags
isAdding: boolean isAdding: boolean
setIsAdding: (isAdding: boolean) => void setIsAdding: (isAdding: boolean) => void
setAddedUploadUrls?: Dispatch<SetStateAction<string[]>> setAddedUploadUrls?: Dispatch<SetStateAction<string[]>>

View File

@ -4,7 +4,7 @@ import AdminTable from '@/admin/AdminTable';
import { Fragment } from 'react'; import { Fragment } from 'react';
import DeleteButton from '@/admin/DeleteButton'; import DeleteButton from '@/admin/DeleteButton';
import { photoQuantityText } from '@/photo'; import { photoQuantityText } from '@/photo';
import { TagsWithMeta, formatTag, sortTagsObject } from '@/tag'; import { Tags, formatTag, sortTagsObject } from '@/tag';
import EditButton from '@/admin/EditButton'; import EditButton from '@/admin/EditButton';
import { pathForAdminTagEdit } from '@/site/paths'; import { pathForAdminTagEdit } from '@/site/paths';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
@ -13,7 +13,7 @@ import AdminTagBadge from './AdminTagBadge';
export default function AdminTagTable({ export default function AdminTagTable({
tags, tags,
}: { }: {
tags: TagsWithMeta tags: Tags
}) { }) {
return ( return (
<AdminTable> <AdminTable>

View File

@ -4,7 +4,7 @@ import { StorageListResponse } from '@/services/storage';
import AdminAddAllUploads from './AdminAddAllUploads'; import AdminAddAllUploads from './AdminAddAllUploads';
import AdminUploadsTable from './AdminUploadsTable'; import AdminUploadsTable from './AdminUploadsTable';
import { useState } from 'react'; import { useState } from 'react';
import { TagsWithMeta } from '@/tag'; import { Tags } from '@/tag';
export default function AdminUploadsClient({ export default function AdminUploadsClient({
title, title,
@ -13,7 +13,7 @@ export default function AdminUploadsClient({
}: { }: {
title?: string title?: string
urls: StorageListResponse urls: StorageListResponse
uniqueTags?: TagsWithMeta uniqueTags?: Tags
}) { }) {
const [isAdding, setIsAdding] = useState(false); const [isAdding, setIsAdding] = useState(false);
const [addedUploadUrls, setAddedUploadUrls] = useState<string[]>([]); 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 { FilmSimulation, generateMetaForFilmSimulation } from '@/simulation';
import FilmSimulationOverview from '@/simulation/FilmSimulationOverview'; import FilmSimulationOverview from '@/simulation/FilmSimulationOverview';
import { getPhotosFilmSimulationDataCached } from '@/simulation/data'; import { getPhotosFilmSimulationDataCached } from '@/simulation/data';
@ -20,7 +20,7 @@ export async function generateMetadata({
{ count, dateRange }, { count, dateRange },
] = await getPhotosFilmSimulationDataCachedCached({ ] = await getPhotosFilmSimulationDataCachedCached({
simulation, simulation,
limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, limit: INFINITE_SCROLL_GRID_INITIAL,
}); });
const { const {
@ -55,7 +55,7 @@ export default async function FilmSimulationPage({
{ count, dateRange }, { count, dateRange },
] = await getPhotosFilmSimulationDataCachedCached({ ] = await getPhotosFilmSimulationDataCachedCached({
simulation, simulation,
limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, limit: INFINITE_SCROLL_GRID_INITIAL,
}); });
return ( 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 { FilmSimulation, generateMetaForFilmSimulation } from '@/simulation';
import FilmSimulationOverview from '@/simulation/FilmSimulationOverview'; import FilmSimulationOverview from '@/simulation/FilmSimulationOverview';
import FilmSimulationShareModal from '@/simulation/FilmSimulationShareModal'; import FilmSimulationShareModal from '@/simulation/FilmSimulationShareModal';
@ -9,7 +9,7 @@ import { cache } from 'react';
const getPhotosFilmSimulationDataCachedCached = const getPhotosFilmSimulationDataCachedCached =
cache((simulation: FilmSimulation) => getPhotosFilmSimulationDataCached({ cache((simulation: FilmSimulation) => getPhotosFilmSimulationDataCached({
simulation, simulation,
limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, limit: INFINITE_SCROLL_GRID_INITIAL,
})); }));
interface FilmSimulationProps { interface FilmSimulationProps {

View File

@ -1,7 +1,7 @@
import { generateMetaForFocalLength, getFocalLengthFromString } from '@/focal'; import { generateMetaForFocalLength, getFocalLengthFromString } from '@/focal';
import FocalLengthOverview from '@/focal/FocalLengthOverview'; import FocalLengthOverview from '@/focal/FocalLengthOverview';
import { getPhotosFocalLengthDataCached } from '@/focal/data'; 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 { PATH_ROOT } from '@/site/paths';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
@ -10,7 +10,7 @@ import { cache } from 'react';
const getPhotosFocalDataCachedCached = cache((focal: number) => const getPhotosFocalDataCachedCached = cache((focal: number) =>
getPhotosFocalLengthDataCached({ getPhotosFocalLengthDataCached({
focal, focal,
limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, limit: INFINITE_SCROLL_GRID_INITIAL,
})); }));
interface FocalLengthProps { interface FocalLengthProps {

View File

@ -2,14 +2,14 @@ import { generateMetaForFocalLength, getFocalLengthFromString } from '@/focal';
import FocalLengthOverview from '@/focal/FocalLengthOverview'; import FocalLengthOverview from '@/focal/FocalLengthOverview';
import FocalLengthShareModal from '@/focal/FocalLengthShareModal'; import FocalLengthShareModal from '@/focal/FocalLengthShareModal';
import { getPhotosFocalLengthDataCached } from '@/focal/data'; 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 type { Metadata } from 'next';
import { cache } from 'react'; import { cache } from 'react';
const getPhotosFocalLengthDataCachedCached = cache((focal: number) => const getPhotosFocalLengthDataCachedCached = cache((focal: number) =>
getPhotosFocalLengthDataCached({ getPhotosFocalLengthDataCached({
focal, focal,
limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, limit: INFINITE_SCROLL_GRID_INITIAL,
})); }));
interface FocalLengthProps { interface FocalLengthProps {

View File

@ -1,20 +1,18 @@
import { import {
INFINITE_SCROLL_GRID_PHOTO_INITIAL, INFINITE_SCROLL_GRID_INITIAL,
generateOgImageMetaForPhotos, generateOgImageMetaForPhotos,
} from '@/photo'; } from '@/photo';
import PhotosEmptyState from '@/photo/PhotosEmptyState'; import PhotosEmptyState from '@/photo/PhotosEmptyState';
import { Metadata } from 'next/types'; import { Metadata } from 'next/types';
import PhotoGridSidebar from '@/photo/PhotoGridSidebar';
import { getPhotoSidebarData } from '@/photo/data'; import { getPhotoSidebarData } from '@/photo/data';
import { getPhotos } from '@/photo/db/query'; import { getPhotos, getPhotosMeta } from '@/photo/db/query';
import { cache } from 'react'; import { cache } from 'react';
import PhotoGridPage from '@/photo/PhotoGridPage'; import PhotoGridPage from '@/photo/PhotoGridPage';
import { PATH_GRID } from '@/site/paths';
export const dynamic = 'force-static'; export const dynamic = 'force-static';
const getPhotosCached = cache(() => getPhotos({ const getPhotosCached = cache(() => getPhotos({
limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, limit: INFINITE_SCROLL_GRID_INITIAL,
})); }));
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
@ -33,23 +31,16 @@ export default async function GridPage() {
] = await Promise.all([ ] = await Promise.all([
getPhotosCached() getPhotosCached()
.catch(() => []), .catch(() => []),
getPhotosMeta()
.then(({ count }) => count)
.catch(() => 0),
...getPhotoSidebarData(), ...getPhotoSidebarData(),
]); ]);
return ( return (
photos.length > 0 photos.length > 0
? <PhotoGridPage ? <PhotoGridPage
cacheKey={`page-${PATH_GRID}`} {...{ photos, photosCount, tags, cameras, simulations }}
photos={photos}
count={photosCount}
sidebar={<div className="sticky top-4 space-y-4 mt-[-4px]">
<PhotoGridSidebar {...{
tags,
cameras,
simulations,
photosCount,
}} />
</div>}
/> />
: <PhotosEmptyState /> : <PhotosEmptyState />
); );

View File

@ -1,6 +1,6 @@
import { import {
INFINITE_SCROLL_GRID_PHOTO_INITIAL, INFINITE_SCROLL_GRID_INITIAL,
INFINITE_SCROLL_GRID_PHOTO_MULTIPLE, INFINITE_SCROLL_GRID_MULTIPLE,
} from '@/photo'; } from '@/photo';
import { getPhotosCached } from '@/photo/cache'; import { getPhotosCached } from '@/photo/cache';
import { getPhotosMeta } from '@/photo/db/query'; import { getPhotosMeta } from '@/photo/db/query';
@ -12,7 +12,7 @@ export default async function OGPage() {
photos, photos,
count, count,
] = await Promise.all([ ] = await Promise.all([
getPhotosCached({ limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL }) getPhotosCached({ limit: INFINITE_SCROLL_GRID_INITIAL })
.catch(() => []), .catch(() => []),
getPhotosMeta() getPhotosMeta()
.then(({ count }) => count) .then(({ count }) => count)
@ -26,7 +26,7 @@ export default async function OGPage() {
<div className="mt-3"> <div className="mt-3">
<StaggeredOgPhotosInfinite <StaggeredOgPhotosInfinite
initialOffset={photos.length} initialOffset={photos.length}
itemsPerPage={INFINITE_SCROLL_GRID_PHOTO_MULTIPLE} itemsPerPage={INFINITE_SCROLL_GRID_MULTIPLE}
/> />
</div>} </div>}
</> </>

View File

@ -1,20 +1,24 @@
import { import {
INFINITE_SCROLL_LARGE_PHOTO_INITIAL, INFINITE_SCROLL_FEED_INITIAL,
INFINITE_SCROLL_LARGE_PHOTO_MULTIPLE, INFINITE_SCROLL_GRID_INITIAL,
generateOgImageMetaForPhotos, generateOgImageMetaForPhotos,
} from '@/photo'; } from '@/photo';
import PhotosEmptyState from '@/photo/PhotosEmptyState'; import PhotosEmptyState from '@/photo/PhotosEmptyState';
import { Metadata } from 'next/types'; import { Metadata } from 'next/types';
import PhotosLarge from '@/photo/PhotosLarge';
import { cache } from 'react'; import { cache } from 'react';
import { getPhotos, getPhotosMeta } from '@/photo/db/query'; 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 dynamic = 'force-static';
export const maxDuration = 60; export const maxDuration = 60;
const getPhotosCached = cache(() => getPhotos({ 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> { export async function generateMetadata(): Promise<Metadata> {
@ -27,24 +31,29 @@ export default async function HomePage() {
const [ const [
photos, photos,
photosCount, photosCount,
tags,
cameras,
simulations,
] = await Promise.all([ ] = await Promise.all([
getPhotosCached() getPhotosCached()
.catch(() => []), .catch(() => []),
getPhotosMeta() getPhotosMeta()
.then(({ count }) => count) .then(({ count }) => count)
.catch(() => 0), .catch(() => 0),
...(SHOW_GRID_FIRST
? getPhotoSidebarData()
: [[], [], []]),
]); ]);
return ( return (
photos.length > 0 photos.length > 0
? <div className="space-y-1"> ? SHOW_GRID_FIRST
<PhotosLarge {...{ photos }} /> ? <PhotoGridPage
{photosCount > photos.length && {...{ photos, photosCount, tags, cameras, simulations }}
<PhotosLargeInfinite />
initialOffset={INFINITE_SCROLL_LARGE_PHOTO_INITIAL} : <PhotoFeedPage
itemsPerPage={INFINITE_SCROLL_LARGE_PHOTO_MULTIPLE} {...{ photos, photosCount }}
/>} />
</div>
: <PhotosEmptyState /> : <PhotosEmptyState />
); );
} }

View File

@ -1,7 +1,7 @@
import { Metadata } from 'next/types'; import { Metadata } from 'next/types';
import { CameraProps } from '@/camera'; import { CameraProps } from '@/camera';
import { generateMetaForCamera } from '@/camera/meta'; 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 { getPhotosCameraDataCached } from '@/camera/data';
import CameraOverview from '@/camera/CameraOverview'; import CameraOverview from '@/camera/CameraOverview';
import { cache } from 'react'; import { cache } from 'react';
@ -12,7 +12,7 @@ const getPhotosCameraDataCachedCached = cache((
) => getPhotosCameraDataCached( ) => getPhotosCameraDataCached(
make, make,
model, model,
INFINITE_SCROLL_GRID_PHOTO_INITIAL, INFINITE_SCROLL_GRID_INITIAL,
)); ));
export async function generateMetadata({ export async function generateMetadata({

View File

@ -2,7 +2,7 @@ import { CameraProps } from '@/camera';
import CameraShareModal from '@/camera/CameraShareModal'; import CameraShareModal from '@/camera/CameraShareModal';
import { generateMetaForCamera } from '@/camera/meta'; import { generateMetaForCamera } from '@/camera/meta';
import { Metadata } from 'next/types'; 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 { getPhotosCameraDataCached } from '@/camera/data';
import CameraOverview from '@/camera/CameraOverview'; import CameraOverview from '@/camera/CameraOverview';
import { cache } from 'react'; import { cache } from 'react';
@ -13,7 +13,7 @@ const getPhotosCameraDataCachedCached = cache((
) => getPhotosCameraDataCached( ) => getPhotosCameraDataCached(
make, make,
model, model,
INFINITE_SCROLL_GRID_PHOTO_INITIAL, INFINITE_SCROLL_GRID_INITIAL,
)); ));
export async function generateMetadata({ 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 { PATH_ROOT } from '@/site/paths';
import { generateMetaForTag } from '@/tag'; import { generateMetaForTag } from '@/tag';
import TagOverview from '@/tag/TagOverview'; import TagOverview from '@/tag/TagOverview';
@ -8,7 +8,7 @@ import { redirect } from 'next/navigation';
import { cache } from 'react'; import { cache } from 'react';
const getPhotosTagDataCachedCached = cache((tag: string) => const getPhotosTagDataCachedCached = cache((tag: string) =>
getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL})); getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_INITIAL}));
interface TagProps { interface TagProps {
params: { tag: string } 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 { generateMetaForTag } from '@/tag';
import TagOverview from '@/tag/TagOverview'; import TagOverview from '@/tag/TagOverview';
import TagShareModal from '@/tag/TagShareModal'; import TagShareModal from '@/tag/TagShareModal';
@ -7,7 +7,7 @@ import type { Metadata } from 'next';
import { cache } from 'react'; import { cache } from 'react';
const getPhotosTagDataCachedCached = cache((tag: string) => const getPhotosTagDataCachedCached = cache((tag: string) =>
getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL })); getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_INITIAL }));
interface TagProps { interface TagProps {
params: { tag: string } params: { tag: string }

View File

@ -1,7 +1,7 @@
import { Photo, PhotoDateRange } from '@/photo'; import { Photo, PhotoDateRange } from '@/photo';
import { Camera, createCameraKey } from '.'; import { Camera, createCameraKey } from '.';
import CameraHeader from './CameraHeader'; import CameraHeader from './CameraHeader';
import PhotoGridPage from '@/photo/PhotoGridPage'; import PhotoGridContainer from '@/photo/PhotoGridContainer';
export default function CameraOverview({ export default function CameraOverview({
camera, camera,
@ -17,7 +17,7 @@ export default function CameraOverview({
animateOnFirstLoadOnly?: boolean, animateOnFirstLoadOnly?: boolean,
}) { }) {
return ( return (
<PhotoGridPage {...{ <PhotoGridContainer {...{
cacheKey: `camera-${createCameraKey(camera)}`, cacheKey: `camera-${createCameraKey(camera)}`,
photos, photos,
count, count,

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import PhotoForm from './form/PhotoForm';
import { useFormState } from 'react-dom'; import { useFormState } from 'react-dom';
import { areSimpleObjectsEqual } from '@/utility/object'; import { areSimpleObjectsEqual } from '@/utility/object';
import { getExifDataAction } from './actions'; import { getExifDataAction } from './actions';
import { TagsWithMeta } from '@/tag'; import { Tags } from '@/tag';
import AiButton from './ai/AiButton'; import AiButton from './ai/AiButton';
import usePhotoFormParent from './form/usePhotoFormParent'; import usePhotoFormParent from './form/usePhotoFormParent';
import ExifSyncButton from '@/admin/ExifSyncButton'; import ExifSyncButton from '@/admin/ExifSyncButton';
@ -24,7 +24,7 @@ export default function PhotoEditPageClient({
blurData, blurData,
}: { }: {
photo: Photo photo: Photo
uniqueTags: TagsWithMeta uniqueTags: Tags
hasAiTextGeneration: boolean hasAiTextGeneration: boolean
imageThumbnailBase64: string imageThumbnailBase64: string
blurData: 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'; 'use client';
import { Camera } from '@/camera'; import { Camera } from '@/camera';
import { INFINITE_SCROLL_GRID_PHOTO_MULTIPLE } from '.'; import { INFINITE_SCROLL_GRID_MULTIPLE } from '.';
import InfinitePhotoScroll from './InfinitePhotoScroll'; import InfinitePhotoScroll from './InfinitePhotoScroll';
import PhotoGrid from './PhotoGrid'; import PhotoGrid from './PhotoGrid';
import { FilmSimulation } from '@/simulation'; import { FilmSimulation } from '@/simulation';
@ -29,7 +29,7 @@ export default function PhotoGridInfinite({
<InfinitePhotoScroll <InfinitePhotoScroll
cacheKey={cacheKey} cacheKey={cacheKey}
initialOffset={initialOffset} initialOffset={initialOffset}
itemsPerPage={INFINITE_SCROLL_GRID_PHOTO_MULTIPLE} itemsPerPage={INFINITE_SCROLL_GRID_MULTIPLE}
tag={tag} tag={tag}
camera={camera} camera={camera}
simulation={simulation} simulation={simulation}

View File

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

View File

@ -7,7 +7,7 @@ import PhotoTag from '@/tag/PhotoTag';
import { FaTag } from 'react-icons/fa'; import { FaTag } from 'react-icons/fa';
import { IoMdCamera } from 'react-icons/io'; import { IoMdCamera } from 'react-icons/io';
import { PhotoDateRange, dateRangeForPhotos, photoQuantityText } from '.'; 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 PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon'; import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
import { FilmSimulations, sortFilmSimulationsWithCount } from '@/simulation'; import { FilmSimulations, sortFilmSimulationsWithCount } from '@/simulation';
@ -23,7 +23,7 @@ export default function PhotoGridSidebar({
photosCount, photosCount,
photosDateRange, photosDateRange,
}: { }: {
tags: TagsWithMeta tags: Tags
cameras: Cameras cameras: Cameras
simulations: FilmSimulations simulations: FilmSimulations
photosCount: number photosCount: number

View File

@ -4,7 +4,7 @@ import AdminChildPage from '@/components/AdminChildPage';
import { PATH_ADMIN_UPLOADS } from '@/site/paths'; import { PATH_ADMIN_UPLOADS } from '@/site/paths';
import { PhotoFormData, generateTakenAtFields } from './form'; import { PhotoFormData, generateTakenAtFields } from './form';
import PhotoForm from './form/PhotoForm'; import PhotoForm from './form/PhotoForm';
import { TagsWithMeta } from '@/tag'; import { Tags } from '@/tag';
import usePhotoFormParent from './form/usePhotoFormParent'; import usePhotoFormParent from './form/usePhotoFormParent';
import AiButton from './ai/AiButton'; import AiButton from './ai/AiButton';
import { AiAutoGeneratedField } from './ai'; import { AiAutoGeneratedField } from './ai';
@ -21,7 +21,7 @@ export default function UploadPageClient({
}: { }: {
blobId?: string blobId?: string
photoFormExif: Partial<PhotoFormData> photoFormExif: Partial<PhotoFormData>
uniqueTags: TagsWithMeta uniqueTags: Tags
hasAiTextGeneration?: boolean hasAiTextGeneration?: boolean
textFieldsToAutoGenerate?: AiAutoGeneratedField[], textFieldsToAutoGenerate?: AiAutoGeneratedField[],
imageThumbnailBase64?: string imageThumbnailBase64?: string

View File

@ -24,6 +24,7 @@ import {
PATHS_ADMIN, PATHS_ADMIN,
PATHS_TO_CACHE, PATHS_TO_CACHE,
PATH_ADMIN, PATH_ADMIN,
PATH_FEED,
PATH_GRID, PATH_GRID,
PATH_ROOT, PATH_ROOT,
PREFIX_CAMERA, PREFIX_CAMERA,
@ -126,6 +127,7 @@ export const revalidatePhoto = (photoId: string) => {
revalidatePath(pathForPhoto({ photo: photoId }), 'layout'); revalidatePath(pathForPhoto({ photo: photoId }), 'layout');
revalidatePath(PATH_ROOT, 'layout'); revalidatePath(PATH_ROOT, 'layout');
revalidatePath(PATH_GRID, 'layout'); revalidatePath(PATH_GRID, 'layout');
revalidatePath(PATH_FEED, 'layout');
revalidatePath(PREFIX_TAG, 'layout'); revalidatePath(PREFIX_TAG, 'layout');
revalidatePath(PREFIX_CAMERA, 'layout'); revalidatePath(PREFIX_CAMERA, 'layout');
revalidatePath(PREFIX_FILM_SIMULATION, 'layout'); revalidatePath(PREFIX_FILM_SIMULATION, 'layout');

View File

@ -1,11 +1,9 @@
import { import {
getPhotosMetaCached,
getUniqueCamerasCached, getUniqueCamerasCached,
getUniqueFilmSimulationsCached, getUniqueFilmSimulationsCached,
getUniqueTagsCached, getUniqueTagsCached,
} from '@/photo/cache'; } from '@/photo/cache';
import { import {
getPhotosMeta,
getUniqueCameras, getUniqueCameras,
getUniqueFilmSimulations, getUniqueFilmSimulations,
getUniqueTags, getUniqueTags,
@ -14,9 +12,6 @@ import { SHOW_FILM_SIMULATIONS } from '@/site/config';
import { sortTagsObject } from '@/tag'; import { sortTagsObject } from '@/tag';
export const getPhotoSidebarData = () => [ export const getPhotoSidebarData = () => [
getPhotosMeta()
.then(({ count }) => count)
.catch(() => 0),
getUniqueTags().then(sortTagsObject).catch(() => []), getUniqueTags().then(sortTagsObject).catch(() => []),
getUniqueCameras().catch(() => []), getUniqueCameras().catch(() => []),
SHOW_FILM_SIMULATIONS SHOW_FILM_SIMULATIONS
@ -25,9 +20,6 @@ export const getPhotoSidebarData = () => [
] as const; ] as const;
export const getPhotoSidebarDataCached = () => [ export const getPhotoSidebarDataCached = () => [
getPhotosMetaCached()
.then(({ count }) => count)
.catch(() => 0),
getUniqueTagsCached().then(sortTagsObject), getUniqueTagsCached().then(sortTagsObject),
getUniqueCamerasCached(), getUniqueCamerasCached(),
SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [], SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [],

View File

@ -12,7 +12,7 @@ import {
PhotoDateRange, PhotoDateRange,
} from '@/photo'; } from '@/photo';
import { Cameras, createCameraKey } from '@/camera'; import { Cameras, createCameraKey } from '@/camera';
import { TagsWithMeta } from '@/tag'; import { Tags } from '@/tag';
import { FilmSimulation, FilmSimulations } from '@/simulation'; import { FilmSimulation, FilmSimulations } from '@/simulation';
import { SHOULD_DEBUG_SQL } from '@/site/config'; import { SHOULD_DEBUG_SQL } from '@/site/config';
import { import {
@ -261,7 +261,7 @@ export const getUniqueTags = async () =>
WHERE hidden IS NOT TRUE WHERE hidden IS NOT TRUE
GROUP BY tag GROUP BY tag
ORDER BY tag ASC ORDER BY tag ASC
`.then(({ rows }): TagsWithMeta => rows.map(({ tag, count }) => ({ `.then(({ rows }): Tags => rows.map(({ tag, count }) => ({
tag: tag as string, tag: tag as string,
count: parseInt(count, 10), count: parseInt(count, 10),
}))) })))
@ -273,7 +273,7 @@ export const getUniqueTagsHidden = async () =>
FROM photos FROM photos
GROUP BY tag GROUP BY tag
ORDER BY tag ASC ORDER BY tag ASC
`.then(({ rows }): TagsWithMeta => rows.map(({ tag, count }) => ({ `.then(({ rows }): Tags => rows.map(({ tag, count }) => ({
tag: tag as string, tag: tag as string,
count: parseInt(count, 10), 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 { toastSuccess, toastWarning } from '@/toast';
import { getDimensionsFromSize } from '@/utility/size'; import { getDimensionsFromSize } from '@/utility/size';
import ImageWithFallback from '@/components/image/ImageWithFallback'; import ImageWithFallback from '@/components/image/ImageWithFallback';
import { TagsWithMeta, convertTagsForForm } from '@/tag'; import { Tags, convertTagsForForm } from '@/tag';
import { AiContent } from '../ai/useAiImageQueries'; import { AiContent } from '../ai/useAiImageQueries';
import AiButton from '../ai/AiButton'; import AiButton from '../ai/AiButton';
import Spinner from '@/components/Spinner'; import Spinner from '@/components/Spinner';
@ -49,7 +49,7 @@ export default function PhotoForm({
initialPhotoForm: Partial<PhotoFormData> initialPhotoForm: Partial<PhotoFormData>
updatedExifData?: Partial<PhotoFormData> updatedExifData?: Partial<PhotoFormData>
updatedBlurData?: string updatedBlurData?: string
uniqueTags?: TagsWithMeta uniqueTags?: Tags
aiContent?: AiContent aiContent?: AiContent
shouldStripGpsData?: boolean shouldStripGpsData?: boolean
onTitleChange?: (updatedTitle: string) => void onTitleChange?: (updatedTitle: string) => void

View File

@ -16,17 +16,17 @@ import type { Metadata } from 'next';
export const OUTDATED_THRESHOLD = new Date('2024-06-16'); export const OUTDATED_THRESHOLD = new Date('2024-06-16');
// INFINITE SCROLL: LARGE PHOTOS // INFINITE SCROLL: FEED
export const INFINITE_SCROLL_LARGE_PHOTO_INITIAL = export const INFINITE_SCROLL_FEED_INITIAL =
process.env.NODE_ENV === 'development' ? 2 : 12; 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; process.env.NODE_ENV === 'development' ? 2 : 24;
// INFINITE SCROLL: GRID PHOTOS // INFINITE SCROLL: GRID
export const INFINITE_SCROLL_GRID_PHOTO_INITIAL = HIGH_DENSITY_GRID export const INFINITE_SCROLL_GRID_INITIAL = HIGH_DENSITY_GRID
? process.env.NODE_ENV === 'development' ? 12 : 24 ? process.env.NODE_ENV === 'development' ? 12 : 24
: 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
: 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 { Photo, PhotoDateRange } from '@/photo';
import FilmSimulationHeader from './FilmSimulationHeader'; import FilmSimulationHeader from './FilmSimulationHeader';
import { FilmSimulation } from '.'; import { FilmSimulation } from '.';
import PhotoGridPage from '@/photo/PhotoGridPage'; import PhotoGridContainer from '@/photo/PhotoGridContainer';
export default function FilmSimulationOverview({ export default function FilmSimulationOverview({
simulation, simulation,
@ -17,7 +17,7 @@ export default function FilmSimulationOverview({
animateOnFirstLoadOnly?: boolean, animateOnFirstLoadOnly?: boolean,
}) { }) {
return ( return (
<PhotoGridPage {...{ <PhotoGridContainer {...{
cacheKey: `simulation-${simulation}`, cacheKey: `simulation-${simulation}`,
photos, photos,
count, count,

View File

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

View File

@ -8,12 +8,14 @@ import ViewSwitcher, { SwitcherSelection } from '@/site/ViewSwitcher';
import { import {
PATH_ROOT, PATH_ROOT,
isPathAdmin, isPathAdmin,
isPathFeed,
isPathGrid, isPathGrid,
isPathProtected, isPathProtected,
isPathSignIn, isPathSignIn,
} from '@/site/paths'; } from '@/site/paths';
import AnimateItems from '../components/AnimateItems'; import AnimateItems from '../components/AnimateItems';
import { useAppState } from '@/state/AppState'; import { useAppState } from '@/state/AppState';
import { SHOW_GRID_FIRST } from './config';
export default function Nav({ export default function Nav({
siteDomainOrTitle, siteDomainOrTitle,
@ -36,9 +38,11 @@ export default function Nav({
const switcherSelectionForPath = (): SwitcherSelection | undefined => { const switcherSelectionForPath = (): SwitcherSelection | undefined => {
if (pathname === PATH_ROOT) { if (pathname === PATH_ROOT) {
return 'full-frame'; return SHOW_GRID_FIRST ? 'grid' : 'feed';
} else if (isPathGrid(pathname)) { } else if (isPathGrid(pathname)) {
return 'grid'; return 'grid';
} else if (isPathFeed(pathname)) {
return 'feed';
} else if (isPathProtected(pathname)) { } else if (isPathProtected(pathname)) {
return 'admin'; return 'admin';
} }

View File

@ -1,13 +1,19 @@
import Switcher from '@/components/Switcher'; import Switcher from '@/components/Switcher';
import SwitcherItem from '@/components/SwitcherItem'; import SwitcherItem from '@/components/SwitcherItem';
import IconFullFrame from '@/site/IconFullFrame'; import IconFeed from '@/site/IconFeed';
import IconGrid from '@/site/IconGrid'; 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 { BiLockAlt } from 'react-icons/bi';
import IconSearch from './IconSearch'; import IconSearch from './IconSearch';
import { useAppState } from '@/state/AppState'; 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({ export default function ViewSwitcher({
currentSelection, currentSelection,
@ -18,21 +24,27 @@ export default function ViewSwitcher({
}) { }) {
const { setIsCommandKOpen } = useAppState(); 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 ( return (
<div className="flex gap-1 sm:gap-2"> <div className="flex gap-1 sm:gap-2">
<Switcher> <Switcher>
<SwitcherItem {SHOW_GRID_FIRST ? renderItemGrid() : renderItemFeed()}
icon={<IconFullFrame />} {SHOW_GRID_FIRST ? renderItemFeed() : renderItemGrid()}
href="/"
active={currentSelection === 'full-frame'}
noPadding
/>
<SwitcherItem
icon={<IconGrid />}
href={PATH_GRID}
active={currentSelection === 'grid'}
noPadding
/>
{showAdmin && {showAdmin &&
<SwitcherItem <SwitcherItem
icon={<BiLockAlt size={16} className="translate-y-[-0.5px]" />} icon={<BiLockAlt size={16} className="translate-y-[-0.5px]" />}

View File

@ -8,6 +8,7 @@ import { TAG_HIDDEN } from '@/tag';
// Core paths // Core paths
export const PATH_ROOT = '/'; export const PATH_ROOT = '/';
export const PATH_GRID = '/grid'; export const PATH_GRID = '/grid';
export const PATH_FEED = '/feed';
export const PATH_ADMIN = '/admin'; export const PATH_ADMIN = '/admin';
export const PATH_API = '/api'; export const PATH_API = '/api';
export const PATH_SIGN_IN = '/sign-in'; export const PATH_SIGN_IN = '/sign-in';
@ -60,6 +61,7 @@ export const PATHS_ADMIN = [
export const PATHS_TO_CACHE = [ export const PATHS_TO_CACHE = [
PATH_ROOT, PATH_ROOT,
PATH_GRID, PATH_GRID,
PATH_FEED,
PATH_OG, PATH_OG,
PATH_PHOTO_DYNAMIC, PATH_PHOTO_DYNAMIC,
PATH_TAG_DYNAMIC, PATH_TAG_DYNAMIC,
@ -252,6 +254,9 @@ export const checkPathPrefix = (pathname = '', prefix: string) =>
export const isPathGrid = (pathname?: string) => export const isPathGrid = (pathname?: string) =>
checkPathPrefix(pathname, PATH_GRID); checkPathPrefix(pathname, PATH_GRID);
export const isPathFeed = (pathname?: string) =>
checkPathPrefix(pathname, PATH_FEED);
export const isPathSignIn = (pathname?: string) => export const isPathSignIn = (pathname?: string) =>
checkPathPrefix(pathname, PATH_SIGN_IN); checkPathPrefix(pathname, PATH_SIGN_IN);
@ -334,7 +339,7 @@ export const getEscapePath = (pathname?: string) => {
(simulation && isPathFilmSimulation(pathname)) || (simulation && isPathFilmSimulation(pathname)) ||
(focal && isPathFocalLength(pathname)) (focal && isPathFocalLength(pathname))
) { ) {
return PATH_GRID; return PATH_ROOT;
} else if (photoId && isPathTagPhotoShare(pathname)) { } else if (photoId && isPathTagPhotoShare(pathname)) {
return pathForPhoto({ photo: photoId, tag }); return pathForPhoto({ photo: photoId, tag });
} else if (photoId && isPathCameraPhotoShare(pathname)) { } else if (photoId && isPathCameraPhotoShare(pathname)) {

View File

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

View File

@ -20,7 +20,7 @@ import {
export const TAG_FAVS = 'favs'; export const TAG_FAVS = 'favs';
export const TAG_HIDDEN = 'hidden'; export const TAG_HIDDEN = 'hidden';
export type TagsWithMeta = { export type Tags = {
tag: string tag: string
count: number count: number
}[] }[]
@ -57,7 +57,7 @@ export const sortTags = (
.sort((a, b) => isTagFavs(a) ? -1 : a.localeCompare(b)); .sort((a, b) => isTagFavs(a) ? -1 : a.localeCompare(b));
export const sortTagsObject = ( export const sortTagsObject = (
tags: TagsWithMeta, tags: Tags,
tagToHide?: string, tagToHide?: string,
) => tags ) => tags
.filter(({ tag }) => tag!== tagToHide) .filter(({ tag }) => tag!== tagToHide)
@ -66,7 +66,7 @@ export const sortTagsObject = (
export const sortTagsWithoutFavs = (tags: string[]) => export const sortTagsWithoutFavs = (tags: string[]) =>
sortTags(tags, TAG_FAVS); sortTags(tags, TAG_FAVS);
export const sortTagsObjectWithoutFavs = (tags: TagsWithMeta) => export const sortTagsObjectWithoutFavs = (tags: Tags) =>
sortTagsObject(tags, TAG_FAVS); sortTagsObject(tags, TAG_FAVS);
export const descriptionForTaggedPhotos = ( export const descriptionForTaggedPhotos = (
@ -105,7 +105,7 @@ export const isPathFavs = (pathname?: string) =>
export const isTagHidden = (tag: string) => tag.toLowerCase() === TAG_HIDDEN; 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) { if (hiddenPhotosCount > 0) {
return tags return tags
.filter(({ tag }) => tag === TAG_FAVS) .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) sortTagsObjectWithoutFavs(tags)
.map(({ tag, count }) => ({ .map(({ tag, count }) => ({
value: tag, value: tag,