Merge pull request #120 from sambecker/grid-home
Offer configuration to enable grid-based homepage
This commit is contained in:
commit
a9bab4a8d8
@ -99,6 +99,7 @@ _⚠️ READ BEFORE PROCEEDING_
|
||||
Application behavior can be changed by configuring the following environment variables:
|
||||
|
||||
- `NEXT_PUBLIC_PRO_MODE = 1` enables higher quality image storage (results in increased storage usage)
|
||||
- `NEXT_PUBLIC_GRID_HOMEPAGE = 1` shows grid layout on homepage
|
||||
- `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PAGES = 1` enables static optimization for pages, i.e., renders pages at build time (results in increased project usage)—⚠️ _Experimental_
|
||||
- `NEXT_PUBLIC_STATICALLY_OPTIMIZE_OG_IMAGES = 1` enables static optimization for OG images, i.e., renders images at build time (results in increased project usage)—⚠️ _Experimental_
|
||||
- `NEXT_PUBLIC_MATTE_PHOTOS = 1` constrains the size of each photo, and enables a surrounding border (potentially useful for photos with tall aspect ratios)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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[]>>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
41
src/app/feed/page.tsx
Normal 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 />
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 />
|
||||
);
|
||||
|
||||
@ -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>}
|
||||
</>
|
||||
|
||||
@ -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 { GRID_HOMEPAGE_ENABLED } 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: GRID_HOMEPAGE_ENABLED
|
||||
? 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),
|
||||
...(GRID_HOMEPAGE_ENABLED
|
||||
? 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>
|
||||
? GRID_HOMEPAGE_ENABLED
|
||||
? <PhotoGridPage
|
||||
{...{ photos, photosCount, tags, cameras, simulations }}
|
||||
/>
|
||||
: <PhotoFeedPage
|
||||
{...{ photos, photosCount }}
|
||||
/>
|
||||
: <PhotosEmptyState />
|
||||
);
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
26
src/photo/PhotoFeedPage.tsx
Normal file
26
src/photo/PhotoFeedPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
84
src/photo/PhotoGridContainer.tsx
Normal file
84
src/photo/PhotoGridContainer.tsx
Normal 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
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
}: {
|
||||
cacheKey: string
|
||||
photosCount,
|
||||
tags,
|
||||
cameras,
|
||||
simulations,
|
||||
}:{
|
||||
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,
|
||||
}} />
|
||||
{count > initialOffset &&
|
||||
<PhotoGridInfinite {...{
|
||||
cacheKey,
|
||||
initialOffset,
|
||||
canStart: shouldAnimateDynamicItems,
|
||||
tag,
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
animateOnFirstLoadOnly,
|
||||
}} />}
|
||||
</div>
|
||||
<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,
|
||||
}} />
|
||||
</div>}
|
||||
contentSide={sidebar}
|
||||
sideHiddenOnMobile
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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() : [],
|
||||
|
||||
@ -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),
|
||||
})))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
}: {
|
||||
@ -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 { GRID_HOMEPAGE_ENABLED } 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 GRID_HOMEPAGE_ENABLED ? 'grid' : 'feed';
|
||||
} else if (isPathGrid(pathname)) {
|
||||
return 'grid';
|
||||
} else if (isPathFeed(pathname)) {
|
||||
return 'feed';
|
||||
} else if (isPathProtected(pathname)) {
|
||||
return 'admin';
|
||||
}
|
||||
|
||||
@ -49,6 +49,7 @@ export default function SiteChecklistClient({
|
||||
showFilmSimulations,
|
||||
showExifInfo,
|
||||
isProModeEnabled,
|
||||
isGridHomepageEnabled: isGridFirst,
|
||||
isStaticallyOptimized,
|
||||
arePagesStaticallyOptimized,
|
||||
areOGImagesStaticallyOptimized,
|
||||
@ -357,7 +358,7 @@ export default function SiteChecklistClient({
|
||||
</Checklist>
|
||||
{!simplifiedView && <>
|
||||
<Checklist
|
||||
title="AI Text Generation"
|
||||
title="AI text generation"
|
||||
titleShort="AI"
|
||||
icon={<HiSparkles />}
|
||||
experimental
|
||||
@ -366,7 +367,7 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title={isAiTextGenerationEnabled && isTestingConnections
|
||||
? 'Testing OpenAI connection'
|
||||
: 'Add OpenAI Secret Key'}
|
||||
: 'Add OpenAI secret key'}
|
||||
status={isAiTextGenerationEnabled}
|
||||
isPending={isAiTextGenerationEnabled && isTestingConnections}
|
||||
optional
|
||||
@ -382,7 +383,7 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title={hasVercelKv && isTestingConnections
|
||||
? 'Testing KV connection'
|
||||
: 'Enable Rate Limiting'}
|
||||
: 'Enable rate limiting'}
|
||||
status={hasVercelKv}
|
||||
isPending={hasVercelKv && isTestingConnections}
|
||||
optional
|
||||
@ -425,7 +426,16 @@ export default function SiteChecklistClient({
|
||||
{renderEnvVars(['NEXT_PUBLIC_PRO_MODE'])}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title="Static Optimization"
|
||||
title="Grid homepage"
|
||||
status={isGridFirst}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"1"'} to show grid layout
|
||||
on homepage:
|
||||
{renderEnvVars(['NEXT_PUBLIC_GRID_HOMEPAGE'])}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title="Static optimization"
|
||||
status={isStaticallyOptimized}
|
||||
optional
|
||||
experimental
|
||||
@ -444,7 +454,7 @@ export default function SiteChecklistClient({
|
||||
)}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title="Photo Matting"
|
||||
title="Photo matting"
|
||||
status={arePhotosMatted}
|
||||
optional
|
||||
>
|
||||
@ -454,7 +464,7 @@ export default function SiteChecklistClient({
|
||||
{renderEnvVars(['NEXT_PUBLIC_MATTE_PHOTOS'])}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title="Image Blur"
|
||||
title="Image blur"
|
||||
status={isBlurEnabled}
|
||||
optional
|
||||
>
|
||||
|
||||
@ -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 { GRID_HOMEPAGE_ENABLED } 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={GRID_HOMEPAGE_ENABLED ? PATH_FEED : PATH_ROOT}
|
||||
active={currentSelection === 'feed'}
|
||||
noPadding
|
||||
/>;
|
||||
|
||||
const renderItemGrid = () =>
|
||||
<SwitcherItem
|
||||
icon={<IconGrid />}
|
||||
href={GRID_HOMEPAGE_ENABLED ? 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
|
||||
/>
|
||||
{GRID_HOMEPAGE_ENABLED ? renderItemGrid() : renderItemFeed()}
|
||||
{GRID_HOMEPAGE_ENABLED ? renderItemFeed() : renderItemGrid()}
|
||||
{showAdmin &&
|
||||
<SwitcherItem
|
||||
icon={<BiLockAlt size={16} className="translate-y-[-0.5px]" />}
|
||||
|
||||
@ -116,6 +116,8 @@ export const CURRENT_STORAGE: StorageType =
|
||||
|
||||
export const PRO_MODE_ENABLED =
|
||||
process.env.NEXT_PUBLIC_PRO_MODE === '1';
|
||||
export const GRID_HOMEPAGE_ENABLED =
|
||||
process.env.NEXT_PUBLIC_GRID_HOMEPAGE === '1';
|
||||
export const STATICALLY_OPTIMIZED_PAGES =
|
||||
process.env.NEXT_PUBLIC_STATICALLY_OPTIMIZE_PAGES === '1';
|
||||
export const STATICALLY_OPTIMIZED_OG_IMAGES =
|
||||
@ -183,6 +185,7 @@ export const CONFIG_CHECKLIST_STATUS = {
|
||||
showFilmSimulations: SHOW_FILM_SIMULATIONS,
|
||||
showExifInfo: SHOW_EXIF_DATA,
|
||||
isProModeEnabled: PRO_MODE_ENABLED,
|
||||
isGridHomepageEnabled: GRID_HOMEPAGE_ENABLED,
|
||||
isStaticallyOptimized: (
|
||||
STATICALLY_OPTIMIZED_PAGES ||
|
||||
STATICALLY_OPTIMIZED_OG_IMAGES
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user