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
|
#### Site meta
|
||||||
- `NEXT_PUBLIC_SITE_TITLE` (seen in browser tab)
|
- `NEXT_PUBLIC_SITE_TITLE` (seen in browser tab)
|
||||||
- `NEXT_PUBLIC_SITE_DESCRIPTION` (seen in nav, beneath title)
|
- `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
|
#### Site behavior
|
||||||
- `NEXT_PUBLIC_GRID_HOMEPAGE = 1` shows grid layout on homepage
|
- `NEXT_PUBLIC_GRID_HOMEPAGE = 1` shows grid layout on homepage
|
||||||
|
|||||||
@ -4,23 +4,14 @@ import {
|
|||||||
getPathComponents,
|
getPathComponents,
|
||||||
isPathCamera,
|
isPathCamera,
|
||||||
isPathCameraPhoto,
|
isPathCameraPhoto,
|
||||||
isPathCameraPhotoShare,
|
|
||||||
isPathCameraShare,
|
|
||||||
isPathFilmSimulation,
|
isPathFilmSimulation,
|
||||||
isPathFilmSimulationPhoto,
|
isPathFilmSimulationPhoto,
|
||||||
isPathFilmSimulationPhotoShare,
|
|
||||||
isPathFilmSimulationShare,
|
|
||||||
isPathFocalLength,
|
isPathFocalLength,
|
||||||
isPathFocalLengthPhoto,
|
isPathFocalLengthPhoto,
|
||||||
isPathFocalLengthPhotoShare,
|
|
||||||
isPathFocalLengthShare,
|
|
||||||
isPathPhoto,
|
isPathPhoto,
|
||||||
isPathPhotoShare,
|
|
||||||
isPathProtected,
|
isPathProtected,
|
||||||
isPathTag,
|
isPathTag,
|
||||||
isPathTagPhoto,
|
isPathTagPhoto,
|
||||||
isPathTagPhotoShare,
|
|
||||||
isPathTagShare,
|
|
||||||
} from '@/site/paths';
|
} from '@/site/paths';
|
||||||
import { TAG_HIDDEN } from '@/tag';
|
import { TAG_HIDDEN } from '@/tag';
|
||||||
|
|
||||||
@ -32,7 +23,6 @@ const CAMERA_OBJECT = { make: CAMERA_MAKE, model: CAMERA_MODEL };
|
|||||||
const FILM_SIMULATION = 'acros';
|
const FILM_SIMULATION = 'acros';
|
||||||
const FOCAL_LENGTH = 90;
|
const FOCAL_LENGTH = 90;
|
||||||
const FOCAL_LENGTH_STRING = `${FOCAL_LENGTH}mm`;
|
const FOCAL_LENGTH_STRING = `${FOCAL_LENGTH}mm`;
|
||||||
const SHARE = 'share';
|
|
||||||
|
|
||||||
const PATH_ROOT = '/';
|
const PATH_ROOT = '/';
|
||||||
const PATH_GRID = '/grid';
|
const PATH_GRID = '/grid';
|
||||||
@ -43,32 +33,21 @@ const PATH_OG_ALL = `${PATH_OG}/all`;
|
|||||||
const PATH_OG_SAMPLE = `${PATH_OG}/sample`;
|
const PATH_OG_SAMPLE = `${PATH_OG}/sample`;
|
||||||
|
|
||||||
const PATH_PHOTO = `/p/${PHOTO_ID}`;
|
const PATH_PHOTO = `/p/${PHOTO_ID}`;
|
||||||
const PATH_PHOTO_SHARE = `${PATH_PHOTO}/${SHARE}`;
|
|
||||||
|
|
||||||
const PATH_TAG = `/tag/${TAG}`;
|
const PATH_TAG = `/tag/${TAG}`;
|
||||||
const PATH_TAG_SHARE = `${PATH_TAG}/${SHARE}`;
|
|
||||||
const PATH_TAG_PHOTO = `${PATH_TAG}/${PHOTO_ID}`;
|
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 = `/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 = `${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 = `/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 = `${PATH_CAMERA}/${PHOTO_ID}`;
|
||||||
const PATH_CAMERA_PHOTO_SHARE = `${PATH_CAMERA_PHOTO}/${SHARE}`;
|
|
||||||
|
|
||||||
const PATH_FILM_SIMULATION = `/film/${FILM_SIMULATION}`;
|
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 = `${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 = `/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 = `${PATH_FOCAL_LENGTH}/${PHOTO_ID}`;
|
||||||
const PATH_FOCAL_LENGTH_PHOTO_SHARE = `${PATH_FOCAL_LENGTH_PHOTO}/${SHARE}`;
|
|
||||||
|
|
||||||
describe('Paths', () => {
|
describe('Paths', () => {
|
||||||
it('can be protected', () => {
|
it('can be protected', () => {
|
||||||
@ -85,49 +64,22 @@ describe('Paths', () => {
|
|||||||
expect(isPathProtected(PATH_OG_ALL)).toBe(true);
|
expect(isPathProtected(PATH_OG_ALL)).toBe(true);
|
||||||
expect(isPathProtected(PATH_OG_SAMPLE)).toBe(true);
|
expect(isPathProtected(PATH_OG_SAMPLE)).toBe(true);
|
||||||
expect(isPathProtected(PATH_TAG_HIDDEN)).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)).toBe(true);
|
||||||
expect(isPathProtected(PATH_TAG_HIDDEN_PHOTO_SHARE)).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('can be classified', () => {
|
it('can be classified', () => {
|
||||||
// Positive
|
// Positive
|
||||||
expect(isPathPhoto(PATH_PHOTO)).toBe(true);
|
expect(isPathPhoto(PATH_PHOTO)).toBe(true);
|
||||||
expect(isPathPhotoShare(PATH_PHOTO_SHARE)).toBe(true);
|
|
||||||
expect(isPathTag(PATH_TAG)).toBe(true);
|
expect(isPathTag(PATH_TAG)).toBe(true);
|
||||||
expect(isPathTagShare(PATH_TAG_SHARE)).toBe(true);
|
|
||||||
expect(isPathTagPhoto(PATH_TAG_PHOTO)).toBe(true);
|
expect(isPathTagPhoto(PATH_TAG_PHOTO)).toBe(true);
|
||||||
expect(isPathTagPhotoShare(PATH_TAG_PHOTO_SHARE)).toBe(true);
|
|
||||||
expect(isPathCamera(PATH_CAMERA)).toBe(true);
|
expect(isPathCamera(PATH_CAMERA)).toBe(true);
|
||||||
expect(isPathCameraShare(PATH_CAMERA_SHARE)).toBe(true);
|
|
||||||
expect(isPathCameraPhoto(PATH_CAMERA_PHOTO)).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(isPathFilmSimulation(PATH_FILM_SIMULATION)).toBe(true);
|
||||||
expect(isPathFilmSimulationShare(PATH_FILM_SIMULATION_SHARE)).toBe(true);
|
|
||||||
expect(isPathFilmSimulationPhoto(PATH_FILM_SIMULATION_PHOTO)).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(isPathFocalLength(PATH_FOCAL_LENGTH)).toBe(true);
|
||||||
expect(isPathFocalLengthShare(PATH_FOCAL_LENGTH_SHARE)).toBe(true);
|
|
||||||
expect(isPathFocalLengthPhoto(PATH_FOCAL_LENGTH_PHOTO)).toBe(true);
|
expect(isPathFocalLengthPhoto(PATH_FOCAL_LENGTH_PHOTO)).toBe(true);
|
||||||
expect(isPathFocalLengthPhotoShare(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toBe(true);
|
|
||||||
// Negative
|
// 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(isPathFocalLength(PATH_FILM_SIMULATION)).toBe(false);
|
||||||
expect(isPathFocalLengthShare(PATH_FILM_SIMULATION_SHARE)).toBe(false);
|
|
||||||
expect(isPathFocalLengthPhoto(PATH_FILM_SIMULATION_PHOTO)).toBe(false);
|
expect(isPathFocalLengthPhoto(PATH_FILM_SIMULATION_PHOTO)).toBe(false);
|
||||||
expect(isPathFocalLengthPhotoShare(PATH_FILM_SIMULATION_PHOTO_SHARE)).toBe(false);
|
|
||||||
});
|
});
|
||||||
it('can be parsed', () => {
|
it('can be parsed', () => {
|
||||||
// Core
|
// Core
|
||||||
@ -135,69 +87,38 @@ describe('Paths', () => {
|
|||||||
expect(getPathComponents(PATH_PHOTO)).toEqual({
|
expect(getPathComponents(PATH_PHOTO)).toEqual({
|
||||||
photoId: PHOTO_ID,
|
photoId: PHOTO_ID,
|
||||||
});
|
});
|
||||||
expect(getPathComponents(PATH_PHOTO_SHARE)).toEqual({
|
|
||||||
photoId: PHOTO_ID,
|
|
||||||
});
|
|
||||||
// Tag
|
// Tag
|
||||||
expect(getPathComponents(PATH_TAG)).toEqual({
|
expect(getPathComponents(PATH_TAG)).toEqual({
|
||||||
tag: TAG,
|
tag: TAG,
|
||||||
});
|
});
|
||||||
expect(getPathComponents(PATH_TAG_SHARE)).toEqual({
|
|
||||||
tag: TAG,
|
|
||||||
});
|
|
||||||
expect(getPathComponents(PATH_TAG_PHOTO)).toEqual({
|
expect(getPathComponents(PATH_TAG_PHOTO)).toEqual({
|
||||||
photoId: PHOTO_ID,
|
photoId: PHOTO_ID,
|
||||||
tag: TAG,
|
tag: TAG,
|
||||||
});
|
});
|
||||||
expect(getPathComponents(PATH_TAG_PHOTO_SHARE)).toEqual({
|
|
||||||
photoId: PHOTO_ID,
|
|
||||||
tag: TAG,
|
|
||||||
});
|
|
||||||
// Camera
|
// Camera
|
||||||
expect(getPathComponents(PATH_CAMERA)).toEqual({
|
expect(getPathComponents(PATH_CAMERA)).toEqual({
|
||||||
camera: CAMERA_OBJECT,
|
camera: CAMERA_OBJECT,
|
||||||
});
|
});
|
||||||
expect(getPathComponents(PATH_CAMERA_SHARE)).toEqual({
|
|
||||||
camera: CAMERA_OBJECT,
|
|
||||||
});
|
|
||||||
expect(getPathComponents(PATH_CAMERA_PHOTO)).toEqual({
|
expect(getPathComponents(PATH_CAMERA_PHOTO)).toEqual({
|
||||||
photoId: PHOTO_ID,
|
photoId: PHOTO_ID,
|
||||||
camera: CAMERA_OBJECT,
|
camera: CAMERA_OBJECT,
|
||||||
});
|
});
|
||||||
expect(getPathComponents(PATH_CAMERA_PHOTO_SHARE)).toEqual({
|
|
||||||
photoId: PHOTO_ID,
|
|
||||||
camera: CAMERA_OBJECT,
|
|
||||||
});
|
|
||||||
// Film Simulation
|
// Film Simulation
|
||||||
expect(getPathComponents(PATH_FILM_SIMULATION)).toEqual({
|
expect(getPathComponents(PATH_FILM_SIMULATION)).toEqual({
|
||||||
simulation: FILM_SIMULATION,
|
simulation: FILM_SIMULATION,
|
||||||
});
|
});
|
||||||
expect(getPathComponents(PATH_FILM_SIMULATION_SHARE)).toEqual({
|
|
||||||
simulation: FILM_SIMULATION,
|
|
||||||
});
|
|
||||||
expect(getPathComponents(PATH_FILM_SIMULATION_PHOTO)).toEqual({
|
expect(getPathComponents(PATH_FILM_SIMULATION_PHOTO)).toEqual({
|
||||||
photoId: PHOTO_ID,
|
photoId: PHOTO_ID,
|
||||||
simulation: FILM_SIMULATION,
|
simulation: FILM_SIMULATION,
|
||||||
});
|
});
|
||||||
expect(getPathComponents(PATH_FILM_SIMULATION_PHOTO_SHARE)).toEqual({
|
|
||||||
photoId: PHOTO_ID,
|
|
||||||
simulation: FILM_SIMULATION,
|
|
||||||
});
|
|
||||||
// Focal Length
|
// Focal Length
|
||||||
expect(getPathComponents(PATH_FOCAL_LENGTH)).toEqual({
|
expect(getPathComponents(PATH_FOCAL_LENGTH)).toEqual({
|
||||||
focal: FOCAL_LENGTH,
|
focal: FOCAL_LENGTH,
|
||||||
});
|
});
|
||||||
expect(getPathComponents(PATH_FOCAL_LENGTH_SHARE)).toEqual({
|
|
||||||
focal: FOCAL_LENGTH,
|
|
||||||
});
|
|
||||||
expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO)).toEqual({
|
expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO)).toEqual({
|
||||||
photoId: PHOTO_ID,
|
photoId: PHOTO_ID,
|
||||||
focal: FOCAL_LENGTH,
|
focal: FOCAL_LENGTH,
|
||||||
});
|
});
|
||||||
expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toEqual({
|
|
||||||
photoId: PHOTO_ID,
|
|
||||||
focal: FOCAL_LENGTH,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
it('can be escaped', () => {
|
it('can be escaped', () => {
|
||||||
// Root
|
// Root
|
||||||
@ -207,26 +128,17 @@ describe('Paths', () => {
|
|||||||
expect(getEscapePath(PATH_ADMIN)).toEqual(undefined);
|
expect(getEscapePath(PATH_ADMIN)).toEqual(undefined);
|
||||||
// Photo
|
// Photo
|
||||||
expect(getEscapePath(PATH_PHOTO)).toEqual(PATH_ROOT);
|
expect(getEscapePath(PATH_PHOTO)).toEqual(PATH_ROOT);
|
||||||
expect(getEscapePath(PATH_PHOTO_SHARE)).toEqual(PATH_PHOTO);
|
|
||||||
// Tag
|
// Tag
|
||||||
expect(getEscapePath(PATH_TAG)).toEqual(PATH_ROOT);
|
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)).toEqual(PATH_TAG);
|
||||||
expect(getEscapePath(PATH_TAG_PHOTO_SHARE)).toEqual(PATH_TAG_PHOTO);
|
|
||||||
// Camera
|
// Camera
|
||||||
expect(getEscapePath(PATH_CAMERA)).toEqual(PATH_ROOT);
|
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)).toEqual(PATH_CAMERA);
|
||||||
expect(getEscapePath(PATH_CAMERA_PHOTO_SHARE)).toEqual(PATH_CAMERA_PHOTO);
|
|
||||||
// Film Simulation
|
// Film Simulation
|
||||||
expect(getEscapePath(PATH_FILM_SIMULATION)).toEqual(PATH_ROOT);
|
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)).toEqual(PATH_FILM_SIMULATION);
|
||||||
expect(getEscapePath(PATH_FILM_SIMULATION_PHOTO_SHARE)).toEqual(PATH_FILM_SIMULATION_PHOTO);
|
|
||||||
// Focal Length
|
// Focal Length
|
||||||
expect(getEscapePath(PATH_FOCAL_LENGTH)).toEqual(PATH_ROOT);
|
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)).toEqual(PATH_FOCAL_LENGTH);
|
||||||
expect(getEscapePath(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toEqual(PATH_FOCAL_LENGTH_PHOTO);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const eslintConfig = [
|
|||||||
rules: {
|
rules: {
|
||||||
'@next/next/no-img-element': 'off',
|
'@next/next/no-img-element': 'off',
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'no-unused-expressions': 'warn',
|
'no-unused-expressions': ['warn'],
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'warn', {
|
'warn', {
|
||||||
'argsIgnorePattern': '^_',
|
'argsIgnorePattern': '^_',
|
||||||
|
|||||||
24
package.json
24
package.json
@ -9,9 +9,9 @@
|
|||||||
"analyze": "ANALYZE=true next build"
|
"analyze": "ANALYZE=true next build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^1.0.13",
|
"@ai-sdk/openai": "^1.0.18",
|
||||||
"@aws-sdk/client-s3": "3.722.0",
|
"@aws-sdk/client-s3": "3.726.1",
|
||||||
"@aws-sdk/s3-request-presigner": "3.722.0",
|
"@aws-sdk/s3-request-presigner": "3.726.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.4",
|
"@radix-ui/react-dialog": "^1.1.4",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||||
"@radix-ui/react-visually-hidden": "^1.1.1",
|
"@radix-ui/react-visually-hidden": "^1.1.1",
|
||||||
@ -20,14 +20,14 @@
|
|||||||
"@vercel/blob": "^0.27.0",
|
"@vercel/blob": "^0.27.0",
|
||||||
"@vercel/kv": "^3.0.0",
|
"@vercel/kv": "^3.0.0",
|
||||||
"@vercel/speed-insights": "^1.1.0",
|
"@vercel/speed-insights": "^1.1.0",
|
||||||
"ai": "^4.0.26",
|
"ai": "^4.0.33",
|
||||||
"camelcase-keys": "^9.1.3",
|
"camelcase-keys": "^9.1.3",
|
||||||
"cmdk": "^1.0.4",
|
"cmdk": "^1.0.4",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^11.17.0",
|
||||||
"nanoid": "^5.0.9",
|
"nanoid": "^5.0.9",
|
||||||
"next": "15.1.3",
|
"next": "15.1.4",
|
||||||
"next-auth": "5.0.0-beta.25",
|
"next-auth": "5.0.0-beta.25",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
@ -42,25 +42,25 @@
|
|||||||
"use-debounce": "^10.0.4"
|
"use-debounce": "^10.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/bundle-analyzer": "15.1.3",
|
"@next/bundle-analyzer": "15.1.4",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@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/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "^16.1.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.10.5",
|
"@types/node": "^22.10.5",
|
||||||
"@types/pg": "^8.11.10",
|
"@types/pg": "^8.11.10",
|
||||||
"@types/react": "19.0.2",
|
"@types/react": "19.0.4",
|
||||||
"@types/react-dom": "19.0.2",
|
"@types/react-dom": "19.0.2",
|
||||||
"@types/sanitize-html": "^2.13.0",
|
"@types/sanitize-html": "^2.13.0",
|
||||||
"autoprefixer": "10.4.20",
|
"autoprefixer": "10.4.20",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint": "9.17.0",
|
"eslint": "9.18.0",
|
||||||
"eslint-config-next": "15.1.3",
|
"eslint-config-next": "15.1.4",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"postcss": "8.4.49",
|
"postcss": "8.4.49",
|
||||||
"tailwindcss": "3.4.17",
|
"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() {
|
import {
|
||||||
return null;
|
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() {
|
import {
|
||||||
return null;
|
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 CommandK from '@/site/CommandK';
|
||||||
import SwrConfigClient from '../state/SwrConfigClient';
|
import SwrConfigClient from '../state/SwrConfigClient';
|
||||||
import AdminBatchEditPanel from '@/admin/AdminBatchEditPanel';
|
import AdminBatchEditPanel from '@/admin/AdminBatchEditPanel';
|
||||||
|
import ShareModals from '@/share/ShareModals';
|
||||||
|
|
||||||
import '../site/globals.css';
|
import '../site/globals.css';
|
||||||
import '../site/sonner.css';
|
import '../site/sonner.css';
|
||||||
@ -97,6 +98,7 @@ export default function RootLayout({
|
|||||||
'min-h-[16rem] sm:min-h-[30rem]',
|
'min-h-[16rem] sm:min-h-[30rem]',
|
||||||
'mb-12',
|
'mb-12',
|
||||||
)}>
|
)}>
|
||||||
|
<ShareModals />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<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() {
|
import {
|
||||||
return null;
|
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() {
|
import {
|
||||||
return null;
|
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() {
|
import {
|
||||||
return null;
|
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 { Photo, PhotoDateRange } from '@/photo';
|
||||||
import { pathForCameraShare } from '@/site/paths';
|
|
||||||
import PhotoHeader from '@/photo/PhotoHeader';
|
import PhotoHeader from '@/photo/PhotoHeader';
|
||||||
import { Camera, cameraFromPhoto } from '.';
|
import { Camera, cameraFromPhoto } from '.';
|
||||||
import PhotoCamera from './PhotoCamera';
|
import PhotoCamera from './PhotoCamera';
|
||||||
@ -29,10 +28,10 @@ export default function CameraHeader({
|
|||||||
descriptionForCameraPhotos(photos, undefined, count, dateRange)}
|
descriptionForCameraPhotos(photos, undefined, count, dateRange)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
sharePath={pathForCameraShare(camera)}
|
|
||||||
indexNumber={indexNumber}
|
indexNumber={indexNumber}
|
||||||
count={count}
|
count={count}
|
||||||
dateRange={dateRange}
|
dateRange={dateRange}
|
||||||
|
includeShareButton
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { absolutePathForCamera, pathForCamera } from '@/site/paths';
|
import { absolutePathForCamera } from '@/site/paths';
|
||||||
import { Photo, PhotoDateRange } from '../photo';
|
import { PhotoSetAttributes } from '../photo';
|
||||||
import ShareModal from '@/components/ShareModal';
|
import ShareModal from '@/share/ShareModal';
|
||||||
import CameraOGTile from './CameraOGTile';
|
import CameraOGTile from './CameraOGTile';
|
||||||
import { Camera } from '.';
|
import { Camera } from '.';
|
||||||
import { shareTextForCamera } from './meta';
|
import { shareTextForCamera } from './meta';
|
||||||
@ -12,14 +12,10 @@ export default function CameraShareModal({
|
|||||||
dateRange,
|
dateRange,
|
||||||
}: {
|
}: {
|
||||||
camera: Camera
|
camera: Camera
|
||||||
photos: Photo[]
|
} & PhotoSetAttributes) {
|
||||||
count: number
|
|
||||||
dateRange?: PhotoDateRange,
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
pathShare={absolutePathForCamera(camera)}
|
pathShare={absolutePathForCamera(camera)}
|
||||||
pathClose={pathForCamera(camera)}
|
|
||||||
socialText={shareTextForCamera(camera, photos)}
|
socialText={shareTextForCamera(camera, photos)}
|
||||||
>
|
>
|
||||||
<CameraOGTile {...{ camera, photos, count, dateRange }} />
|
<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 { Photo, PhotoDateRange } from '@/photo';
|
||||||
import { descriptionForFocalLengthPhotos } from '.';
|
import { descriptionForFocalLengthPhotos } from '.';
|
||||||
import { pathForFocalLengthShare } from '@/site/paths';
|
|
||||||
import PhotoHeader from '@/photo/PhotoHeader';
|
import PhotoHeader from '@/photo/PhotoHeader';
|
||||||
import PhotoFocalLength from './PhotoFocalLength';
|
import PhotoFocalLength from './PhotoFocalLength';
|
||||||
|
|
||||||
@ -30,10 +29,10 @@ export default function FocalLengthHeader({
|
|||||||
)}
|
)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
sharePath={pathForFocalLengthShare(focal)}
|
|
||||||
indexNumber={indexNumber}
|
indexNumber={indexNumber}
|
||||||
count={count}
|
count={count}
|
||||||
dateRange={dateRange}
|
dateRange={dateRange}
|
||||||
|
includeShareButton
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { absolutePathForFocalLength, pathForFocalLength } from '@/site/paths';
|
import { absolutePathForFocalLength } from '@/site/paths';
|
||||||
import { Photo, PhotoDateRange } from '../photo';
|
import { PhotoSetAttributes } from '../photo';
|
||||||
import ShareModal from '@/components/ShareModal';
|
import ShareModal from '@/share/ShareModal';
|
||||||
import FocalLengthOGTile from './FocalLengthOGTile';
|
import FocalLengthOGTile from './FocalLengthOGTile';
|
||||||
import { shareTextFocalLength } from '.';
|
import { shareTextFocalLength } from '.';
|
||||||
|
|
||||||
@ -11,14 +11,10 @@ export default function FocalLengthShareModal({
|
|||||||
dateRange,
|
dateRange,
|
||||||
}: {
|
}: {
|
||||||
focal: number
|
focal: number
|
||||||
photos: Photo[]
|
} & PhotoSetAttributes) {
|
||||||
count?: number
|
|
||||||
dateRange?: PhotoDateRange
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
pathShare={absolutePathForFocalLength(focal)}
|
pathShare={absolutePathForFocalLength(focal)}
|
||||||
pathClose={pathForFocalLength(focal)}
|
|
||||||
socialText={shareTextFocalLength(focal)}
|
socialText={shareTextFocalLength(focal)}
|
||||||
>
|
>
|
||||||
<FocalLengthOGTile {...{ focal, photos, count, dateRange }} />
|
<FocalLengthOGTile {...{ focal, photos, count, dateRange }} />
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import Spinner from '@/components/Spinner';
|
import Spinner from '@/components/Spinner';
|
||||||
import { getPhotosCachedAction, getPhotosAction } from '@/photo/actions';
|
import { getPhotosCachedAction, getPhotosAction } from '@/photo/actions';
|
||||||
import { Photo, PhotoSetAttributes } from '.';
|
import { Photo, PhotoSetCategory } from '.';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import { GetPhotosOptions } from './db';
|
import { GetPhotosOptions } from './db';
|
||||||
@ -45,7 +45,7 @@ export default function InfinitePhotoScroll({
|
|||||||
onLastPhotoVisible: () => void
|
onLastPhotoVisible: () => void
|
||||||
revalidatePhoto?: RevalidatePhoto
|
revalidatePhoto?: RevalidatePhoto
|
||||||
}) => ReactNode
|
}) => ReactNode
|
||||||
} & PhotoSetAttributes) {
|
} & PhotoSetCategory) {
|
||||||
const { swrTimestamp, isUserSignedIn } = useAppState();
|
const { swrTimestamp, isUserSignedIn } = useAppState();
|
||||||
|
|
||||||
const key = `${swrTimestamp}-${cacheKey}`;
|
const key = `${swrTimestamp}-${cacheKey}`;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import AnimateItems from '@/components/AnimateItems';
|
import AnimateItems from '@/components/AnimateItems';
|
||||||
import { Photo, PhotoDateRange, PhotoSetAttributes } from '.';
|
import { Photo, PhotoDateRange, PhotoSetCategory } from '.';
|
||||||
import PhotoLarge from './PhotoLarge';
|
import PhotoLarge from './PhotoLarge';
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import PhotoGrid from './PhotoGrid';
|
import PhotoGrid from './PhotoGrid';
|
||||||
@ -34,7 +34,7 @@ export default function PhotoDetailPage({
|
|||||||
dateRange?: PhotoDateRange
|
dateRange?: PhotoDateRange
|
||||||
shouldShare?: boolean
|
shouldShare?: boolean
|
||||||
includeFavoriteInAdminMenu?: boolean
|
includeFavoriteInAdminMenu?: boolean
|
||||||
} & PhotoSetAttributes) {
|
} & PhotoSetCategory) {
|
||||||
let customHeader: JSX.Element | undefined;
|
let customHeader: JSX.Element | undefined;
|
||||||
|
|
||||||
if (tag) {
|
if (tag) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Photo, PhotoSetAttributes } from '.';
|
import { Photo, PhotoSetCategory } from '.';
|
||||||
import PhotoMedium from './PhotoMedium';
|
import PhotoMedium from './PhotoMedium';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import AnimateItems from '@/components/AnimateItems';
|
import AnimateItems from '@/components/AnimateItems';
|
||||||
@ -41,7 +41,7 @@ export default function PhotoGrid({
|
|||||||
canSelect?: boolean
|
canSelect?: boolean
|
||||||
onLastPhotoVisible?: () => void
|
onLastPhotoVisible?: () => void
|
||||||
onAnimationComplete?: () => void
|
onAnimationComplete?: () => void
|
||||||
} & PhotoSetAttributes) {
|
} & PhotoSetCategory) {
|
||||||
const {
|
const {
|
||||||
isUserSignedIn,
|
isUserSignedIn,
|
||||||
selectedPhotoIds,
|
selectedPhotoIds,
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import { clsx } from 'clsx/lite';
|
|||||||
import {
|
import {
|
||||||
Photo,
|
Photo,
|
||||||
PhotoDateRange,
|
PhotoDateRange,
|
||||||
PhotoSetAttributes,
|
PhotoSetCategory,
|
||||||
dateRangeForPhotos,
|
dateRangeForPhotos,
|
||||||
titleForPhoto,
|
titleForPhoto,
|
||||||
} from '.';
|
} from '.';
|
||||||
import ShareButton from '@/components/ShareButton';
|
import ShareButton from '@/share/ShareButton';
|
||||||
import AnimateItems from '@/components/AnimateItems';
|
import AnimateItems from '@/components/AnimateItems';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid';
|
import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid';
|
||||||
@ -27,21 +27,21 @@ export default function PhotoHeader({
|
|||||||
entity,
|
entity,
|
||||||
entityVerb = 'PHOTO',
|
entityVerb = 'PHOTO',
|
||||||
entityDescription,
|
entityDescription,
|
||||||
sharePath,
|
|
||||||
indexNumber,
|
indexNumber,
|
||||||
count,
|
count,
|
||||||
dateRange,
|
dateRange,
|
||||||
|
includeShareButton,
|
||||||
}: {
|
}: {
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
selectedPhoto?: Photo
|
selectedPhoto?: Photo
|
||||||
entity?: ReactNode
|
entity?: ReactNode
|
||||||
entityVerb?: string
|
entityVerb?: string
|
||||||
entityDescription?: string
|
entityDescription?: string
|
||||||
sharePath?: string
|
|
||||||
indexNumber?: number
|
indexNumber?: number
|
||||||
count?: number
|
count?: number
|
||||||
dateRange?: PhotoDateRange
|
dateRange?: PhotoDateRange
|
||||||
} & PhotoSetAttributes) {
|
includeShareButton?: boolean
|
||||||
|
} & PhotoSetCategory) {
|
||||||
const { isGridHighDensity } = useAppState();
|
const { isGridHighDensity } = useAppState();
|
||||||
|
|
||||||
const { start, end } = dateRangeForPhotos(photos, dateRange);
|
const { start, end } = dateRangeForPhotos(photos, dateRange);
|
||||||
@ -138,10 +138,17 @@ export default function PhotoHeader({
|
|||||||
{headerType === 'photo-set'
|
{headerType === 'photo-set'
|
||||||
? <>
|
? <>
|
||||||
{entityDescription}
|
{entityDescription}
|
||||||
{sharePath &&
|
{includeShareButton &&
|
||||||
<ShareButton
|
<ShareButton
|
||||||
|
photos={photos}
|
||||||
|
tag={tag}
|
||||||
|
camera={camera}
|
||||||
|
simulation={simulation}
|
||||||
|
focal={focal}
|
||||||
|
count={count}
|
||||||
|
dateRange={dateRange}
|
||||||
className="translate-y-[1.5px]"
|
className="translate-y-[1.5px]"
|
||||||
path={sharePath}
|
prefetch
|
||||||
dim
|
dim
|
||||||
/>}
|
/>}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -15,10 +15,9 @@ import Link from 'next/link';
|
|||||||
import {
|
import {
|
||||||
pathForFocalLength,
|
pathForFocalLength,
|
||||||
pathForPhoto,
|
pathForPhoto,
|
||||||
pathForPhotoShare,
|
|
||||||
} from '@/site/paths';
|
} from '@/site/paths';
|
||||||
import PhotoTags from '@/tag/PhotoTags';
|
import PhotoTags from '@/tag/PhotoTags';
|
||||||
import ShareButton from '@/components/ShareButton';
|
import ShareButton from '@/share/ShareButton';
|
||||||
import DownloadButton from '@/components/DownloadButton';
|
import DownloadButton from '@/components/DownloadButton';
|
||||||
import PhotoCamera from '../camera/PhotoCamera';
|
import PhotoCamera from '../camera/PhotoCamera';
|
||||||
import { cameraFromPhoto } from '@/camera';
|
import { cameraFromPhoto } from '@/camera';
|
||||||
@ -54,7 +53,6 @@ export default function PhotoLarge({
|
|||||||
shouldShareCamera,
|
shouldShareCamera,
|
||||||
shouldShareSimulation,
|
shouldShareSimulation,
|
||||||
shouldShareFocalLength,
|
shouldShareFocalLength,
|
||||||
shouldScrollOnShare,
|
|
||||||
includeFavoriteInAdminMenu,
|
includeFavoriteInAdminMenu,
|
||||||
onVisible,
|
onVisible,
|
||||||
}: {
|
}: {
|
||||||
@ -258,17 +256,14 @@ export default function PhotoLarge({
|
|||||||
)}>
|
)}>
|
||||||
{shouldShare &&
|
{shouldShare &&
|
||||||
<ShareButton
|
<ShareButton
|
||||||
path={pathForPhotoShare({
|
photo={photo}
|
||||||
photo,
|
tag={shouldShareTag ? primaryTag : undefined}
|
||||||
tag: shouldShareTag ? primaryTag : undefined,
|
camera={shouldShareCamera ? camera : undefined}
|
||||||
camera: shouldShareCamera ? camera : undefined,
|
// eslint-disable-next-line max-len
|
||||||
// eslint-disable-next-line max-len
|
simulation={shouldShareSimulation? photo.filmSimulation : undefined}
|
||||||
simulation: shouldShareSimulation ? photo.filmSimulation : undefined,
|
// eslint-disable-next-line max-len
|
||||||
// eslint-disable-next-line max-len
|
focal={shouldShareFocalLength ? photo.focalLength : undefined}
|
||||||
focal: shouldShareFocalLength ? photo.focalLength : undefined,
|
|
||||||
})}
|
|
||||||
prefetch={prefetchRelatedLinks}
|
prefetch={prefetchRelatedLinks}
|
||||||
shouldScroll={shouldScrollOnShare}
|
|
||||||
/>}
|
/>}
|
||||||
{ALLOW_PUBLIC_DOWNLOADS &&
|
{ALLOW_PUBLIC_DOWNLOADS &&
|
||||||
<DownloadButton
|
<DownloadButton
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Photo, PhotoSetAttributes, titleForPhoto } from '@/photo';
|
import { Photo, PhotoSetCategory, titleForPhoto } from '@/photo';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { AnimationConfig } from '../components/AnimateItems';
|
import { AnimationConfig } from '../components/AnimateItems';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
@ -26,7 +26,7 @@ export default function PhotoLink({
|
|||||||
nextPhotoAnimation?: AnimationConfig
|
nextPhotoAnimation?: AnimationConfig
|
||||||
className?: string
|
className?: string
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
} & PhotoSetAttributes) {
|
} & PhotoSetCategory) {
|
||||||
const { setNextPhotoAnimation } = useAppState();
|
const { setNextPhotoAnimation } = useAppState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Photo,
|
Photo,
|
||||||
PhotoSetAttributes,
|
PhotoSetCategory,
|
||||||
altTextForPhoto,
|
altTextForPhoto,
|
||||||
doesPhotoNeedBlurCompatibility,
|
doesPhotoNeedBlurCompatibility,
|
||||||
} from '.';
|
} from '.';
|
||||||
@ -32,7 +32,7 @@ export default function PhotoMedium({
|
|||||||
prefetch?: boolean
|
prefetch?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
onVisible?: () => void
|
onVisible?: () => void
|
||||||
} & PhotoSetAttributes) {
|
} & PhotoSetCategory) {
|
||||||
const ref = useRef<HTMLAnchorElement>(null);
|
const ref = useRef<HTMLAnchorElement>(null);
|
||||||
|
|
||||||
useOnVisible(ref, onVisible);
|
useOnVisible(ref, onVisible);
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Photo,
|
Photo,
|
||||||
PhotoSetAttributes,
|
PhotoSetCategory,
|
||||||
getNextPhoto,
|
getNextPhoto,
|
||||||
getPreviousPhoto,
|
getPreviousPhoto,
|
||||||
} from '@/photo';
|
} from '@/photo';
|
||||||
@ -32,7 +32,7 @@ export default function PhotoPrevNext({
|
|||||||
photo?: Photo
|
photo?: Photo
|
||||||
photos?: Photo[]
|
photos?: Photo[]
|
||||||
className?: string
|
className?: string
|
||||||
} & PhotoSetAttributes) {
|
} & PhotoSetCategory) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
import PhotoOGTile from '@/photo/PhotoOGTile';
|
import PhotoOGTile from '@/photo/PhotoOGTile';
|
||||||
import { absolutePathForPhoto, pathForPhoto } from '@/site/paths';
|
import { absolutePathForPhoto } from '@/site/paths';
|
||||||
import { Photo, PhotoSetAttributes } from '.';
|
import { Photo, PhotoSetCategory } from '.';
|
||||||
import ShareModal from '@/components/ShareModal';
|
import ShareModal from '@/share/ShareModal';
|
||||||
|
|
||||||
export default function PhotoShareModal(props: {
|
export default function PhotoShareModal(props: {
|
||||||
photo: Photo
|
photo: Photo
|
||||||
} & PhotoSetAttributes) {
|
} & PhotoSetCategory) {
|
||||||
return (
|
return (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
pathShare={absolutePathForPhoto(props)}
|
pathShare={absolutePathForPhoto(props)}
|
||||||
pathClose={pathForPhoto(props)}
|
|
||||||
socialText="Check out this photo"
|
socialText="Check out this photo"
|
||||||
>
|
>
|
||||||
<PhotoOGTile photo={props.photo} />
|
<PhotoOGTile photo={props.photo} />
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Photo,
|
Photo,
|
||||||
PhotoSetAttributes,
|
PhotoSetCategory,
|
||||||
altTextForPhoto,
|
altTextForPhoto,
|
||||||
doesPhotoNeedBlurCompatibility,
|
doesPhotoNeedBlurCompatibility,
|
||||||
} from '.';
|
} from '.';
|
||||||
@ -28,7 +28,7 @@ export default function PhotoSmall({
|
|||||||
className?: string
|
className?: string
|
||||||
prefetch?: boolean
|
prefetch?: boolean
|
||||||
onVisible?: () => void
|
onVisible?: () => void
|
||||||
} & PhotoSetAttributes) {
|
} & PhotoSetCategory) {
|
||||||
const ref = useRef<HTMLAnchorElement>(null);
|
const ref = useRef<HTMLAnchorElement>(null);
|
||||||
|
|
||||||
useOnVisible(ref, onVisible);
|
useOnVisible(ref, onVisible);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { PRIORITY_ORDER_ENABLED } from '@/site/config';
|
import { PRIORITY_ORDER_ENABLED } from '@/site/config';
|
||||||
import { parameterize } from '@/utility/string';
|
import { parameterize } from '@/utility/string';
|
||||||
import { PhotoSetAttributes } from '..';
|
import { PhotoSetCategory } from '..';
|
||||||
|
|
||||||
export const GENERATE_STATIC_PARAMS_LIMIT = 1000;
|
export const GENERATE_STATIC_PARAMS_LIMIT = 1000;
|
||||||
export const PHOTO_DEFAULT_LIMIT = 100;
|
export const PHOTO_DEFAULT_LIMIT = 100;
|
||||||
@ -14,7 +14,7 @@ export type GetPhotosOptions = {
|
|||||||
takenAfterInclusive?: Date
|
takenAfterInclusive?: Date
|
||||||
updatedBefore?: Date
|
updatedBefore?: Date
|
||||||
hidden?: 'exclude' | 'include' | 'only'
|
hidden?: 'exclude' | 'include' | 'only'
|
||||||
} & PhotoSetAttributes;
|
} & PhotoSetCategory;
|
||||||
|
|
||||||
export const areOptionsSensitive = (options: GetPhotosOptions) =>
|
export const areOptionsSensitive = (options: GetPhotosOptions) =>
|
||||||
options.hidden === 'include' || options.hidden === 'only';
|
options.hidden === 'include' || options.hidden === 'only';
|
||||||
|
|||||||
@ -102,7 +102,7 @@ export interface Photo extends PhotoDb {
|
|||||||
takenAtNaiveFormatted: string
|
takenAtNaiveFormatted: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PhotoSetAttributes {
|
export interface PhotoSetCategory {
|
||||||
tag?: string
|
tag?: string
|
||||||
camera?: Camera
|
camera?: Camera
|
||||||
simulation?: FilmSimulation
|
simulation?: FilmSimulation
|
||||||
@ -110,6 +110,12 @@ export interface PhotoSetAttributes {
|
|||||||
lens?: Lens // Unimplemented as a set
|
lens?: Lens // Unimplemented as a set
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PhotoSetAttributes {
|
||||||
|
photos: Photo[]
|
||||||
|
count?: number
|
||||||
|
dateRange?: PhotoDateRange
|
||||||
|
}
|
||||||
|
|
||||||
export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
|
export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
|
||||||
const photoDb = camelcaseKeys(
|
const photoDb = camelcaseKeys(
|
||||||
photoDbRaw as unknown as Record<string, unknown>,
|
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 { PiXLogo } from 'react-icons/pi';
|
||||||
import { SHOW_SOCIAL } from '@/site/config';
|
import { SHOW_SOCIAL } from '@/site/config';
|
||||||
import { generateXPostText } from '@/utility/social';
|
import { generateXPostText } from '@/utility/social';
|
||||||
|
import { useAppState } from '@/state/AppState';
|
||||||
|
|
||||||
export default function ShareModal({
|
export default function ShareModal({
|
||||||
title,
|
title,
|
||||||
pathShare,
|
pathShare,
|
||||||
pathClose,
|
|
||||||
socialText,
|
socialText,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
title?: string
|
title?: string
|
||||||
pathShare: string
|
pathShare: string
|
||||||
pathClose: string
|
|
||||||
socialText: string
|
socialText: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
|
const { setShareModalProps } = useAppState();
|
||||||
|
|
||||||
const renderIcon = (
|
const renderIcon = (
|
||||||
icon: JSX.Element,
|
icon: JSX.Element,
|
||||||
action: () => void,
|
action: () => void,
|
||||||
@ -44,7 +45,7 @@ export default function ShareModal({
|
|||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal onClosePath={pathClose}>
|
<Modal onClose={() => setShareModalProps?.(undefined)}>
|
||||||
<div className="space-y-3 md:space-y-4 w-full">
|
<div className="space-y-3 md:space-y-4 w-full">
|
||||||
{title &&
|
{title &&
|
||||||
<div className={clsx(
|
<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 { Photo, PhotoDateRange } from '@/photo';
|
||||||
import { FilmSimulation, descriptionForFilmSimulationPhotos } from '.';
|
import { FilmSimulation, descriptionForFilmSimulationPhotos } from '.';
|
||||||
import { pathForFilmSimulationShare } from '@/site/paths';
|
|
||||||
import PhotoHeader from '@/photo/PhotoHeader';
|
import PhotoHeader from '@/photo/PhotoHeader';
|
||||||
import PhotoFilmSimulation from
|
import PhotoFilmSimulation from
|
||||||
'@/simulation/PhotoFilmSimulation';
|
'@/simulation/PhotoFilmSimulation';
|
||||||
@ -28,10 +27,10 @@ export default function FilmSimulationHeader({
|
|||||||
photos, undefined, count, dateRange)}
|
photos, undefined, count, dateRange)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
sharePath={pathForFilmSimulationShare(simulation)}
|
|
||||||
indexNumber={indexNumber}
|
indexNumber={indexNumber}
|
||||||
count={count}
|
count={count}
|
||||||
dateRange={dateRange}
|
dateRange={dateRange}
|
||||||
|
includeShareButton
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import {
|
import { absolutePathForFilmSimulation } from '@/site/paths';
|
||||||
absolutePathForFilmSimulation,
|
import { PhotoSetAttributes } from '../photo';
|
||||||
pathForFilmSimulation,
|
import ShareModal from '@/share/ShareModal';
|
||||||
} from '@/site/paths';
|
|
||||||
import { Photo, PhotoDateRange } from '../photo';
|
|
||||||
import ShareModal from '@/components/ShareModal';
|
|
||||||
import FilmSimulationOGTile from './FilmSimulationOGTile';
|
import FilmSimulationOGTile from './FilmSimulationOGTile';
|
||||||
import { FilmSimulation, shareTextForFilmSimulation } from '.';
|
import { FilmSimulation, shareTextForFilmSimulation } from '.';
|
||||||
|
|
||||||
@ -14,14 +11,10 @@ export default function FilmSimulationShareModal({
|
|||||||
dateRange,
|
dateRange,
|
||||||
}: {
|
}: {
|
||||||
simulation: FilmSimulation
|
simulation: FilmSimulation
|
||||||
photos: Photo[]
|
} & PhotoSetAttributes) {
|
||||||
count?: number
|
|
||||||
dateRange?: PhotoDateRange
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
pathShare={absolutePathForFilmSimulation(simulation)}
|
pathShare={absolutePathForFilmSimulation(simulation)}
|
||||||
pathClose={pathForFilmSimulation(simulation)}
|
|
||||||
socialText={shareTextForFilmSimulation(simulation)}
|
socialText={shareTextForFilmSimulation(simulation)}
|
||||||
>
|
>
|
||||||
<FilmSimulationOGTile {...{ simulation, photos, count, dateRange }} />
|
<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 { BASE_URL, GRID_HOMEPAGE_ENABLED } from './config';
|
||||||
import { Camera } from '@/camera';
|
import { Camera } from '@/camera';
|
||||||
import { FilmSimulation } from '@/simulation';
|
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`;
|
export const PATH_API_PRESIGNED_URL = `${PATH_API_STORAGE}/presigned-url`;
|
||||||
|
|
||||||
// Modifiers
|
// Modifiers
|
||||||
const SHARE = 'share';
|
|
||||||
const EDIT = 'edit';
|
const EDIT = 'edit';
|
||||||
|
|
||||||
export const PATHS_ADMIN = [
|
export const PATHS_ADMIN = [
|
||||||
@ -75,7 +74,7 @@ export const PATHS_TO_CACHE = [
|
|||||||
...PATHS_ADMIN,
|
...PATHS_ADMIN,
|
||||||
];
|
];
|
||||||
|
|
||||||
type PhotoPathParams = { photo: PhotoOrPhotoId } & PhotoSetAttributes;
|
type PhotoPathParams = { photo: PhotoOrPhotoId } & PhotoSetCategory;
|
||||||
|
|
||||||
// Absolute paths
|
// Absolute paths
|
||||||
export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`;
|
export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`;
|
||||||
@ -113,33 +112,18 @@ export const pathForPhoto = ({
|
|||||||
? `${pathForFocalLength(focal)}/${getPhotoId(photo)}`
|
? `${pathForFocalLength(focal)}/${getPhotoId(photo)}`
|
||||||
: `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
|
: `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
|
||||||
|
|
||||||
export const pathForPhotoShare = (params: PhotoPathParams) =>
|
|
||||||
`${pathForPhoto(params)}/${SHARE}`;
|
|
||||||
|
|
||||||
export const pathForTag = (tag: string) =>
|
export const pathForTag = (tag: string) =>
|
||||||
`${PREFIX_TAG}/${tag}`;
|
`${PREFIX_TAG}/${tag}`;
|
||||||
|
|
||||||
export const pathForTagShare = (tag: string) =>
|
|
||||||
`${pathForTag(tag)}/${SHARE}`;
|
|
||||||
|
|
||||||
export const pathForCamera = ({ make, model }: Camera) =>
|
export const pathForCamera = ({ make, model }: Camera) =>
|
||||||
`${PREFIX_CAMERA}/${parameterize(make, true)}/${parameterize(model, true)}`;
|
`${PREFIX_CAMERA}/${parameterize(make, true)}/${parameterize(model, true)}`;
|
||||||
|
|
||||||
export const pathForCameraShare = (camera: Camera) =>
|
|
||||||
`${pathForCamera(camera)}/${SHARE}`;
|
|
||||||
|
|
||||||
export const pathForFilmSimulation = (simulation: FilmSimulation) =>
|
export const pathForFilmSimulation = (simulation: FilmSimulation) =>
|
||||||
`${PREFIX_FILM_SIMULATION}/${simulation}`;
|
`${PREFIX_FILM_SIMULATION}/${simulation}`;
|
||||||
|
|
||||||
export const pathForFilmSimulationShare = (simulation: FilmSimulation) =>
|
|
||||||
`${pathForFilmSimulation(simulation)}/${SHARE}`;
|
|
||||||
|
|
||||||
export const pathForFocalLength = (focal: number) =>
|
export const pathForFocalLength = (focal: number) =>
|
||||||
`${PREFIX_FOCAL_LENGTH}/${focal}mm`;
|
`${PREFIX_FOCAL_LENGTH}/${focal}mm`;
|
||||||
|
|
||||||
export const pathForFocalLengthShare = (focal: number) =>
|
|
||||||
`${pathForFocalLength(focal)}/${SHARE}`;;
|
|
||||||
|
|
||||||
export const absolutePathForPhoto = (params: PhotoPathParams) =>
|
export const absolutePathForPhoto = (params: PhotoPathParams) =>
|
||||||
`${BASE_URL}${pathForPhoto(params)}`;
|
`${BASE_URL}${pathForPhoto(params)}`;
|
||||||
|
|
||||||
@ -176,76 +160,38 @@ export const absolutePathForFocalLengthImage =
|
|||||||
export const isPathPhoto = (pathname = '') =>
|
export const isPathPhoto = (pathname = '') =>
|
||||||
new RegExp(`^${PREFIX_PHOTO}/[^/]+/?$`).test(pathname);
|
new RegExp(`^${PREFIX_PHOTO}/[^/]+/?$`).test(pathname);
|
||||||
|
|
||||||
// p/[photoId]/share
|
|
||||||
export const isPathPhotoShare = (pathname = '') =>
|
|
||||||
new RegExp(`^${PREFIX_PHOTO}/[^/]+/${SHARE}/?$`).test(pathname);
|
|
||||||
|
|
||||||
// tag/[tag]
|
// tag/[tag]
|
||||||
export const isPathTag = (pathname = '') =>
|
export const isPathTag = (pathname = '') =>
|
||||||
new RegExp(`^${PREFIX_TAG}/[^/]+/?$`).test(pathname);
|
new RegExp(`^${PREFIX_TAG}/[^/]+/?$`).test(pathname);;
|
||||||
|
|
||||||
// tag/[tag]/share
|
|
||||||
export const isPathTagShare = (pathname = '') =>
|
|
||||||
new RegExp(`^${PREFIX_TAG}/[^/]+/${SHARE}/?$`).test(pathname);
|
|
||||||
|
|
||||||
// tag/[tag]/[photoId]
|
// tag/[tag]/[photoId]
|
||||||
export const isPathTagPhoto = (pathname = '') =>
|
export const isPathTagPhoto = (pathname = '') =>
|
||||||
new RegExp(`^${PREFIX_TAG}/[^/]+/[^/]+/?$`).test(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]
|
// shot-on/[make]/[model]
|
||||||
export const isPathCamera = (pathname = '') =>
|
export const isPathCamera = (pathname = '') =>
|
||||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/?$`).test(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]
|
// shot-on/[make]/[model]/[photoId]
|
||||||
export const isPathCameraPhoto = (pathname = '') =>
|
export const isPathCameraPhoto = (pathname = '') =>
|
||||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/[^/]+/?$`).test(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]
|
// film/[simulation]
|
||||||
export const isPathFilmSimulation = (pathname = '') =>
|
export const isPathFilmSimulation = (pathname = '') =>
|
||||||
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/?$`).test(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]
|
// film/[simulation]/[photoId]
|
||||||
export const isPathFilmSimulationPhoto = (pathname = '') =>
|
export const isPathFilmSimulationPhoto = (pathname = '') =>
|
||||||
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/[^/]+/?$`).test(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]
|
// focal/[focal]
|
||||||
export const isPathFocalLength = (pathname = '') =>
|
export const isPathFocalLength = (pathname = '') =>
|
||||||
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[^/]+/?$`).test(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]
|
// focal/[focal]/[photoId]
|
||||||
export const isPathFocalLengthPhoto = (pathname = '') =>
|
export const isPathFocalLengthPhoto = (pathname = '') =>
|
||||||
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[^/]+/[^/]+/?$`).test(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) =>
|
export const checkPathPrefix = (pathname = '', prefix: string) =>
|
||||||
pathname.toLowerCase().startsWith(prefix);
|
pathname.toLowerCase().startsWith(prefix);
|
||||||
|
|
||||||
@ -274,17 +220,17 @@ export const isPathProtected = (pathname?: string) =>
|
|||||||
|
|
||||||
export const getPathComponents = (pathname = ''): {
|
export const getPathComponents = (pathname = ''): {
|
||||||
photoId?: string
|
photoId?: string
|
||||||
} & PhotoSetAttributes => {
|
} & PhotoSetCategory => {
|
||||||
const photoIdFromPhoto = pathname.match(
|
const photoIdFromPhoto = pathname.match(
|
||||||
new RegExp(`^${PREFIX_PHOTO}/([^/]+)`))?.[1];
|
new RegExp(`^${PREFIX_PHOTO}/([^/]+)`))?.[1];
|
||||||
const photoIdFromTag = pathname.match(
|
const photoIdFromTag = pathname.match(
|
||||||
new RegExp(`^${PREFIX_TAG}/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
|
new RegExp(`^${PREFIX_TAG}/[^/]+/([^/]+)`))?.[1];
|
||||||
const photoIdFromCamera = pathname.match(
|
const photoIdFromCamera = pathname.match(
|
||||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
|
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/([^/]+)`))?.[1];
|
||||||
const photoIdFromFilmSimulation = pathname.match(
|
const photoIdFromFilmSimulation = pathname.match(
|
||||||
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
|
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/([^/]+)`))?.[1];
|
||||||
const photoIdFromFocalLength = pathname.match(
|
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(
|
const tag = pathname.match(
|
||||||
new RegExp(`^${PREFIX_TAG}/([^/]+)`))?.[1];
|
new RegExp(`^${PREFIX_TAG}/([^/]+)`))?.[1];
|
||||||
const cameraMake = pathname.match(
|
const cameraMake = pathname.match(
|
||||||
@ -334,35 +280,13 @@ export const getEscapePath = (pathname?: string) => {
|
|||||||
(focal && isPathFocalLength(pathname))
|
(focal && isPathFocalLength(pathname))
|
||||||
) {
|
) {
|
||||||
return PATH_ROOT;
|
return PATH_ROOT;
|
||||||
} else if (photoId && isPathTagPhotoShare(pathname)) {
|
} else if (tag && isPathTagPhoto(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)
|
|
||||||
)) {
|
|
||||||
return pathForTag(tag);
|
return pathForTag(tag);
|
||||||
} else if (camera && (
|
} else if (camera && isPathCameraPhoto(pathname)) {
|
||||||
isPathCameraPhoto(pathname) ||
|
|
||||||
isPathCameraShare(pathname)
|
|
||||||
)) {
|
|
||||||
return pathForCamera(camera);
|
return pathForCamera(camera);
|
||||||
} else if (simulation && (
|
} else if (simulation && isPathFilmSimulationPhoto(pathname)) {
|
||||||
isPathFilmSimulationPhoto(pathname) ||
|
|
||||||
isPathFilmSimulationShare(pathname)
|
|
||||||
)) {
|
|
||||||
return pathForFilmSimulation(simulation);
|
return pathForFilmSimulation(simulation);
|
||||||
} else if (focal && (
|
} else if (focal && isPathFocalLengthPhoto(pathname)) {
|
||||||
isPathFocalLengthPhoto(pathname) ||
|
|
||||||
isPathFocalLengthShare(pathname)
|
|
||||||
)) {
|
|
||||||
return pathForFocalLength(focal);
|
return pathForFocalLength(focal);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Dispatch, SetStateAction, createContext, useContext } from 'react';
|
import { Dispatch, SetStateAction, createContext, useContext } from 'react';
|
||||||
import { AnimationConfig } from '@/components/AnimateItems';
|
import { AnimationConfig } from '@/components/AnimateItems';
|
||||||
|
import { ShareModalProps } from '@/share';
|
||||||
|
|
||||||
export interface AppStateContext {
|
export interface AppStateContext {
|
||||||
// CORE
|
// CORE
|
||||||
@ -13,9 +14,12 @@ export interface AppStateContext {
|
|||||||
clearNextPhotoAnimation?: () => void
|
clearNextPhotoAnimation?: () => void
|
||||||
shouldRespondToKeyboardCommands?: boolean
|
shouldRespondToKeyboardCommands?: boolean
|
||||||
setShouldRespondToKeyboardCommands?: Dispatch<SetStateAction<boolean>>
|
setShouldRespondToKeyboardCommands?: Dispatch<SetStateAction<boolean>>
|
||||||
|
// MODAL
|
||||||
isCommandKOpen?: boolean
|
isCommandKOpen?: boolean
|
||||||
setIsCommandKOpen?: Dispatch<SetStateAction<boolean>>
|
setIsCommandKOpen?: Dispatch<SetStateAction<boolean>>
|
||||||
// ADMIN
|
shareModalProps?: ShareModalProps
|
||||||
|
setShareModalProps?: Dispatch<SetStateAction<ShareModalProps | undefined>>
|
||||||
|
// ADMIN
|
||||||
userEmail?: string
|
userEmail?: string
|
||||||
setUserEmail?: Dispatch<SetStateAction<string | undefined>>
|
setUserEmail?: Dispatch<SetStateAction<string | undefined>>
|
||||||
isUserSignedIn?: boolean
|
isUserSignedIn?: boolean
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { getAuthAction } from '@/auth/actions';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { HIGH_DENSITY_GRID, MATTE_PHOTOS } from '@/site/config';
|
import { HIGH_DENSITY_GRID, MATTE_PHOTOS } from '@/site/config';
|
||||||
import { getPhotosHiddenMetaCachedAction } from '@/photo/actions';
|
import { getPhotosHiddenMetaCachedAction } from '@/photo/actions';
|
||||||
|
import { ShareModalProps } from '@/share';
|
||||||
|
|
||||||
export default function AppStateProvider({
|
export default function AppStateProvider({
|
||||||
children,
|
children,
|
||||||
@ -25,8 +26,11 @@ export default function AppStateProvider({
|
|||||||
useState<AnimationConfig>();
|
useState<AnimationConfig>();
|
||||||
const [shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands] =
|
const [shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands] =
|
||||||
useState(true);
|
useState(true);
|
||||||
|
// MODAL
|
||||||
const [isCommandKOpen, setIsCommandKOpen] =
|
const [isCommandKOpen, setIsCommandKOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [shareModalProps, setShareModalProps] =
|
||||||
|
useState<ShareModalProps>();
|
||||||
// ADMIN
|
// ADMIN
|
||||||
const [userEmail, setUserEmail] =
|
const [userEmail, setUserEmail] =
|
||||||
useState<string>();
|
useState<string>();
|
||||||
@ -89,8 +93,11 @@ export default function AppStateProvider({
|
|||||||
clearNextPhotoAnimation: () => setNextPhotoAnimation?.(undefined),
|
clearNextPhotoAnimation: () => setNextPhotoAnimation?.(undefined),
|
||||||
shouldRespondToKeyboardCommands,
|
shouldRespondToKeyboardCommands,
|
||||||
setShouldRespondToKeyboardCommands,
|
setShouldRespondToKeyboardCommands,
|
||||||
|
// MODAL
|
||||||
isCommandKOpen,
|
isCommandKOpen,
|
||||||
setIsCommandKOpen,
|
setIsCommandKOpen,
|
||||||
|
shareModalProps,
|
||||||
|
setShareModalProps,
|
||||||
// ADMIN
|
// ADMIN
|
||||||
userEmail,
|
userEmail,
|
||||||
setUserEmail,
|
setUserEmail,
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Photo, PhotoDateRange } from '@/photo';
|
import { Photo, PhotoDateRange } from '@/photo';
|
||||||
import PhotoTag from './PhotoTag';
|
import PhotoTag from './PhotoTag';
|
||||||
import { descriptionForTaggedPhotos, isTagFavs } from '.';
|
import { descriptionForTaggedPhotos, isTagFavs } from '.';
|
||||||
import { pathForTagShare } from '@/site/paths';
|
|
||||||
import PhotoHeader from '@/photo/PhotoHeader';
|
import PhotoHeader from '@/photo/PhotoHeader';
|
||||||
import FavsTag from './FavsTag';
|
import FavsTag from './FavsTag';
|
||||||
|
|
||||||
@ -30,10 +29,10 @@ export default function TagHeader({
|
|||||||
entityDescription={descriptionForTaggedPhotos(photos, undefined, count)}
|
entityDescription={descriptionForTaggedPhotos(photos, undefined, count)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
sharePath={pathForTagShare(tag)}
|
|
||||||
indexNumber={indexNumber}
|
indexNumber={indexNumber}
|
||||||
count={count}
|
count={count}
|
||||||
dateRange={dateRange}
|
dateRange={dateRange}
|
||||||
|
includeShareButton
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { absolutePathForTag, pathForTag } from '@/site/paths';
|
import { absolutePathForTag } from '@/site/paths';
|
||||||
import { Photo, PhotoDateRange } from '../photo';
|
import { PhotoSetAttributes } from '../photo';
|
||||||
import ShareModal from '@/components/ShareModal';
|
import ShareModal from '@/share/ShareModal';
|
||||||
import TagOGTile from './TagOGTile';
|
import TagOGTile from './TagOGTile';
|
||||||
import { shareTextForTag } from '.';
|
import { shareTextForTag } from '.';
|
||||||
|
|
||||||
@ -11,14 +11,10 @@ export default function TagShareModal({
|
|||||||
dateRange,
|
dateRange,
|
||||||
}: {
|
}: {
|
||||||
tag: string
|
tag: string
|
||||||
photos: Photo[]
|
} & PhotoSetAttributes) {
|
||||||
count?: number
|
|
||||||
dateRange?: PhotoDateRange
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
pathShare={absolutePathForTag(tag)}
|
pathShare={absolutePathForTag(tag)}
|
||||||
pathClose={pathForTag(tag)}
|
|
||||||
socialText={shareTextForTag(tag)}
|
socialText={shareTextForTag(tag)}
|
||||||
>
|
>
|
||||||
<TagOGTile {...{ tag, photos, count, dateRange }} />
|
<TagOGTile {...{ tag, photos, count, dateRange }} />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user