Merge pull request #160 from sambecker/share-modal
Move share modals to internal app state
This commit is contained in:
commit
ee913206d7
@ -99,7 +99,7 @@ Application behavior can be changed by configuring the following environment var
|
||||
#### Site meta
|
||||
- `NEXT_PUBLIC_SITE_TITLE` (seen in browser tab)
|
||||
- `NEXT_PUBLIC_SITE_DESCRIPTION` (seen in nav, beneath title)
|
||||
- `NEXT_PUBLIC_SITE_ABOUT` (seen in grid sidebar—accepted rich formatting tags: `<b>`, `<strong>`, `<i>`, `<em>`, `<u>`, `<br>`)
|
||||
- `NEXT_PUBLIC_SITE_ABOUT` (seen in grid sidebar—accepts rich formatting tags: `<b>`, `<strong>`, `<i>`, `<em>`, `<u>`, `<br>`)
|
||||
|
||||
#### Site behavior
|
||||
- `NEXT_PUBLIC_GRID_HOMEPAGE = 1` shows grid layout on homepage
|
||||
|
||||
@ -4,23 +4,14 @@ import {
|
||||
getPathComponents,
|
||||
isPathCamera,
|
||||
isPathCameraPhoto,
|
||||
isPathCameraPhotoShare,
|
||||
isPathCameraShare,
|
||||
isPathFilmSimulation,
|
||||
isPathFilmSimulationPhoto,
|
||||
isPathFilmSimulationPhotoShare,
|
||||
isPathFilmSimulationShare,
|
||||
isPathFocalLength,
|
||||
isPathFocalLengthPhoto,
|
||||
isPathFocalLengthPhotoShare,
|
||||
isPathFocalLengthShare,
|
||||
isPathPhoto,
|
||||
isPathPhotoShare,
|
||||
isPathProtected,
|
||||
isPathTag,
|
||||
isPathTagPhoto,
|
||||
isPathTagPhotoShare,
|
||||
isPathTagShare,
|
||||
} from '@/site/paths';
|
||||
import { TAG_HIDDEN } from '@/tag';
|
||||
|
||||
@ -32,7 +23,6 @@ const CAMERA_OBJECT = { make: CAMERA_MAKE, model: CAMERA_MODEL };
|
||||
const FILM_SIMULATION = 'acros';
|
||||
const FOCAL_LENGTH = 90;
|
||||
const FOCAL_LENGTH_STRING = `${FOCAL_LENGTH}mm`;
|
||||
const SHARE = 'share';
|
||||
|
||||
const PATH_ROOT = '/';
|
||||
const PATH_GRID = '/grid';
|
||||
@ -43,32 +33,21 @@ const PATH_OG_ALL = `${PATH_OG}/all`;
|
||||
const PATH_OG_SAMPLE = `${PATH_OG}/sample`;
|
||||
|
||||
const PATH_PHOTO = `/p/${PHOTO_ID}`;
|
||||
const PATH_PHOTO_SHARE = `${PATH_PHOTO}/${SHARE}`;
|
||||
|
||||
const PATH_TAG = `/tag/${TAG}`;
|
||||
const PATH_TAG_SHARE = `${PATH_TAG}/${SHARE}`;
|
||||
const PATH_TAG_PHOTO = `${PATH_TAG}/${PHOTO_ID}`;
|
||||
const PATH_TAG_PHOTO_SHARE = `${PATH_TAG_PHOTO}/${SHARE}`;
|
||||
|
||||
const PATH_TAG_HIDDEN = `/tag/${TAG_HIDDEN}`;
|
||||
const PATH_TAG_HIDDEN_SHARE = `${PATH_TAG_HIDDEN}/${SHARE}`;
|
||||
const PATH_TAG_HIDDEN_PHOTO = `${PATH_TAG_HIDDEN}/${PHOTO_ID}`;
|
||||
const PATH_TAG_HIDDEN_PHOTO_SHARE = `${PATH_TAG_HIDDEN_PHOTO}/${SHARE}`;
|
||||
|
||||
const PATH_CAMERA = `/shot-on/${CAMERA_MAKE}/${CAMERA_MODEL}`;
|
||||
const PATH_CAMERA_SHARE = `${PATH_CAMERA}/${SHARE}`;
|
||||
const PATH_CAMERA_PHOTO = `${PATH_CAMERA}/${PHOTO_ID}`;
|
||||
const PATH_CAMERA_PHOTO_SHARE = `${PATH_CAMERA_PHOTO}/${SHARE}`;
|
||||
|
||||
const PATH_FILM_SIMULATION = `/film/${FILM_SIMULATION}`;
|
||||
const PATH_FILM_SIMULATION_SHARE = `${PATH_FILM_SIMULATION}/${SHARE}`;
|
||||
const PATH_FILM_SIMULATION_PHOTO = `${PATH_FILM_SIMULATION}/${PHOTO_ID}`;
|
||||
const PATH_FILM_SIMULATION_PHOTO_SHARE = `${PATH_FILM_SIMULATION_PHOTO}/${SHARE}`;
|
||||
|
||||
const PATH_FOCAL_LENGTH = `/focal/${FOCAL_LENGTH_STRING}`;
|
||||
const PATH_FOCAL_LENGTH_SHARE = `${PATH_FOCAL_LENGTH}/${SHARE}`;
|
||||
const PATH_FOCAL_LENGTH_PHOTO = `${PATH_FOCAL_LENGTH}/${PHOTO_ID}`;
|
||||
const PATH_FOCAL_LENGTH_PHOTO_SHARE = `${PATH_FOCAL_LENGTH_PHOTO}/${SHARE}`;
|
||||
|
||||
describe('Paths', () => {
|
||||
it('can be protected', () => {
|
||||
@ -85,49 +64,22 @@ describe('Paths', () => {
|
||||
expect(isPathProtected(PATH_OG_ALL)).toBe(true);
|
||||
expect(isPathProtected(PATH_OG_SAMPLE)).toBe(true);
|
||||
expect(isPathProtected(PATH_TAG_HIDDEN)).toBe(true);
|
||||
expect(isPathProtected(PATH_TAG_HIDDEN_SHARE)).toBe(true);
|
||||
expect(isPathProtected(PATH_TAG_HIDDEN_PHOTO)).toBe(true);
|
||||
expect(isPathProtected(PATH_TAG_HIDDEN_PHOTO_SHARE)).toBe(true);
|
||||
});
|
||||
it('can be classified', () => {
|
||||
// Positive
|
||||
expect(isPathPhoto(PATH_PHOTO)).toBe(true);
|
||||
expect(isPathPhotoShare(PATH_PHOTO_SHARE)).toBe(true);
|
||||
expect(isPathTag(PATH_TAG)).toBe(true);
|
||||
expect(isPathTagShare(PATH_TAG_SHARE)).toBe(true);
|
||||
expect(isPathTagPhoto(PATH_TAG_PHOTO)).toBe(true);
|
||||
expect(isPathTagPhotoShare(PATH_TAG_PHOTO_SHARE)).toBe(true);
|
||||
expect(isPathCamera(PATH_CAMERA)).toBe(true);
|
||||
expect(isPathCameraShare(PATH_CAMERA_SHARE)).toBe(true);
|
||||
expect(isPathCameraPhoto(PATH_CAMERA_PHOTO)).toBe(true);
|
||||
expect(isPathCameraPhotoShare(PATH_CAMERA_PHOTO_SHARE)).toBe(true);
|
||||
expect(isPathFilmSimulation(PATH_FILM_SIMULATION)).toBe(true);
|
||||
expect(isPathFilmSimulationShare(PATH_FILM_SIMULATION_SHARE)).toBe(true);
|
||||
expect(isPathFilmSimulationPhoto(PATH_FILM_SIMULATION_PHOTO)).toBe(true);
|
||||
expect(isPathFilmSimulationPhotoShare(PATH_FILM_SIMULATION_PHOTO_SHARE)).toBe(true);
|
||||
expect(isPathFocalLength(PATH_FOCAL_LENGTH)).toBe(true);
|
||||
expect(isPathFocalLengthShare(PATH_FOCAL_LENGTH_SHARE)).toBe(true);
|
||||
expect(isPathFocalLengthPhoto(PATH_FOCAL_LENGTH_PHOTO)).toBe(true);
|
||||
expect(isPathFocalLengthPhotoShare(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toBe(true);
|
||||
// Negative
|
||||
expect(isPathPhoto(PATH_TAG_PHOTO_SHARE)).toBe(false);
|
||||
expect(isPathPhotoShare(PATH_TAG_PHOTO)).toBe(false);
|
||||
expect(isPathTag(PATH_TAG_SHARE)).toBe(false);
|
||||
expect(isPathTagShare(PATH_TAG)).toBe(false);
|
||||
expect(isPathTagPhoto(PATH_PHOTO_SHARE)).toBe(false);
|
||||
expect(isPathTagPhotoShare(PATH_PHOTO)).toBe(false);
|
||||
expect(isPathCamera(PATH_TAG_SHARE)).toBe(false);
|
||||
expect(isPathCameraShare(PATH_TAG)).toBe(false);
|
||||
expect(isPathCameraPhoto(PATH_PHOTO_SHARE)).toBe(false);
|
||||
expect(isPathCameraPhotoShare(PATH_PHOTO)).toBe(false);
|
||||
expect(isPathFilmSimulation(PATH_TAG_SHARE)).toBe(false);
|
||||
expect(isPathFilmSimulationShare(PATH_TAG)).toBe(false);
|
||||
expect(isPathFilmSimulationPhoto(PATH_PHOTO_SHARE)).toBe(false);
|
||||
expect(isPathFilmSimulationPhotoShare(PATH_PHOTO)).toBe(false);
|
||||
expect(isPathFocalLength(PATH_FILM_SIMULATION)).toBe(false);
|
||||
expect(isPathFocalLengthShare(PATH_FILM_SIMULATION_SHARE)).toBe(false);
|
||||
expect(isPathFocalLengthPhoto(PATH_FILM_SIMULATION_PHOTO)).toBe(false);
|
||||
expect(isPathFocalLengthPhotoShare(PATH_FILM_SIMULATION_PHOTO_SHARE)).toBe(false);
|
||||
});
|
||||
it('can be parsed', () => {
|
||||
// Core
|
||||
@ -135,69 +87,38 @@ describe('Paths', () => {
|
||||
expect(getPathComponents(PATH_PHOTO)).toEqual({
|
||||
photoId: PHOTO_ID,
|
||||
});
|
||||
expect(getPathComponents(PATH_PHOTO_SHARE)).toEqual({
|
||||
photoId: PHOTO_ID,
|
||||
});
|
||||
// Tag
|
||||
expect(getPathComponents(PATH_TAG)).toEqual({
|
||||
tag: TAG,
|
||||
});
|
||||
expect(getPathComponents(PATH_TAG_SHARE)).toEqual({
|
||||
tag: TAG,
|
||||
});
|
||||
expect(getPathComponents(PATH_TAG_PHOTO)).toEqual({
|
||||
photoId: PHOTO_ID,
|
||||
tag: TAG,
|
||||
});
|
||||
expect(getPathComponents(PATH_TAG_PHOTO_SHARE)).toEqual({
|
||||
photoId: PHOTO_ID,
|
||||
tag: TAG,
|
||||
});
|
||||
// Camera
|
||||
expect(getPathComponents(PATH_CAMERA)).toEqual({
|
||||
camera: CAMERA_OBJECT,
|
||||
});
|
||||
expect(getPathComponents(PATH_CAMERA_SHARE)).toEqual({
|
||||
camera: CAMERA_OBJECT,
|
||||
});
|
||||
expect(getPathComponents(PATH_CAMERA_PHOTO)).toEqual({
|
||||
photoId: PHOTO_ID,
|
||||
camera: CAMERA_OBJECT,
|
||||
});
|
||||
expect(getPathComponents(PATH_CAMERA_PHOTO_SHARE)).toEqual({
|
||||
photoId: PHOTO_ID,
|
||||
camera: CAMERA_OBJECT,
|
||||
});
|
||||
// Film Simulation
|
||||
expect(getPathComponents(PATH_FILM_SIMULATION)).toEqual({
|
||||
simulation: FILM_SIMULATION,
|
||||
});
|
||||
expect(getPathComponents(PATH_FILM_SIMULATION_SHARE)).toEqual({
|
||||
simulation: FILM_SIMULATION,
|
||||
});
|
||||
expect(getPathComponents(PATH_FILM_SIMULATION_PHOTO)).toEqual({
|
||||
photoId: PHOTO_ID,
|
||||
simulation: FILM_SIMULATION,
|
||||
});
|
||||
expect(getPathComponents(PATH_FILM_SIMULATION_PHOTO_SHARE)).toEqual({
|
||||
photoId: PHOTO_ID,
|
||||
simulation: FILM_SIMULATION,
|
||||
});
|
||||
// Focal Length
|
||||
expect(getPathComponents(PATH_FOCAL_LENGTH)).toEqual({
|
||||
focal: FOCAL_LENGTH,
|
||||
});
|
||||
expect(getPathComponents(PATH_FOCAL_LENGTH_SHARE)).toEqual({
|
||||
focal: FOCAL_LENGTH,
|
||||
});
|
||||
expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO)).toEqual({
|
||||
photoId: PHOTO_ID,
|
||||
focal: FOCAL_LENGTH,
|
||||
});
|
||||
expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toEqual({
|
||||
photoId: PHOTO_ID,
|
||||
focal: FOCAL_LENGTH,
|
||||
});
|
||||
});
|
||||
it('can be escaped', () => {
|
||||
// Root
|
||||
@ -207,26 +128,17 @@ describe('Paths', () => {
|
||||
expect(getEscapePath(PATH_ADMIN)).toEqual(undefined);
|
||||
// Photo
|
||||
expect(getEscapePath(PATH_PHOTO)).toEqual(PATH_ROOT);
|
||||
expect(getEscapePath(PATH_PHOTO_SHARE)).toEqual(PATH_PHOTO);
|
||||
// Tag
|
||||
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_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_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_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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,7 +14,7 @@ const eslintConfig = [
|
||||
rules: {
|
||||
'@next/next/no-img-element': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'no-unused-expressions': 'warn',
|
||||
'no-unused-expressions': ['warn'],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn', {
|
||||
'argsIgnorePattern': '^_',
|
||||
|
||||
24
package.json
24
package.json
@ -9,9 +9,9 @@
|
||||
"analyze": "ANALYZE=true next build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^1.0.13",
|
||||
"@aws-sdk/client-s3": "3.722.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.722.0",
|
||||
"@ai-sdk/openai": "^1.0.18",
|
||||
"@aws-sdk/client-s3": "3.726.1",
|
||||
"@aws-sdk/s3-request-presigner": "3.726.1",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||
"@radix-ui/react-visually-hidden": "^1.1.1",
|
||||
@ -20,14 +20,14 @@
|
||||
"@vercel/blob": "^0.27.0",
|
||||
"@vercel/kv": "^3.0.0",
|
||||
"@vercel/speed-insights": "^1.1.0",
|
||||
"ai": "^4.0.26",
|
||||
"ai": "^4.0.33",
|
||||
"camelcase-keys": "^9.1.3",
|
||||
"cmdk": "^1.0.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"framer-motion": "^11.15.0",
|
||||
"framer-motion": "^11.17.0",
|
||||
"nanoid": "^5.0.9",
|
||||
"next": "15.1.3",
|
||||
"next": "15.1.4",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"next-themes": "^0.4.4",
|
||||
"pg": "^8.13.1",
|
||||
@ -42,25 +42,25 @@
|
||||
"use-debounce": "^10.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "15.1.3",
|
||||
"@next/bundle-analyzer": "15.1.4",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/pg": "^8.11.10",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/react": "19.0.4",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "9.17.0",
|
||||
"eslint-config-next": "15.1.3",
|
||||
"eslint": "9.18.0",
|
||||
"eslint-config-next": "15.1.4",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"postcss": "8.4.49",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.7.2"
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
2305
pnpm-lock.yaml
generated
2305
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,91 +0,0 @@
|
||||
import {
|
||||
RELATED_GRID_PHOTOS_TO_SHOW,
|
||||
descriptionForPhoto,
|
||||
titleForPhoto,
|
||||
} from '@/photo';
|
||||
import { Metadata } from 'next/types';
|
||||
import { redirect } from 'next/navigation';
|
||||
import {
|
||||
PATH_ROOT,
|
||||
absolutePathForPhoto,
|
||||
absolutePathForPhotoImage,
|
||||
} from '@/site/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import { ReactNode, cache } from 'react';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import {
|
||||
getPhotosMetaCached,
|
||||
getPhotosNearIdCached,
|
||||
} from '@/photo/cache';
|
||||
|
||||
const getPhotosNearIdCachedCached = cache((
|
||||
photoId: string,
|
||||
simulation: FilmSimulation,
|
||||
) =>
|
||||
getPhotosNearIdCached(
|
||||
photoId,
|
||||
{ simulation, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 },
|
||||
));
|
||||
|
||||
interface PhotoFilmSimulationProps {
|
||||
params: Promise<{ photoId: string, simulation: FilmSimulation }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PhotoFilmSimulationProps): Promise<Metadata> {
|
||||
const { photoId, simulation } = await params;
|
||||
|
||||
const { photo } = await getPhotosNearIdCachedCached(photoId, simulation);
|
||||
|
||||
if (!photo) { return {}; }
|
||||
|
||||
const title = titleForPhoto(photo);
|
||||
const description = descriptionForPhoto(photo);
|
||||
const images = absolutePathForPhotoImage(photo);
|
||||
const url = absolutePathForPhoto({ photo, simulation });
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
images,
|
||||
description,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PhotoFilmSimulationPage({
|
||||
params,
|
||||
children,
|
||||
}: PhotoFilmSimulationProps & { children: ReactNode }) {
|
||||
const { photoId, simulation } = await params;
|
||||
|
||||
const { photo, photos, photosGrid, indexNumber } =
|
||||
await getPhotosNearIdCachedCached(photoId, simulation);
|
||||
|
||||
if (!photo) { redirect(PATH_ROOT); }
|
||||
|
||||
const { count, dateRange } = await getPhotosMetaCached({ simulation });
|
||||
|
||||
return <>
|
||||
{children}
|
||||
<PhotoDetailPage {...{
|
||||
photo,
|
||||
photos,
|
||||
photosGrid,
|
||||
simulation,
|
||||
indexNumber,
|
||||
count,
|
||||
dateRange,
|
||||
}} />
|
||||
</>;
|
||||
}
|
||||
@ -1,3 +1,89 @@
|
||||
export default function Page() {
|
||||
return null;
|
||||
import {
|
||||
RELATED_GRID_PHOTOS_TO_SHOW,
|
||||
descriptionForPhoto,
|
||||
titleForPhoto,
|
||||
} from '@/photo';
|
||||
import { Metadata } from 'next/types';
|
||||
import { redirect } from 'next/navigation';
|
||||
import {
|
||||
PATH_ROOT,
|
||||
absolutePathForPhoto,
|
||||
absolutePathForPhotoImage,
|
||||
} from '@/site/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import {
|
||||
getPhotosMetaCached,
|
||||
getPhotosNearIdCached,
|
||||
} from '@/photo/cache';
|
||||
import { cache } from 'react';
|
||||
|
||||
const getPhotosNearIdCachedCached = cache((
|
||||
photoId: string,
|
||||
simulation: FilmSimulation,
|
||||
) =>
|
||||
getPhotosNearIdCached(
|
||||
photoId,
|
||||
{ simulation, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 },
|
||||
));
|
||||
|
||||
interface PhotoFilmSimulationProps {
|
||||
params: Promise<{ photoId: string, simulation: FilmSimulation }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PhotoFilmSimulationProps): Promise<Metadata> {
|
||||
const { photoId, simulation } = await params;
|
||||
|
||||
const { photo } = await getPhotosNearIdCachedCached(photoId, simulation);
|
||||
|
||||
if (!photo) { return {}; }
|
||||
|
||||
const title = titleForPhoto(photo);
|
||||
const description = descriptionForPhoto(photo);
|
||||
const images = absolutePathForPhotoImage(photo);
|
||||
const url = absolutePathForPhoto({ photo, simulation });
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
images,
|
||||
description,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PhotoFilmSimulationPage({
|
||||
params,
|
||||
}: PhotoFilmSimulationProps) {
|
||||
const { photoId, simulation } = await params;
|
||||
|
||||
const { photo, photos, photosGrid, indexNumber } =
|
||||
await getPhotosNearIdCachedCached(photoId, simulation);
|
||||
|
||||
if (!photo) { redirect(PATH_ROOT); }
|
||||
|
||||
const { count, dateRange } = await getPhotosMetaCached({ simulation });
|
||||
|
||||
return (
|
||||
<PhotoDetailPage {...{
|
||||
photo,
|
||||
photos,
|
||||
photosGrid,
|
||||
simulation,
|
||||
indexNumber,
|
||||
count,
|
||||
dateRange,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
import { getPhotoCached } from '@/photo/cache';
|
||||
import PhotoShareModal from '@/photo/PhotoShareModal';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import { PATH_ROOT } from '@/site/paths';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function Share({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ photoId: string, simulation: FilmSimulation }>
|
||||
}) {
|
||||
const { photoId, simulation } = await params;
|
||||
|
||||
const photo = await getPhotoCached(photoId);
|
||||
|
||||
if (!photo) { return redirect(PATH_ROOT); }
|
||||
|
||||
return <PhotoShareModal {...{ photo, simulation }} />;
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
|
||||
import { FilmSimulation, generateMetaForFilmSimulation } from '@/simulation';
|
||||
import FilmSimulationOverview from '@/simulation/FilmSimulationOverview';
|
||||
import FilmSimulationShareModal from '@/simulation/FilmSimulationShareModal';
|
||||
import { getPhotosFilmSimulationDataCached } from '@/simulation/data';
|
||||
import { Metadata } from 'next/types';
|
||||
import { cache } from 'react';
|
||||
|
||||
const getPhotosFilmSimulationDataCachedCached =
|
||||
cache((simulation: FilmSimulation) => getPhotosFilmSimulationDataCached({
|
||||
simulation,
|
||||
limit: INFINITE_SCROLL_GRID_INITIAL,
|
||||
}));
|
||||
|
||||
interface FilmSimulationProps {
|
||||
params: Promise<{ simulation: FilmSimulation }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: FilmSimulationProps): Promise<Metadata> {
|
||||
const { simulation } = await params;
|
||||
|
||||
const [
|
||||
photos,
|
||||
{ count, dateRange },
|
||||
] = await getPhotosFilmSimulationDataCachedCached(simulation);
|
||||
|
||||
const {
|
||||
url,
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
} = generateMetaForFilmSimulation(simulation, photos, count, dateRange);
|
||||
|
||||
return {
|
||||
title,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
images,
|
||||
description,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
description,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Share({
|
||||
params,
|
||||
}: FilmSimulationProps) {
|
||||
const { simulation } = await params;
|
||||
|
||||
const [
|
||||
photos,
|
||||
{ count, dateRange },
|
||||
] = await getPhotosFilmSimulationDataCachedCached(simulation);
|
||||
|
||||
return <>
|
||||
<FilmSimulationShareModal {...{ simulation, photos, count, dateRange }} />
|
||||
<FilmSimulationOverview
|
||||
{...{ simulation, photos, count, dateRange }}
|
||||
animateOnFirstLoadOnly
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
import {
|
||||
RELATED_GRID_PHOTOS_TO_SHOW,
|
||||
descriptionForPhoto,
|
||||
titleForPhoto,
|
||||
} from '@/photo';
|
||||
import { Metadata } from 'next/types';
|
||||
import { redirect } from 'next/navigation';
|
||||
import {
|
||||
PATH_ROOT,
|
||||
absolutePathForPhoto,
|
||||
absolutePathForPhotoImage,
|
||||
} from '@/site/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import { getPhotosNearIdCached } from '@/photo/cache';
|
||||
import { ReactNode, cache } from 'react';
|
||||
import { getPhotosMeta } from '@/photo/db/query';
|
||||
import { getFocalLengthFromString } from '@/focal';
|
||||
|
||||
const getPhotosNearIdCachedCached = cache((photoId: string, focal: number) =>
|
||||
getPhotosNearIdCached(
|
||||
photoId,
|
||||
{ focal, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 },
|
||||
));
|
||||
|
||||
interface PhotoFocalLengthProps {
|
||||
params: Promise<{ photoId: string, focal: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PhotoFocalLengthProps): Promise<Metadata> {
|
||||
const { photoId, focal: focalString } = await params;
|
||||
|
||||
const focal = getFocalLengthFromString(focalString);
|
||||
|
||||
const { photo } = await getPhotosNearIdCachedCached(photoId, focal);
|
||||
|
||||
if (!photo) { return {}; }
|
||||
|
||||
const title = titleForPhoto(photo);
|
||||
const description = descriptionForPhoto(photo);
|
||||
const images = absolutePathForPhotoImage(photo);
|
||||
const url = absolutePathForPhoto({ photo, focal });
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
images,
|
||||
description,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PhotoFocalLengthPage({
|
||||
params,
|
||||
children,
|
||||
}: PhotoFocalLengthProps & { children: ReactNode }) {
|
||||
const { photoId, focal: focalString } = await params;
|
||||
|
||||
const focal = getFocalLengthFromString(focalString);
|
||||
|
||||
const { photo, photos, photosGrid, indexNumber } =
|
||||
await getPhotosNearIdCachedCached(photoId, focal);
|
||||
|
||||
if (!photo) { redirect(PATH_ROOT); }
|
||||
|
||||
const { count, dateRange } = await getPhotosMeta({ focal });
|
||||
|
||||
return <>
|
||||
{children}
|
||||
<PhotoDetailPage {...{
|
||||
photo,
|
||||
photos,
|
||||
photosGrid,
|
||||
focal,
|
||||
indexNumber,
|
||||
count,
|
||||
dateRange,
|
||||
}} />
|
||||
</>;
|
||||
}
|
||||
@ -1,3 +1,88 @@
|
||||
export default function Page() {
|
||||
return null;
|
||||
import {
|
||||
RELATED_GRID_PHOTOS_TO_SHOW,
|
||||
descriptionForPhoto,
|
||||
titleForPhoto,
|
||||
} from '@/photo';
|
||||
import { Metadata } from 'next/types';
|
||||
import { redirect } from 'next/navigation';
|
||||
import {
|
||||
PATH_ROOT,
|
||||
absolutePathForPhoto,
|
||||
absolutePathForPhotoImage,
|
||||
} from '@/site/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import { getPhotosNearIdCached } from '@/photo/cache';
|
||||
import { cache } from 'react';
|
||||
import { getPhotosMeta } from '@/photo/db/query';
|
||||
import { getFocalLengthFromString } from '@/focal';
|
||||
|
||||
const getPhotosNearIdCachedCached = cache((photoId: string, focal: number) =>
|
||||
getPhotosNearIdCached(
|
||||
photoId,
|
||||
{ focal, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 },
|
||||
));
|
||||
|
||||
interface PhotoFocalLengthProps {
|
||||
params: Promise<{ photoId: string, focal: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PhotoFocalLengthProps): Promise<Metadata> {
|
||||
const { photoId, focal: focalString } = await params;
|
||||
|
||||
const focal = getFocalLengthFromString(focalString);
|
||||
|
||||
const { photo } = await getPhotosNearIdCachedCached(photoId, focal);
|
||||
|
||||
if (!photo) { return {}; }
|
||||
|
||||
const title = titleForPhoto(photo);
|
||||
const description = descriptionForPhoto(photo);
|
||||
const images = absolutePathForPhotoImage(photo);
|
||||
const url = absolutePathForPhoto({ photo, focal });
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
images,
|
||||
description,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PhotoFocalLengthPage({
|
||||
params,
|
||||
}: PhotoFocalLengthProps) {
|
||||
const { photoId, focal: focalString } = await params;
|
||||
|
||||
const focal = getFocalLengthFromString(focalString);
|
||||
|
||||
const { photo, photos, photosGrid, indexNumber } =
|
||||
await getPhotosNearIdCachedCached(photoId, focal);
|
||||
|
||||
if (!photo) { redirect(PATH_ROOT); }
|
||||
|
||||
const { count, dateRange } = await getPhotosMeta({ focal });
|
||||
|
||||
return (
|
||||
<PhotoDetailPage {...{
|
||||
photo,
|
||||
photos,
|
||||
photosGrid,
|
||||
focal,
|
||||
indexNumber,
|
||||
count,
|
||||
dateRange,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import { getFocalLengthFromString } from '@/focal';
|
||||
import { getPhotoCached } from '@/photo/cache';
|
||||
import PhotoShareModal from '@/photo/PhotoShareModal';
|
||||
import { PATH_ROOT } from '@/site/paths';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function Share({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ photoId: string, focal: string }>
|
||||
}) {
|
||||
const { photoId, focal: focalString } = await params;
|
||||
|
||||
const focal = getFocalLengthFromString(focalString);
|
||||
|
||||
const photo = await getPhotoCached(photoId);
|
||||
|
||||
if (!photo) { return redirect(PATH_ROOT); }
|
||||
|
||||
return <PhotoShareModal {...{ photo, focal }} />;
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
import { generateMetaForFocalLength, getFocalLengthFromString } from '@/focal';
|
||||
import FocalLengthOverview from '@/focal/FocalLengthOverview';
|
||||
import FocalLengthShareModal from '@/focal/FocalLengthShareModal';
|
||||
import { getPhotosFocalLengthDataCached } from '@/focal/data';
|
||||
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_INITIAL,
|
||||
}));
|
||||
|
||||
interface FocalLengthProps {
|
||||
params: Promise<{ focal: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: FocalLengthProps): Promise<Metadata> {
|
||||
const { focal: focalString } = await params;
|
||||
|
||||
const focal = getFocalLengthFromString(focalString);
|
||||
|
||||
const [
|
||||
photos,
|
||||
{ count, dateRange },
|
||||
] = await getPhotosFocalLengthDataCachedCached(focal);
|
||||
|
||||
const {
|
||||
url,
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
} = generateMetaForFocalLength(focal, photos, count, dateRange);
|
||||
|
||||
return {
|
||||
title,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
images,
|
||||
description,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
description,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Share({
|
||||
params,
|
||||
}: FocalLengthProps) {
|
||||
const { focal: focalString } = await params;
|
||||
|
||||
const focal = getFocalLengthFromString(focalString);
|
||||
|
||||
const [
|
||||
photos,
|
||||
{ count, dateRange },
|
||||
] = await getPhotosFocalLengthDataCachedCached(focal);
|
||||
|
||||
return <>
|
||||
<FocalLengthShareModal {...{ focal, photos, count, dateRange }} />
|
||||
<FocalLengthOverview
|
||||
{...{ focal, photos, count, dateRange }}
|
||||
animateOnFirstLoadOnly
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
@ -19,6 +19,7 @@ import Footer from '@/site/Footer';
|
||||
import CommandK from '@/site/CommandK';
|
||||
import SwrConfigClient from '../state/SwrConfigClient';
|
||||
import AdminBatchEditPanel from '@/admin/AdminBatchEditPanel';
|
||||
import ShareModals from '@/share/ShareModals';
|
||||
|
||||
import '../site/globals.css';
|
||||
import '../site/sonner.css';
|
||||
@ -97,6 +98,7 @@ export default function RootLayout({
|
||||
'min-h-[16rem] sm:min-h-[30rem]',
|
||||
'mb-12',
|
||||
)}>
|
||||
<ShareModals />
|
||||
{children}
|
||||
</div>
|
||||
<Footer />
|
||||
|
||||
@ -1,84 +0,0 @@
|
||||
import {
|
||||
RELATED_GRID_PHOTOS_TO_SHOW,
|
||||
descriptionForPhoto,
|
||||
titleForPhoto,
|
||||
} from '@/photo';
|
||||
import { Metadata } from 'next/types';
|
||||
import { redirect } from 'next/navigation';
|
||||
import {
|
||||
PATH_ROOT,
|
||||
absolutePathForPhoto,
|
||||
absolutePathForPhotoImage,
|
||||
} from '@/site/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import { getPhotosNearIdCached } from '@/photo/cache';
|
||||
import { IS_PRODUCTION, STATICALLY_OPTIMIZED_PAGES } from '@/site/config';
|
||||
import { getPhotoIds } from '@/photo/db/query';
|
||||
import { GENERATE_STATIC_PARAMS_LIMIT } from '@/photo/db';
|
||||
import { ReactNode, cache } from 'react';
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
const getPhotosNearIdCachedCached = cache((photoId: string) =>
|
||||
getPhotosNearIdCached(photoId, { limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 }));
|
||||
|
||||
export let generateStaticParams:
|
||||
(() => Promise<{ photoId: string }[]>) | undefined = undefined;
|
||||
|
||||
if (STATICALLY_OPTIMIZED_PAGES && IS_PRODUCTION) {
|
||||
generateStaticParams = async () => {
|
||||
const photos = await getPhotoIds({ limit: GENERATE_STATIC_PARAMS_LIMIT });
|
||||
return photos.map(photoId => ({ photoId }));
|
||||
};
|
||||
}
|
||||
|
||||
interface PhotoProps {
|
||||
params: Promise<{ photoId: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}:PhotoProps): Promise<Metadata> {
|
||||
const { photoId } = await params;
|
||||
const { photo } = await getPhotosNearIdCachedCached(photoId);
|
||||
|
||||
if (!photo) { return {}; }
|
||||
|
||||
const title = titleForPhoto(photo);
|
||||
const description = descriptionForPhoto(photo);
|
||||
const images = absolutePathForPhotoImage(photo);
|
||||
const url = absolutePathForPhoto({ photo });
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
images,
|
||||
description,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PhotoPage({
|
||||
params,
|
||||
children,
|
||||
}: PhotoProps & { children: ReactNode }) {
|
||||
const { photoId } = await params;
|
||||
const { photo, photos, photosGrid } =
|
||||
await getPhotosNearIdCachedCached(photoId);
|
||||
|
||||
if (!photo) { redirect(PATH_ROOT); }
|
||||
|
||||
return <>
|
||||
{children}
|
||||
<PhotoDetailPage {...{ photo, photos, photosGrid }} />
|
||||
</>;
|
||||
}
|
||||
@ -1,3 +1,82 @@
|
||||
export default function Page() {
|
||||
return null;
|
||||
import {
|
||||
RELATED_GRID_PHOTOS_TO_SHOW,
|
||||
descriptionForPhoto,
|
||||
titleForPhoto,
|
||||
} from '@/photo';
|
||||
import { Metadata } from 'next/types';
|
||||
import { redirect } from 'next/navigation';
|
||||
import {
|
||||
PATH_ROOT,
|
||||
absolutePathForPhoto,
|
||||
absolutePathForPhotoImage,
|
||||
} from '@/site/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import { getPhotosNearIdCached } from '@/photo/cache';
|
||||
import { IS_PRODUCTION, STATICALLY_OPTIMIZED_PAGES } from '@/site/config';
|
||||
import { getPhotoIds } from '@/photo/db/query';
|
||||
import { GENERATE_STATIC_PARAMS_LIMIT } from '@/photo/db';
|
||||
import { cache } from 'react';
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
const getPhotosNearIdCachedCached = cache((photoId: string) =>
|
||||
getPhotosNearIdCached(photoId, { limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 }));
|
||||
|
||||
export let generateStaticParams:
|
||||
(() => Promise<{ photoId: string }[]>) | undefined = undefined;
|
||||
|
||||
if (STATICALLY_OPTIMIZED_PAGES && IS_PRODUCTION) {
|
||||
generateStaticParams = async () => {
|
||||
const photos = await getPhotoIds({ limit: GENERATE_STATIC_PARAMS_LIMIT });
|
||||
return photos.map(photoId => ({ photoId }));
|
||||
};
|
||||
}
|
||||
|
||||
interface PhotoProps {
|
||||
params: Promise<{ photoId: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}:PhotoProps): Promise<Metadata> {
|
||||
const { photoId } = await params;
|
||||
const { photo } = await getPhotosNearIdCachedCached(photoId);
|
||||
|
||||
if (!photo) { return {}; }
|
||||
|
||||
const title = titleForPhoto(photo);
|
||||
const description = descriptionForPhoto(photo);
|
||||
const images = absolutePathForPhotoImage(photo);
|
||||
const url = absolutePathForPhoto({ photo });
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
images,
|
||||
description,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PhotoPage({
|
||||
params,
|
||||
}: PhotoProps) {
|
||||
const { photoId } = await params;
|
||||
const { photo, photos, photosGrid } =
|
||||
await getPhotosNearIdCachedCached(photoId);
|
||||
|
||||
if (!photo) { redirect(PATH_ROOT); }
|
||||
|
||||
return (
|
||||
<PhotoDetailPage {...{ photo, photos, photosGrid }} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
import { getPhotoCached } from '@/photo/cache';
|
||||
import PhotoShareModal from '@/photo/PhotoShareModal';
|
||||
import { PATH_ROOT } from '@/site/paths';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function Share({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ photoId: string }>
|
||||
}) {
|
||||
const { photoId } = await params;
|
||||
|
||||
const photo = await getPhotoCached(photoId);
|
||||
|
||||
if (!photo) { return redirect(PATH_ROOT); }
|
||||
|
||||
return <PhotoShareModal photo={photo} />;
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
import {
|
||||
RELATED_GRID_PHOTOS_TO_SHOW,
|
||||
descriptionForPhoto,
|
||||
titleForPhoto,
|
||||
} from '@/photo';
|
||||
import { Metadata } from 'next/types';
|
||||
import { redirect } from 'next/navigation';
|
||||
import {
|
||||
PATH_ROOT,
|
||||
absolutePathForPhoto,
|
||||
absolutePathForPhotoImage,
|
||||
} from '@/site/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import {
|
||||
getPhotosMetaCached,
|
||||
getPhotosNearIdCached,
|
||||
} from '@/photo/cache';
|
||||
import {
|
||||
PhotoCameraProps,
|
||||
cameraFromPhoto,
|
||||
getCameraFromParams,
|
||||
} from '@/camera';
|
||||
import { ReactNode, cache } from 'react';
|
||||
|
||||
const getPhotosNearIdCachedCached = cache((
|
||||
photoId: string,
|
||||
make: string,
|
||||
model: string,
|
||||
) =>
|
||||
getPhotosNearIdCached(
|
||||
photoId, {
|
||||
camera: getCameraFromParams({ make, model }),
|
||||
limit: RELATED_GRID_PHOTOS_TO_SHOW + 2,
|
||||
},
|
||||
));
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PhotoCameraProps): Promise<Metadata> {
|
||||
const { photoId, make, model } = await params;
|
||||
|
||||
const { photo } = await getPhotosNearIdCachedCached(photoId, make, model);
|
||||
|
||||
if (!photo) { return {}; }
|
||||
|
||||
const title = titleForPhoto(photo);
|
||||
const description = descriptionForPhoto(photo);
|
||||
const images = absolutePathForPhotoImage(photo);
|
||||
const url = absolutePathForPhoto({
|
||||
photo,
|
||||
camera: cameraFromPhoto(photo, { make, model }),
|
||||
});
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
images,
|
||||
description,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PhotoCameraPage({
|
||||
params,
|
||||
children,
|
||||
}: PhotoCameraProps & { children: ReactNode }) {
|
||||
const { photoId, make, model } = await params;
|
||||
|
||||
const { photo, photos, photosGrid, indexNumber } =
|
||||
await getPhotosNearIdCachedCached(photoId, make, model);
|
||||
|
||||
if (!photo) { redirect(PATH_ROOT); }
|
||||
|
||||
const camera = cameraFromPhoto(photo, { make, model });
|
||||
|
||||
const { count, dateRange } = await getPhotosMetaCached({ camera });
|
||||
|
||||
return <>
|
||||
{children}
|
||||
<PhotoDetailPage {...{
|
||||
photo,
|
||||
photos,
|
||||
photosGrid,
|
||||
camera,
|
||||
indexNumber,
|
||||
count,
|
||||
dateRange,
|
||||
}} />
|
||||
</>;
|
||||
}
|
||||
@ -1,3 +1,97 @@
|
||||
export default function Page() {
|
||||
return null;
|
||||
import {
|
||||
RELATED_GRID_PHOTOS_TO_SHOW,
|
||||
descriptionForPhoto,
|
||||
titleForPhoto,
|
||||
} from '@/photo';
|
||||
import { Metadata } from 'next/types';
|
||||
import { redirect } from 'next/navigation';
|
||||
import {
|
||||
PATH_ROOT,
|
||||
absolutePathForPhoto,
|
||||
absolutePathForPhotoImage,
|
||||
} from '@/site/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import {
|
||||
getPhotosMetaCached,
|
||||
getPhotosNearIdCached,
|
||||
} from '@/photo/cache';
|
||||
import {
|
||||
PhotoCameraProps,
|
||||
cameraFromPhoto,
|
||||
getCameraFromParams,
|
||||
} from '@/camera';
|
||||
import { cache } from 'react';
|
||||
|
||||
const getPhotosNearIdCachedCached = cache((
|
||||
photoId: string,
|
||||
make: string,
|
||||
model: string,
|
||||
) =>
|
||||
getPhotosNearIdCached(
|
||||
photoId, {
|
||||
camera: getCameraFromParams({ make, model }),
|
||||
limit: RELATED_GRID_PHOTOS_TO_SHOW + 2,
|
||||
},
|
||||
));
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PhotoCameraProps): Promise<Metadata> {
|
||||
const { photoId, make, model } = await params;
|
||||
|
||||
const { photo } = await getPhotosNearIdCachedCached(photoId, make, model);
|
||||
|
||||
if (!photo) { return {}; }
|
||||
|
||||
const title = titleForPhoto(photo);
|
||||
const description = descriptionForPhoto(photo);
|
||||
const images = absolutePathForPhotoImage(photo);
|
||||
const url = absolutePathForPhoto({
|
||||
photo,
|
||||
camera: cameraFromPhoto(photo, { make, model }),
|
||||
});
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
images,
|
||||
description,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PhotoCameraPage({
|
||||
params,
|
||||
}: PhotoCameraProps) {
|
||||
const { photoId, make, model } = await params;
|
||||
|
||||
const { photo, photos, photosGrid, indexNumber } =
|
||||
await getPhotosNearIdCachedCached(photoId, make, model);
|
||||
|
||||
if (!photo) { redirect(PATH_ROOT); }
|
||||
|
||||
const camera = cameraFromPhoto(photo, { make, model });
|
||||
|
||||
const { count, dateRange } = await getPhotosMetaCached({ camera });
|
||||
|
||||
return (
|
||||
<PhotoDetailPage {...{
|
||||
photo,
|
||||
photos,
|
||||
photosGrid,
|
||||
camera,
|
||||
indexNumber,
|
||||
count,
|
||||
dateRange,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
import { getPhotoCached } from '@/photo/cache';
|
||||
import { PhotoCameraProps, cameraFromPhoto } from '@/camera';
|
||||
import PhotoShareModal from '@/photo/PhotoShareModal';
|
||||
import { PATH_ROOT } from '@/site/paths';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function Share({
|
||||
params,
|
||||
}: PhotoCameraProps) {
|
||||
const { photoId, make, model } = await params;
|
||||
|
||||
const photo = await getPhotoCached(photoId);
|
||||
|
||||
if (!photo) { return redirect(PATH_ROOT); }
|
||||
|
||||
const camera = cameraFromPhoto(photo, { make, model });
|
||||
|
||||
return <PhotoShareModal {...{ photo, camera }} />;
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
import { CameraProps } from '@/camera';
|
||||
import CameraShareModal from '@/camera/CameraShareModal';
|
||||
import { generateMetaForCamera } from '@/camera/meta';
|
||||
import { Metadata } from 'next/types';
|
||||
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
|
||||
import { getPhotosCameraDataCached } from '@/camera/data';
|
||||
import CameraOverview from '@/camera/CameraOverview';
|
||||
import { cache } from 'react';
|
||||
|
||||
const getPhotosCameraDataCachedCached = cache((
|
||||
make: string,
|
||||
model: string,
|
||||
) => getPhotosCameraDataCached(
|
||||
make,
|
||||
model,
|
||||
INFINITE_SCROLL_GRID_INITIAL,
|
||||
));
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: CameraProps): Promise<Metadata> {
|
||||
const { make, model } = await params;
|
||||
|
||||
const [
|
||||
photos,
|
||||
{ count, dateRange },
|
||||
camera,
|
||||
] = await getPhotosCameraDataCachedCached(make, model);
|
||||
|
||||
const {
|
||||
url,
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
} = generateMetaForCamera(camera, photos, count, dateRange);
|
||||
|
||||
return {
|
||||
title,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
images,
|
||||
description,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
description,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Share({ params }: CameraProps) {
|
||||
const { make, model } = await params;
|
||||
|
||||
const [
|
||||
photos,
|
||||
{ count, dateRange },
|
||||
camera,
|
||||
] = await getPhotosCameraDataCachedCached(make, model);
|
||||
|
||||
return <>
|
||||
<CameraShareModal {...{ camera, photos, count, dateRange }} />
|
||||
<CameraOverview
|
||||
{...{ camera, photos, count, dateRange }}
|
||||
animateOnFirstLoadOnly
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
import {
|
||||
RELATED_GRID_PHOTOS_TO_SHOW,
|
||||
descriptionForPhoto,
|
||||
titleForPhoto,
|
||||
} from '@/photo';
|
||||
import { Metadata } from 'next/types';
|
||||
import { redirect } from 'next/navigation';
|
||||
import {
|
||||
PATH_ROOT,
|
||||
absolutePathForPhoto,
|
||||
absolutePathForPhotoImage,
|
||||
} from '@/site/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import { getPhotosNearIdCached } from '@/photo/cache';
|
||||
import { ReactNode, cache } from 'react';
|
||||
import { getPhotosMeta } from '@/photo/db/query';
|
||||
|
||||
const getPhotosNearIdCachedCached = cache((photoId: string, tag: string) =>
|
||||
getPhotosNearIdCached(
|
||||
photoId,
|
||||
{ tag, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 },
|
||||
));
|
||||
|
||||
interface PhotoTagProps {
|
||||
params: Promise<{ photoId: string, tag: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PhotoTagProps): Promise<Metadata> {
|
||||
const { photoId, tag } = await params;
|
||||
|
||||
const { photo } = await getPhotosNearIdCachedCached(photoId, tag);
|
||||
|
||||
if (!photo) { return {}; }
|
||||
|
||||
const title = titleForPhoto(photo);
|
||||
const description = descriptionForPhoto(photo);
|
||||
const images = absolutePathForPhotoImage(photo);
|
||||
const url = absolutePathForPhoto({ photo, tag });
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
images,
|
||||
description,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PhotoTagPage({
|
||||
params,
|
||||
children,
|
||||
}: PhotoTagProps & { children: ReactNode }) {
|
||||
const { photoId, tag } = await params;
|
||||
const { photo, photos, photosGrid, indexNumber } =
|
||||
await getPhotosNearIdCachedCached(photoId, tag);
|
||||
|
||||
if (!photo) { redirect(PATH_ROOT); }
|
||||
|
||||
const { count, dateRange } = await getPhotosMeta({ tag });
|
||||
|
||||
return <>
|
||||
{children}
|
||||
<PhotoDetailPage {...{
|
||||
photo,
|
||||
photos,
|
||||
photosGrid,
|
||||
tag,
|
||||
indexNumber,
|
||||
count,
|
||||
dateRange,
|
||||
}} />
|
||||
</>;
|
||||
}
|
||||
@ -1,3 +1,82 @@
|
||||
export default function Page() {
|
||||
return null;
|
||||
import {
|
||||
RELATED_GRID_PHOTOS_TO_SHOW,
|
||||
descriptionForPhoto,
|
||||
titleForPhoto,
|
||||
} from '@/photo';
|
||||
import { Metadata } from 'next/types';
|
||||
import { redirect } from 'next/navigation';
|
||||
import {
|
||||
PATH_ROOT,
|
||||
absolutePathForPhoto,
|
||||
absolutePathForPhotoImage,
|
||||
} from '@/site/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import { getPhotosNearIdCached } from '@/photo/cache';
|
||||
import { cache } from 'react';
|
||||
import { getPhotosMeta } from '@/photo/db/query';
|
||||
|
||||
const getPhotosNearIdCachedCached = cache((photoId: string, tag: string) =>
|
||||
getPhotosNearIdCached(
|
||||
photoId,
|
||||
{ tag, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 },
|
||||
));
|
||||
|
||||
interface PhotoTagProps {
|
||||
params: Promise<{ photoId: string, tag: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PhotoTagProps): Promise<Metadata> {
|
||||
const { photoId, tag } = await params;
|
||||
|
||||
const { photo } = await getPhotosNearIdCachedCached(photoId, tag);
|
||||
|
||||
if (!photo) { return {}; }
|
||||
|
||||
const title = titleForPhoto(photo);
|
||||
const description = descriptionForPhoto(photo);
|
||||
const images = absolutePathForPhotoImage(photo);
|
||||
const url = absolutePathForPhoto({ photo, tag });
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
images,
|
||||
description,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PhotoTagPage({
|
||||
params,
|
||||
}: PhotoTagProps) {
|
||||
const { photoId, tag } = await params;
|
||||
const { photo, photos, photosGrid, indexNumber } =
|
||||
await getPhotosNearIdCachedCached(photoId, tag);
|
||||
|
||||
if (!photo) { redirect(PATH_ROOT); }
|
||||
|
||||
const { count, dateRange } = await getPhotosMeta({ tag });
|
||||
|
||||
return (
|
||||
<PhotoDetailPage {...{
|
||||
photo,
|
||||
photos,
|
||||
photosGrid,
|
||||
tag,
|
||||
indexNumber,
|
||||
count,
|
||||
dateRange,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
import { getPhotoCached } from '@/photo/cache';
|
||||
import PhotoShareModal from '@/photo/PhotoShareModal';
|
||||
import { PATH_ROOT } from '@/site/paths';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function Share({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ photoId: string, tag: string }>
|
||||
}) {
|
||||
const { photoId, tag } = await params;
|
||||
|
||||
const photo = await getPhotoCached(photoId);
|
||||
|
||||
if (!photo) { return redirect(PATH_ROOT); }
|
||||
|
||||
return <PhotoShareModal {...{ photo, tag }} />;
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
|
||||
import { generateMetaForTag } from '@/tag';
|
||||
import TagOverview from '@/tag/TagOverview';
|
||||
import TagShareModal from '@/tag/TagShareModal';
|
||||
import { getPhotosTagDataCached } from '@/tag/data';
|
||||
import type { Metadata } from 'next';
|
||||
import { cache } from 'react';
|
||||
|
||||
const getPhotosTagDataCachedCached = cache((tag: string) =>
|
||||
getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_INITIAL }));
|
||||
|
||||
interface TagProps {
|
||||
params: Promise<{ tag: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: TagProps): Promise<Metadata> {
|
||||
const { tag: tagFromParams } = await params;
|
||||
|
||||
const tag = decodeURIComponent(tagFromParams);
|
||||
|
||||
const [
|
||||
photos,
|
||||
{ count, dateRange },
|
||||
] = await getPhotosTagDataCachedCached(tag);
|
||||
|
||||
const {
|
||||
url,
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
} = generateMetaForTag(tag, photos, count, dateRange);
|
||||
|
||||
return {
|
||||
title,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
images,
|
||||
description,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
description,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Share({
|
||||
params,
|
||||
}: TagProps) {
|
||||
const { tag: tagFromParams } = await params;
|
||||
|
||||
const tag = decodeURIComponent(tagFromParams);
|
||||
|
||||
const [
|
||||
photos,
|
||||
{ count, dateRange },
|
||||
] = await getPhotosTagDataCachedCached(tag);
|
||||
|
||||
return <>
|
||||
<TagShareModal {...{ tag, photos, count, dateRange }} />
|
||||
<TagOverview
|
||||
{...{ tag, photos, count, dateRange }}
|
||||
animateOnFirstLoadOnly
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
import { Photo, PhotoDateRange } from '@/photo';
|
||||
import { pathForCameraShare } from '@/site/paths';
|
||||
import PhotoHeader from '@/photo/PhotoHeader';
|
||||
import { Camera, cameraFromPhoto } from '.';
|
||||
import PhotoCamera from './PhotoCamera';
|
||||
@ -29,10 +28,10 @@ export default function CameraHeader({
|
||||
descriptionForCameraPhotos(photos, undefined, count, dateRange)}
|
||||
photos={photos}
|
||||
selectedPhoto={selectedPhoto}
|
||||
sharePath={pathForCameraShare(camera)}
|
||||
indexNumber={indexNumber}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
includeShareButton
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { absolutePathForCamera, pathForCamera } from '@/site/paths';
|
||||
import { Photo, PhotoDateRange } from '../photo';
|
||||
import ShareModal from '@/components/ShareModal';
|
||||
import { absolutePathForCamera } from '@/site/paths';
|
||||
import { PhotoSetAttributes } from '../photo';
|
||||
import ShareModal from '@/share/ShareModal';
|
||||
import CameraOGTile from './CameraOGTile';
|
||||
import { Camera } from '.';
|
||||
import { shareTextForCamera } from './meta';
|
||||
@ -12,14 +12,10 @@ export default function CameraShareModal({
|
||||
dateRange,
|
||||
}: {
|
||||
camera: Camera
|
||||
photos: Photo[]
|
||||
count: number
|
||||
dateRange?: PhotoDateRange,
|
||||
}) {
|
||||
} & PhotoSetAttributes) {
|
||||
return (
|
||||
<ShareModal
|
||||
pathShare={absolutePathForCamera(camera)}
|
||||
pathClose={pathForCamera(camera)}
|
||||
socialText={shareTextForCamera(camera, photos)}
|
||||
>
|
||||
<CameraOGTile {...{ camera, photos, count, dateRange }} />
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
import { TbPhotoShare } from 'react-icons/tb';
|
||||
import PathLoaderButton from './primitives/PathLoaderButton';
|
||||
import { clsx } from 'clsx/lite';
|
||||
|
||||
export default function ShareButton({
|
||||
path,
|
||||
prefetch,
|
||||
shouldScroll,
|
||||
dim,
|
||||
className,
|
||||
}: {
|
||||
path: string
|
||||
prefetch?: boolean
|
||||
shouldScroll?: boolean
|
||||
dim?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<PathLoaderButton
|
||||
path={path}
|
||||
className={clsx(
|
||||
className,
|
||||
dim ? 'text-dim' : 'text-medium',
|
||||
)}
|
||||
icon={<TbPhotoShare size={16} />}
|
||||
spinnerColor="dim"
|
||||
prefetch={prefetch}
|
||||
shouldScroll={shouldScroll}
|
||||
styleAs="link"
|
||||
shouldReplace
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import { Photo, PhotoDateRange } from '@/photo';
|
||||
import { descriptionForFocalLengthPhotos } from '.';
|
||||
import { pathForFocalLengthShare } from '@/site/paths';
|
||||
import PhotoHeader from '@/photo/PhotoHeader';
|
||||
import PhotoFocalLength from './PhotoFocalLength';
|
||||
|
||||
@ -30,10 +29,10 @@ export default function FocalLengthHeader({
|
||||
)}
|
||||
photos={photos}
|
||||
selectedPhoto={selectedPhoto}
|
||||
sharePath={pathForFocalLengthShare(focal)}
|
||||
indexNumber={indexNumber}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
includeShareButton
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { absolutePathForFocalLength, pathForFocalLength } from '@/site/paths';
|
||||
import { Photo, PhotoDateRange } from '../photo';
|
||||
import ShareModal from '@/components/ShareModal';
|
||||
import { absolutePathForFocalLength } from '@/site/paths';
|
||||
import { PhotoSetAttributes } from '../photo';
|
||||
import ShareModal from '@/share/ShareModal';
|
||||
import FocalLengthOGTile from './FocalLengthOGTile';
|
||||
import { shareTextFocalLength } from '.';
|
||||
|
||||
@ -11,14 +11,10 @@ export default function FocalLengthShareModal({
|
||||
dateRange,
|
||||
}: {
|
||||
focal: number
|
||||
photos: Photo[]
|
||||
count?: number
|
||||
dateRange?: PhotoDateRange
|
||||
}) {
|
||||
} & PhotoSetAttributes) {
|
||||
return (
|
||||
<ShareModal
|
||||
pathShare={absolutePathForFocalLength(focal)}
|
||||
pathClose={pathForFocalLength(focal)}
|
||||
socialText={shareTextFocalLength(focal)}
|
||||
>
|
||||
<FocalLengthOGTile {...{ focal, photos, count, dateRange }} />
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import { getPhotosCachedAction, getPhotosAction } from '@/photo/actions';
|
||||
import { Photo, PhotoSetAttributes } from '.';
|
||||
import { Photo, PhotoSetCategory } from '.';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { GetPhotosOptions } from './db';
|
||||
@ -45,7 +45,7 @@ export default function InfinitePhotoScroll({
|
||||
onLastPhotoVisible: () => void
|
||||
revalidatePhoto?: RevalidatePhoto
|
||||
}) => ReactNode
|
||||
} & PhotoSetAttributes) {
|
||||
} & PhotoSetCategory) {
|
||||
const { swrTimestamp, isUserSignedIn } = useAppState();
|
||||
|
||||
const key = `${swrTimestamp}-${cacheKey}`;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import AnimateItems from '@/components/AnimateItems';
|
||||
import { Photo, PhotoDateRange, PhotoSetAttributes } from '.';
|
||||
import { Photo, PhotoDateRange, PhotoSetCategory } from '.';
|
||||
import PhotoLarge from './PhotoLarge';
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import PhotoGrid from './PhotoGrid';
|
||||
@ -34,7 +34,7 @@ export default function PhotoDetailPage({
|
||||
dateRange?: PhotoDateRange
|
||||
shouldShare?: boolean
|
||||
includeFavoriteInAdminMenu?: boolean
|
||||
} & PhotoSetAttributes) {
|
||||
} & PhotoSetCategory) {
|
||||
let customHeader: JSX.Element | undefined;
|
||||
|
||||
if (tag) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Photo, PhotoSetAttributes } from '.';
|
||||
import { Photo, PhotoSetCategory } from '.';
|
||||
import PhotoMedium from './PhotoMedium';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import AnimateItems from '@/components/AnimateItems';
|
||||
@ -41,7 +41,7 @@ export default function PhotoGrid({
|
||||
canSelect?: boolean
|
||||
onLastPhotoVisible?: () => void
|
||||
onAnimationComplete?: () => void
|
||||
} & PhotoSetAttributes) {
|
||||
} & PhotoSetCategory) {
|
||||
const {
|
||||
isUserSignedIn,
|
||||
selectedPhotoIds,
|
||||
|
||||
@ -4,11 +4,11 @@ import { clsx } from 'clsx/lite';
|
||||
import {
|
||||
Photo,
|
||||
PhotoDateRange,
|
||||
PhotoSetAttributes,
|
||||
PhotoSetCategory,
|
||||
dateRangeForPhotos,
|
||||
titleForPhoto,
|
||||
} from '.';
|
||||
import ShareButton from '@/components/ShareButton';
|
||||
import ShareButton from '@/share/ShareButton';
|
||||
import AnimateItems from '@/components/AnimateItems';
|
||||
import { ReactNode } from 'react';
|
||||
import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid';
|
||||
@ -27,21 +27,21 @@ export default function PhotoHeader({
|
||||
entity,
|
||||
entityVerb = 'PHOTO',
|
||||
entityDescription,
|
||||
sharePath,
|
||||
indexNumber,
|
||||
count,
|
||||
dateRange,
|
||||
includeShareButton,
|
||||
}: {
|
||||
photos: Photo[]
|
||||
selectedPhoto?: Photo
|
||||
entity?: ReactNode
|
||||
entityVerb?: string
|
||||
entityDescription?: string
|
||||
sharePath?: string
|
||||
indexNumber?: number
|
||||
count?: number
|
||||
dateRange?: PhotoDateRange
|
||||
} & PhotoSetAttributes) {
|
||||
includeShareButton?: boolean
|
||||
} & PhotoSetCategory) {
|
||||
const { isGridHighDensity } = useAppState();
|
||||
|
||||
const { start, end } = dateRangeForPhotos(photos, dateRange);
|
||||
@ -138,10 +138,17 @@ export default function PhotoHeader({
|
||||
{headerType === 'photo-set'
|
||||
? <>
|
||||
{entityDescription}
|
||||
{sharePath &&
|
||||
{includeShareButton &&
|
||||
<ShareButton
|
||||
photos={photos}
|
||||
tag={tag}
|
||||
camera={camera}
|
||||
simulation={simulation}
|
||||
focal={focal}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
className="translate-y-[1.5px]"
|
||||
path={sharePath}
|
||||
prefetch
|
||||
dim
|
||||
/>}
|
||||
</>
|
||||
|
||||
@ -15,10 +15,9 @@ import Link from 'next/link';
|
||||
import {
|
||||
pathForFocalLength,
|
||||
pathForPhoto,
|
||||
pathForPhotoShare,
|
||||
} from '@/site/paths';
|
||||
import PhotoTags from '@/tag/PhotoTags';
|
||||
import ShareButton from '@/components/ShareButton';
|
||||
import ShareButton from '@/share/ShareButton';
|
||||
import DownloadButton from '@/components/DownloadButton';
|
||||
import PhotoCamera from '../camera/PhotoCamera';
|
||||
import { cameraFromPhoto } from '@/camera';
|
||||
@ -54,7 +53,6 @@ export default function PhotoLarge({
|
||||
shouldShareCamera,
|
||||
shouldShareSimulation,
|
||||
shouldShareFocalLength,
|
||||
shouldScrollOnShare,
|
||||
includeFavoriteInAdminMenu,
|
||||
onVisible,
|
||||
}: {
|
||||
@ -258,17 +256,14 @@ export default function PhotoLarge({
|
||||
)}>
|
||||
{shouldShare &&
|
||||
<ShareButton
|
||||
path={pathForPhotoShare({
|
||||
photo,
|
||||
tag: shouldShareTag ? primaryTag : undefined,
|
||||
camera: shouldShareCamera ? camera : undefined,
|
||||
// eslint-disable-next-line max-len
|
||||
simulation: shouldShareSimulation ? photo.filmSimulation : undefined,
|
||||
// eslint-disable-next-line max-len
|
||||
focal: shouldShareFocalLength ? photo.focalLength : undefined,
|
||||
})}
|
||||
photo={photo}
|
||||
tag={shouldShareTag ? primaryTag : undefined}
|
||||
camera={shouldShareCamera ? camera : undefined}
|
||||
// eslint-disable-next-line max-len
|
||||
simulation={shouldShareSimulation? photo.filmSimulation : undefined}
|
||||
// eslint-disable-next-line max-len
|
||||
focal={shouldShareFocalLength ? photo.focalLength : undefined}
|
||||
prefetch={prefetchRelatedLinks}
|
||||
shouldScroll={shouldScrollOnShare}
|
||||
/>}
|
||||
{ALLOW_PUBLIC_DOWNLOADS &&
|
||||
<DownloadButton
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { Photo, PhotoSetAttributes, titleForPhoto } from '@/photo';
|
||||
import { Photo, PhotoSetCategory, titleForPhoto } from '@/photo';
|
||||
import Link from 'next/link';
|
||||
import { AnimationConfig } from '../components/AnimateItems';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
@ -26,7 +26,7 @@ export default function PhotoLink({
|
||||
nextPhotoAnimation?: AnimationConfig
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
} & PhotoSetAttributes) {
|
||||
} & PhotoSetCategory) {
|
||||
const { setNextPhotoAnimation } = useAppState();
|
||||
|
||||
return (
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import {
|
||||
Photo,
|
||||
PhotoSetAttributes,
|
||||
PhotoSetCategory,
|
||||
altTextForPhoto,
|
||||
doesPhotoNeedBlurCompatibility,
|
||||
} from '.';
|
||||
@ -32,7 +32,7 @@ export default function PhotoMedium({
|
||||
prefetch?: boolean
|
||||
className?: string
|
||||
onVisible?: () => void
|
||||
} & PhotoSetAttributes) {
|
||||
} & PhotoSetCategory) {
|
||||
const ref = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
useOnVisible(ref, onVisible);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
Photo,
|
||||
PhotoSetAttributes,
|
||||
PhotoSetCategory,
|
||||
getNextPhoto,
|
||||
getPreviousPhoto,
|
||||
} from '@/photo';
|
||||
@ -32,7 +32,7 @@ export default function PhotoPrevNext({
|
||||
photo?: Photo
|
||||
photos?: Photo[]
|
||||
className?: string
|
||||
} & PhotoSetAttributes) {
|
||||
} & PhotoSetCategory) {
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import PhotoOGTile from '@/photo/PhotoOGTile';
|
||||
import { absolutePathForPhoto, pathForPhoto } from '@/site/paths';
|
||||
import { Photo, PhotoSetAttributes } from '.';
|
||||
import ShareModal from '@/components/ShareModal';
|
||||
import { absolutePathForPhoto } from '@/site/paths';
|
||||
import { Photo, PhotoSetCategory } from '.';
|
||||
import ShareModal from '@/share/ShareModal';
|
||||
|
||||
export default function PhotoShareModal(props: {
|
||||
photo: Photo
|
||||
} & PhotoSetAttributes) {
|
||||
} & PhotoSetCategory) {
|
||||
return (
|
||||
<ShareModal
|
||||
pathShare={absolutePathForPhoto(props)}
|
||||
pathClose={pathForPhoto(props)}
|
||||
socialText="Check out this photo"
|
||||
>
|
||||
<PhotoOGTile photo={props.photo} />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {
|
||||
Photo,
|
||||
PhotoSetAttributes,
|
||||
PhotoSetCategory,
|
||||
altTextForPhoto,
|
||||
doesPhotoNeedBlurCompatibility,
|
||||
} from '.';
|
||||
@ -28,7 +28,7 @@ export default function PhotoSmall({
|
||||
className?: string
|
||||
prefetch?: boolean
|
||||
onVisible?: () => void
|
||||
} & PhotoSetAttributes) {
|
||||
} & PhotoSetCategory) {
|
||||
const ref = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
useOnVisible(ref, onVisible);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { PRIORITY_ORDER_ENABLED } from '@/site/config';
|
||||
import { parameterize } from '@/utility/string';
|
||||
import { PhotoSetAttributes } from '..';
|
||||
import { PhotoSetCategory } from '..';
|
||||
|
||||
export const GENERATE_STATIC_PARAMS_LIMIT = 1000;
|
||||
export const PHOTO_DEFAULT_LIMIT = 100;
|
||||
@ -14,7 +14,7 @@ export type GetPhotosOptions = {
|
||||
takenAfterInclusive?: Date
|
||||
updatedBefore?: Date
|
||||
hidden?: 'exclude' | 'include' | 'only'
|
||||
} & PhotoSetAttributes;
|
||||
} & PhotoSetCategory;
|
||||
|
||||
export const areOptionsSensitive = (options: GetPhotosOptions) =>
|
||||
options.hidden === 'include' || options.hidden === 'only';
|
||||
|
||||
@ -102,7 +102,7 @@ export interface Photo extends PhotoDb {
|
||||
takenAtNaiveFormatted: string
|
||||
}
|
||||
|
||||
export interface PhotoSetAttributes {
|
||||
export interface PhotoSetCategory {
|
||||
tag?: string
|
||||
camera?: Camera
|
||||
simulation?: FilmSimulation
|
||||
@ -110,6 +110,12 @@ export interface PhotoSetAttributes {
|
||||
lens?: Lens // Unimplemented as a set
|
||||
}
|
||||
|
||||
export interface PhotoSetAttributes {
|
||||
photos: Photo[]
|
||||
count?: number
|
||||
dateRange?: PhotoDateRange
|
||||
}
|
||||
|
||||
export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
|
||||
const photoDb = camelcaseKeys(
|
||||
photoDbRaw as unknown as Record<string, unknown>,
|
||||
|
||||
48
src/share/ShareButton.tsx
Normal file
48
src/share/ShareButton.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import { TbPhotoShare } from 'react-icons/tb';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { getSharePathFromShareModalProps, ShareModalProps } from '.';
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
let prefetchedImage: HTMLImageElement | null = null;
|
||||
|
||||
export default function ShareButton({
|
||||
dim,
|
||||
prefetch,
|
||||
className,
|
||||
...rest
|
||||
}: {
|
||||
dim?: boolean
|
||||
prefetch?: boolean
|
||||
className?: string
|
||||
} & ShareModalProps) {
|
||||
const { setShareModalProps } = useAppState();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const absoluteImagePath = getSharePathFromShareModalProps({ ...rest });
|
||||
|
||||
useEffect(() => {
|
||||
if (prefetch && absoluteImagePath) {
|
||||
prefetchedImage = new Image();
|
||||
prefetchedImage.src = absoluteImagePath;
|
||||
}
|
||||
}, [prefetch, absoluteImagePath, router]);
|
||||
|
||||
return (
|
||||
<LoaderButton
|
||||
onClick={() => setShareModalProps?.({ ...rest })}
|
||||
className={clsx(
|
||||
className,
|
||||
dim ? 'text-dim' : 'text-medium',
|
||||
)}
|
||||
icon={<TbPhotoShare size={16} />}
|
||||
spinnerColor="dim"
|
||||
styleAs="link"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -10,20 +10,21 @@ import { toastSuccess } from '@/toast';
|
||||
import { PiXLogo } from 'react-icons/pi';
|
||||
import { SHOW_SOCIAL } from '@/site/config';
|
||||
import { generateXPostText } from '@/utility/social';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
export default function ShareModal({
|
||||
title,
|
||||
pathShare,
|
||||
pathClose,
|
||||
socialText,
|
||||
children,
|
||||
}: {
|
||||
title?: string
|
||||
pathShare: string
|
||||
pathClose: string
|
||||
socialText: string
|
||||
children: ReactNode
|
||||
}) {
|
||||
const { setShareModalProps } = useAppState();
|
||||
|
||||
const renderIcon = (
|
||||
icon: JSX.Element,
|
||||
action: () => void,
|
||||
@ -44,7 +45,7 @@ export default function ShareModal({
|
||||
</div>;
|
||||
|
||||
return (
|
||||
<Modal onClosePath={pathClose}>
|
||||
<Modal onClose={() => setShareModalProps?.(undefined)}>
|
||||
<div className="space-y-3 md:space-y-4 w-full">
|
||||
{title &&
|
||||
<div className={clsx(
|
||||
38
src/share/ShareModals.tsx
Normal file
38
src/share/ShareModals.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import PhotoShareModal from '@/photo/PhotoShareModal';
|
||||
import TagShareModal from '@/tag/TagShareModal';
|
||||
import CameraShareModal from '@/camera/CameraShareModal';
|
||||
import FilmSimulationShareModal from '@/simulation/FilmSimulationShareModal';
|
||||
import FocalLengthShareModal from '@/focal/FocalLengthShareModal';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
export default function ShareModals() {
|
||||
const { shareModalProps = {} } = useAppState();
|
||||
|
||||
const {
|
||||
photo,
|
||||
photos,
|
||||
count,
|
||||
dateRange,
|
||||
tag,
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
} = shareModalProps;
|
||||
|
||||
if (photo) {
|
||||
return <PhotoShareModal {...{photo, tag, camera, simulation, focal}} />;
|
||||
} else if (photos) {
|
||||
const attributes = {photos, count, dateRange};
|
||||
if (tag) {
|
||||
return <TagShareModal {...{tag, ...attributes}} />;
|
||||
} else if (camera) {
|
||||
return <CameraShareModal {...{camera, ...attributes}} />;
|
||||
} else if (simulation) {
|
||||
return <FilmSimulationShareModal {...{simulation, ...attributes}} />;
|
||||
} else if (focal !== undefined) {
|
||||
return <FocalLengthShareModal {...{focal, ...attributes}} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/share/index.ts
Normal file
37
src/share/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { Photo, PhotoSetAttributes, PhotoSetCategory } from '@/photo';
|
||||
import {
|
||||
absolutePathForCameraImage,
|
||||
absolutePathForFilmSimulationImage,
|
||||
absolutePathForFocalLengthImage,
|
||||
absolutePathForPhotoImage,
|
||||
absolutePathForTagImage,
|
||||
} from '@/site/paths';
|
||||
|
||||
export type ShareModalProps = Omit<PhotoSetAttributes, 'photos'> & {
|
||||
photo?: Photo
|
||||
photos?: Photo[]
|
||||
} & PhotoSetCategory;
|
||||
|
||||
export const getSharePathFromShareModalProps = ({
|
||||
photo,
|
||||
tag,
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
}: ShareModalProps) => {
|
||||
if (photo) {
|
||||
return absolutePathForPhotoImage(photo);
|
||||
}
|
||||
if (tag) {
|
||||
return absolutePathForTagImage(tag);
|
||||
}
|
||||
if (camera) {
|
||||
return absolutePathForCameraImage(camera);
|
||||
}
|
||||
if (simulation) {
|
||||
return absolutePathForFilmSimulationImage(simulation);
|
||||
}
|
||||
if (focal) {
|
||||
return absolutePathForFocalLengthImage(focal);
|
||||
}
|
||||
};
|
||||
@ -1,6 +1,5 @@
|
||||
import { Photo, PhotoDateRange } from '@/photo';
|
||||
import { FilmSimulation, descriptionForFilmSimulationPhotos } from '.';
|
||||
import { pathForFilmSimulationShare } from '@/site/paths';
|
||||
import PhotoHeader from '@/photo/PhotoHeader';
|
||||
import PhotoFilmSimulation from
|
||||
'@/simulation/PhotoFilmSimulation';
|
||||
@ -28,10 +27,10 @@ export default function FilmSimulationHeader({
|
||||
photos, undefined, count, dateRange)}
|
||||
photos={photos}
|
||||
selectedPhoto={selectedPhoto}
|
||||
sharePath={pathForFilmSimulationShare(simulation)}
|
||||
indexNumber={indexNumber}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
includeShareButton
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
import {
|
||||
absolutePathForFilmSimulation,
|
||||
pathForFilmSimulation,
|
||||
} from '@/site/paths';
|
||||
import { Photo, PhotoDateRange } from '../photo';
|
||||
import ShareModal from '@/components/ShareModal';
|
||||
import { absolutePathForFilmSimulation } from '@/site/paths';
|
||||
import { PhotoSetAttributes } from '../photo';
|
||||
import ShareModal from '@/share/ShareModal';
|
||||
import FilmSimulationOGTile from './FilmSimulationOGTile';
|
||||
import { FilmSimulation, shareTextForFilmSimulation } from '.';
|
||||
|
||||
@ -14,14 +11,10 @@ export default function FilmSimulationShareModal({
|
||||
dateRange,
|
||||
}: {
|
||||
simulation: FilmSimulation
|
||||
photos: Photo[]
|
||||
count?: number
|
||||
dateRange?: PhotoDateRange
|
||||
}) {
|
||||
} & PhotoSetAttributes) {
|
||||
return (
|
||||
<ShareModal
|
||||
pathShare={absolutePathForFilmSimulation(simulation)}
|
||||
pathClose={pathForFilmSimulation(simulation)}
|
||||
socialText={shareTextForFilmSimulation(simulation)}
|
||||
>
|
||||
<FilmSimulationOGTile {...{ simulation, photos, count, dateRange }} />
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Photo, PhotoSetAttributes } from '@/photo';
|
||||
import { Photo, PhotoSetCategory } from '@/photo';
|
||||
import { BASE_URL, GRID_HOMEPAGE_ENABLED } from './config';
|
||||
import { Camera } from '@/camera';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
@ -51,7 +51,6 @@ export const PATH_API_VERCEL_BLOB_UPLOAD = `${PATH_API_STORAGE}/vercel-blob`;
|
||||
export const PATH_API_PRESIGNED_URL = `${PATH_API_STORAGE}/presigned-url`;
|
||||
|
||||
// Modifiers
|
||||
const SHARE = 'share';
|
||||
const EDIT = 'edit';
|
||||
|
||||
export const PATHS_ADMIN = [
|
||||
@ -75,7 +74,7 @@ export const PATHS_TO_CACHE = [
|
||||
...PATHS_ADMIN,
|
||||
];
|
||||
|
||||
type PhotoPathParams = { photo: PhotoOrPhotoId } & PhotoSetAttributes;
|
||||
type PhotoPathParams = { photo: PhotoOrPhotoId } & PhotoSetCategory;
|
||||
|
||||
// Absolute paths
|
||||
export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`;
|
||||
@ -113,33 +112,18 @@ export const pathForPhoto = ({
|
||||
? `${pathForFocalLength(focal)}/${getPhotoId(photo)}`
|
||||
: `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
|
||||
|
||||
export const pathForPhotoShare = (params: PhotoPathParams) =>
|
||||
`${pathForPhoto(params)}/${SHARE}`;
|
||||
|
||||
export const pathForTag = (tag: string) =>
|
||||
`${PREFIX_TAG}/${tag}`;
|
||||
|
||||
export const pathForTagShare = (tag: string) =>
|
||||
`${pathForTag(tag)}/${SHARE}`;
|
||||
|
||||
export const pathForCamera = ({ make, model }: Camera) =>
|
||||
`${PREFIX_CAMERA}/${parameterize(make, true)}/${parameterize(model, true)}`;
|
||||
|
||||
export const pathForCameraShare = (camera: Camera) =>
|
||||
`${pathForCamera(camera)}/${SHARE}`;
|
||||
|
||||
export const pathForFilmSimulation = (simulation: FilmSimulation) =>
|
||||
`${PREFIX_FILM_SIMULATION}/${simulation}`;
|
||||
|
||||
export const pathForFilmSimulationShare = (simulation: FilmSimulation) =>
|
||||
`${pathForFilmSimulation(simulation)}/${SHARE}`;
|
||||
|
||||
export const pathForFocalLength = (focal: number) =>
|
||||
`${PREFIX_FOCAL_LENGTH}/${focal}mm`;
|
||||
|
||||
export const pathForFocalLengthShare = (focal: number) =>
|
||||
`${pathForFocalLength(focal)}/${SHARE}`;;
|
||||
|
||||
export const absolutePathForPhoto = (params: PhotoPathParams) =>
|
||||
`${BASE_URL}${pathForPhoto(params)}`;
|
||||
|
||||
@ -176,76 +160,38 @@ export const absolutePathForFocalLengthImage =
|
||||
export const isPathPhoto = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_PHOTO}/[^/]+/?$`).test(pathname);
|
||||
|
||||
// p/[photoId]/share
|
||||
export const isPathPhotoShare = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_PHOTO}/[^/]+/${SHARE}/?$`).test(pathname);
|
||||
|
||||
// tag/[tag]
|
||||
export const isPathTag = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_TAG}/[^/]+/?$`).test(pathname);
|
||||
|
||||
// tag/[tag]/share
|
||||
export const isPathTagShare = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_TAG}/[^/]+/${SHARE}/?$`).test(pathname);
|
||||
new RegExp(`^${PREFIX_TAG}/[^/]+/?$`).test(pathname);;
|
||||
|
||||
// tag/[tag]/[photoId]
|
||||
export const isPathTagPhoto = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_TAG}/[^/]+/[^/]+/?$`).test(pathname);
|
||||
|
||||
// tag/[tag]/[photoId]/share
|
||||
export const isPathTagPhotoShare = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_TAG}/[^/]+/[^/]+/${SHARE}/?$`).test(pathname);
|
||||
|
||||
// shot-on/[make]/[model]
|
||||
export const isPathCamera = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/?$`).test(pathname);
|
||||
|
||||
// shot-on/[make]/[model]/share
|
||||
export const isPathCameraShare = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/${SHARE}/?$`).test(pathname);
|
||||
|
||||
// shot-on/[make]/[model]/[photoId]
|
||||
export const isPathCameraPhoto = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/[^/]+/?$`).test(pathname);
|
||||
|
||||
// shot-on/[make]/[model]/[photoId]/share
|
||||
export const isPathCameraPhotoShare = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/[^/]+/${SHARE}/?$`).test(pathname);
|
||||
|
||||
// film/[simulation]
|
||||
export const isPathFilmSimulation = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/?$`).test(pathname);
|
||||
|
||||
// film/[simulation]/share
|
||||
export const isPathFilmSimulationShare = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/${SHARE}/?$`).test(pathname);
|
||||
|
||||
// film/[simulation]/[photoId]
|
||||
export const isPathFilmSimulationPhoto = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/[^/]+/?$`).test(pathname);
|
||||
|
||||
// film/[simulation]/[photoId]/share
|
||||
export const isPathFilmSimulationPhotoShare = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/[^/]+/${SHARE}/?$`)
|
||||
.test(pathname);
|
||||
|
||||
// focal/[focal]
|
||||
export const isPathFocalLength = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[^/]+/?$`).test(pathname);
|
||||
|
||||
// focal/[focal]/share
|
||||
export const isPathFocalLengthShare = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[^/]+/${SHARE}/?$`).test(pathname);
|
||||
|
||||
// focal/[focal]/[photoId]
|
||||
export const isPathFocalLengthPhoto = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[^/]+/[^/]+/?$`).test(pathname);
|
||||
|
||||
// focal/[focal]/[photoId]/share
|
||||
export const isPathFocalLengthPhotoShare = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[^/]+/[^/]+/${SHARE}/?$`)
|
||||
.test(pathname);
|
||||
|
||||
export const checkPathPrefix = (pathname = '', prefix: string) =>
|
||||
pathname.toLowerCase().startsWith(prefix);
|
||||
|
||||
@ -274,17 +220,17 @@ export const isPathProtected = (pathname?: string) =>
|
||||
|
||||
export const getPathComponents = (pathname = ''): {
|
||||
photoId?: string
|
||||
} & PhotoSetAttributes => {
|
||||
} & PhotoSetCategory => {
|
||||
const photoIdFromPhoto = pathname.match(
|
||||
new RegExp(`^${PREFIX_PHOTO}/([^/]+)`))?.[1];
|
||||
const photoIdFromTag = pathname.match(
|
||||
new RegExp(`^${PREFIX_TAG}/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
|
||||
new RegExp(`^${PREFIX_TAG}/[^/]+/([^/]+)`))?.[1];
|
||||
const photoIdFromCamera = pathname.match(
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/([^/]+)`))?.[1];
|
||||
const photoIdFromFilmSimulation = pathname.match(
|
||||
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
|
||||
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/([^/]+)`))?.[1];
|
||||
const photoIdFromFocalLength = pathname.match(
|
||||
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[0-9]+mm/((?!${SHARE})[^/]+)`))?.[1];
|
||||
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[0-9]+mm/([^/]+)`))?.[1];
|
||||
const tag = pathname.match(
|
||||
new RegExp(`^${PREFIX_TAG}/([^/]+)`))?.[1];
|
||||
const cameraMake = pathname.match(
|
||||
@ -334,35 +280,13 @@ export const getEscapePath = (pathname?: string) => {
|
||||
(focal && isPathFocalLength(pathname))
|
||||
) {
|
||||
return PATH_ROOT;
|
||||
} else if (photoId && isPathTagPhotoShare(pathname)) {
|
||||
return pathForPhoto({ photo: photoId, tag });
|
||||
} else if (photoId && isPathCameraPhotoShare(pathname)) {
|
||||
return pathForPhoto({ photo: photoId, camera });
|
||||
} else if (photoId && isPathFilmSimulationPhotoShare(pathname)) {
|
||||
return pathForPhoto({ photo: photoId, simulation });
|
||||
} else if (photoId && isPathFocalLengthPhotoShare(pathname)) {
|
||||
return pathForPhoto({ photo: photoId, focal });
|
||||
} else if (photoId && isPathPhotoShare(pathname)) {
|
||||
return pathForPhoto({ photo: photoId });
|
||||
} else if (tag && (
|
||||
isPathTagPhoto(pathname) ||
|
||||
isPathTagShare(pathname)
|
||||
)) {
|
||||
} else if (tag && isPathTagPhoto(pathname)) {
|
||||
return pathForTag(tag);
|
||||
} else if (camera && (
|
||||
isPathCameraPhoto(pathname) ||
|
||||
isPathCameraShare(pathname)
|
||||
)) {
|
||||
} else if (camera && isPathCameraPhoto(pathname)) {
|
||||
return pathForCamera(camera);
|
||||
} else if (simulation && (
|
||||
isPathFilmSimulationPhoto(pathname) ||
|
||||
isPathFilmSimulationShare(pathname)
|
||||
)) {
|
||||
} else if (simulation && isPathFilmSimulationPhoto(pathname)) {
|
||||
return pathForFilmSimulation(simulation);
|
||||
} else if (focal && (
|
||||
isPathFocalLengthPhoto(pathname) ||
|
||||
isPathFocalLengthShare(pathname)
|
||||
)) {
|
||||
} else if (focal && isPathFocalLengthPhoto(pathname)) {
|
||||
return pathForFocalLength(focal);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Dispatch, SetStateAction, createContext, useContext } from 'react';
|
||||
import { AnimationConfig } from '@/components/AnimateItems';
|
||||
import { ShareModalProps } from '@/share';
|
||||
|
||||
export interface AppStateContext {
|
||||
// CORE
|
||||
@ -13,9 +14,12 @@ export interface AppStateContext {
|
||||
clearNextPhotoAnimation?: () => void
|
||||
shouldRespondToKeyboardCommands?: boolean
|
||||
setShouldRespondToKeyboardCommands?: Dispatch<SetStateAction<boolean>>
|
||||
// MODAL
|
||||
isCommandKOpen?: boolean
|
||||
setIsCommandKOpen?: Dispatch<SetStateAction<boolean>>
|
||||
// ADMIN
|
||||
shareModalProps?: ShareModalProps
|
||||
setShareModalProps?: Dispatch<SetStateAction<ShareModalProps | undefined>>
|
||||
// ADMIN
|
||||
userEmail?: string
|
||||
setUserEmail?: Dispatch<SetStateAction<string | undefined>>
|
||||
isUserSignedIn?: boolean
|
||||
|
||||
@ -8,6 +8,7 @@ import { getAuthAction } from '@/auth/actions';
|
||||
import useSWR from 'swr';
|
||||
import { HIGH_DENSITY_GRID, MATTE_PHOTOS } from '@/site/config';
|
||||
import { getPhotosHiddenMetaCachedAction } from '@/photo/actions';
|
||||
import { ShareModalProps } from '@/share';
|
||||
|
||||
export default function AppStateProvider({
|
||||
children,
|
||||
@ -25,8 +26,11 @@ export default function AppStateProvider({
|
||||
useState<AnimationConfig>();
|
||||
const [shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands] =
|
||||
useState(true);
|
||||
// MODAL
|
||||
const [isCommandKOpen, setIsCommandKOpen] =
|
||||
useState(false);
|
||||
const [shareModalProps, setShareModalProps] =
|
||||
useState<ShareModalProps>();
|
||||
// ADMIN
|
||||
const [userEmail, setUserEmail] =
|
||||
useState<string>();
|
||||
@ -89,8 +93,11 @@ export default function AppStateProvider({
|
||||
clearNextPhotoAnimation: () => setNextPhotoAnimation?.(undefined),
|
||||
shouldRespondToKeyboardCommands,
|
||||
setShouldRespondToKeyboardCommands,
|
||||
// MODAL
|
||||
isCommandKOpen,
|
||||
setIsCommandKOpen,
|
||||
shareModalProps,
|
||||
setShareModalProps,
|
||||
// ADMIN
|
||||
userEmail,
|
||||
setUserEmail,
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { Photo, PhotoDateRange } from '@/photo';
|
||||
import PhotoTag from './PhotoTag';
|
||||
import { descriptionForTaggedPhotos, isTagFavs } from '.';
|
||||
import { pathForTagShare } from '@/site/paths';
|
||||
import PhotoHeader from '@/photo/PhotoHeader';
|
||||
import FavsTag from './FavsTag';
|
||||
|
||||
@ -30,10 +29,10 @@ export default function TagHeader({
|
||||
entityDescription={descriptionForTaggedPhotos(photos, undefined, count)}
|
||||
photos={photos}
|
||||
selectedPhoto={selectedPhoto}
|
||||
sharePath={pathForTagShare(tag)}
|
||||
indexNumber={indexNumber}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
includeShareButton
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { absolutePathForTag, pathForTag } from '@/site/paths';
|
||||
import { Photo, PhotoDateRange } from '../photo';
|
||||
import ShareModal from '@/components/ShareModal';
|
||||
import { absolutePathForTag } from '@/site/paths';
|
||||
import { PhotoSetAttributes } from '../photo';
|
||||
import ShareModal from '@/share/ShareModal';
|
||||
import TagOGTile from './TagOGTile';
|
||||
import { shareTextForTag } from '.';
|
||||
|
||||
@ -11,14 +11,10 @@ export default function TagShareModal({
|
||||
dateRange,
|
||||
}: {
|
||||
tag: string
|
||||
photos: Photo[]
|
||||
count?: number
|
||||
dateRange?: PhotoDateRange
|
||||
}) {
|
||||
} & PhotoSetAttributes) {
|
||||
return (
|
||||
<ShareModal
|
||||
pathShare={absolutePathForTag(tag)}
|
||||
pathClose={pathForTag(tag)}
|
||||
socialText={shareTextForTag(tag)}
|
||||
>
|
||||
<TagOGTile {...{ tag, photos, count, dateRange }} />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user