Merge pull request #160 from sambecker/share-modal

Move share modals to internal app state
This commit is contained in:
Sam Becker 2025-01-11 22:28:53 -06:00 committed by GitHub
commit ee913206d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 1825 additions and 2299 deletions

View File

@ -99,7 +99,7 @@ Application behavior can be changed by configuring the following environment var
#### Site meta
- `NEXT_PUBLIC_SITE_TITLE` (seen in browser tab)
- `NEXT_PUBLIC_SITE_DESCRIPTION` (seen in nav, beneath title)
- `NEXT_PUBLIC_SITE_ABOUT` (seen in grid sidebar—accepted rich formatting tags: `<b>`, `<strong>`, `<i>`, `<em>`, `<u>`, `<br>`)
- `NEXT_PUBLIC_SITE_ABOUT` (seen in grid sidebar—accepts rich formatting tags: `<b>`, `<strong>`, `<i>`, `<em>`, `<u>`, `<br>`)
#### Site behavior
- `NEXT_PUBLIC_GRID_HOMEPAGE = 1` shows grid layout on homepage

View File

@ -4,23 +4,14 @@ import {
getPathComponents,
isPathCamera,
isPathCameraPhoto,
isPathCameraPhotoShare,
isPathCameraShare,
isPathFilmSimulation,
isPathFilmSimulationPhoto,
isPathFilmSimulationPhotoShare,
isPathFilmSimulationShare,
isPathFocalLength,
isPathFocalLengthPhoto,
isPathFocalLengthPhotoShare,
isPathFocalLengthShare,
isPathPhoto,
isPathPhotoShare,
isPathProtected,
isPathTag,
isPathTagPhoto,
isPathTagPhotoShare,
isPathTagShare,
} from '@/site/paths';
import { TAG_HIDDEN } from '@/tag';
@ -32,7 +23,6 @@ const CAMERA_OBJECT = { make: CAMERA_MAKE, model: CAMERA_MODEL };
const FILM_SIMULATION = 'acros';
const FOCAL_LENGTH = 90;
const FOCAL_LENGTH_STRING = `${FOCAL_LENGTH}mm`;
const SHARE = 'share';
const PATH_ROOT = '/';
const PATH_GRID = '/grid';
@ -43,32 +33,21 @@ const PATH_OG_ALL = `${PATH_OG}/all`;
const PATH_OG_SAMPLE = `${PATH_OG}/sample`;
const PATH_PHOTO = `/p/${PHOTO_ID}`;
const PATH_PHOTO_SHARE = `${PATH_PHOTO}/${SHARE}`;
const PATH_TAG = `/tag/${TAG}`;
const PATH_TAG_SHARE = `${PATH_TAG}/${SHARE}`;
const PATH_TAG_PHOTO = `${PATH_TAG}/${PHOTO_ID}`;
const PATH_TAG_PHOTO_SHARE = `${PATH_TAG_PHOTO}/${SHARE}`;
const PATH_TAG_HIDDEN = `/tag/${TAG_HIDDEN}`;
const PATH_TAG_HIDDEN_SHARE = `${PATH_TAG_HIDDEN}/${SHARE}`;
const PATH_TAG_HIDDEN_PHOTO = `${PATH_TAG_HIDDEN}/${PHOTO_ID}`;
const PATH_TAG_HIDDEN_PHOTO_SHARE = `${PATH_TAG_HIDDEN_PHOTO}/${SHARE}`;
const PATH_CAMERA = `/shot-on/${CAMERA_MAKE}/${CAMERA_MODEL}`;
const PATH_CAMERA_SHARE = `${PATH_CAMERA}/${SHARE}`;
const PATH_CAMERA_PHOTO = `${PATH_CAMERA}/${PHOTO_ID}`;
const PATH_CAMERA_PHOTO_SHARE = `${PATH_CAMERA_PHOTO}/${SHARE}`;
const PATH_FILM_SIMULATION = `/film/${FILM_SIMULATION}`;
const PATH_FILM_SIMULATION_SHARE = `${PATH_FILM_SIMULATION}/${SHARE}`;
const PATH_FILM_SIMULATION_PHOTO = `${PATH_FILM_SIMULATION}/${PHOTO_ID}`;
const PATH_FILM_SIMULATION_PHOTO_SHARE = `${PATH_FILM_SIMULATION_PHOTO}/${SHARE}`;
const PATH_FOCAL_LENGTH = `/focal/${FOCAL_LENGTH_STRING}`;
const PATH_FOCAL_LENGTH_SHARE = `${PATH_FOCAL_LENGTH}/${SHARE}`;
const PATH_FOCAL_LENGTH_PHOTO = `${PATH_FOCAL_LENGTH}/${PHOTO_ID}`;
const PATH_FOCAL_LENGTH_PHOTO_SHARE = `${PATH_FOCAL_LENGTH_PHOTO}/${SHARE}`;
describe('Paths', () => {
it('can be protected', () => {
@ -85,49 +64,22 @@ describe('Paths', () => {
expect(isPathProtected(PATH_OG_ALL)).toBe(true);
expect(isPathProtected(PATH_OG_SAMPLE)).toBe(true);
expect(isPathProtected(PATH_TAG_HIDDEN)).toBe(true);
expect(isPathProtected(PATH_TAG_HIDDEN_SHARE)).toBe(true);
expect(isPathProtected(PATH_TAG_HIDDEN_PHOTO)).toBe(true);
expect(isPathProtected(PATH_TAG_HIDDEN_PHOTO_SHARE)).toBe(true);
});
it('can be classified', () => {
// Positive
expect(isPathPhoto(PATH_PHOTO)).toBe(true);
expect(isPathPhotoShare(PATH_PHOTO_SHARE)).toBe(true);
expect(isPathTag(PATH_TAG)).toBe(true);
expect(isPathTagShare(PATH_TAG_SHARE)).toBe(true);
expect(isPathTagPhoto(PATH_TAG_PHOTO)).toBe(true);
expect(isPathTagPhotoShare(PATH_TAG_PHOTO_SHARE)).toBe(true);
expect(isPathCamera(PATH_CAMERA)).toBe(true);
expect(isPathCameraShare(PATH_CAMERA_SHARE)).toBe(true);
expect(isPathCameraPhoto(PATH_CAMERA_PHOTO)).toBe(true);
expect(isPathCameraPhotoShare(PATH_CAMERA_PHOTO_SHARE)).toBe(true);
expect(isPathFilmSimulation(PATH_FILM_SIMULATION)).toBe(true);
expect(isPathFilmSimulationShare(PATH_FILM_SIMULATION_SHARE)).toBe(true);
expect(isPathFilmSimulationPhoto(PATH_FILM_SIMULATION_PHOTO)).toBe(true);
expect(isPathFilmSimulationPhotoShare(PATH_FILM_SIMULATION_PHOTO_SHARE)).toBe(true);
expect(isPathFocalLength(PATH_FOCAL_LENGTH)).toBe(true);
expect(isPathFocalLengthShare(PATH_FOCAL_LENGTH_SHARE)).toBe(true);
expect(isPathFocalLengthPhoto(PATH_FOCAL_LENGTH_PHOTO)).toBe(true);
expect(isPathFocalLengthPhotoShare(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toBe(true);
// Negative
expect(isPathPhoto(PATH_TAG_PHOTO_SHARE)).toBe(false);
expect(isPathPhotoShare(PATH_TAG_PHOTO)).toBe(false);
expect(isPathTag(PATH_TAG_SHARE)).toBe(false);
expect(isPathTagShare(PATH_TAG)).toBe(false);
expect(isPathTagPhoto(PATH_PHOTO_SHARE)).toBe(false);
expect(isPathTagPhotoShare(PATH_PHOTO)).toBe(false);
expect(isPathCamera(PATH_TAG_SHARE)).toBe(false);
expect(isPathCameraShare(PATH_TAG)).toBe(false);
expect(isPathCameraPhoto(PATH_PHOTO_SHARE)).toBe(false);
expect(isPathCameraPhotoShare(PATH_PHOTO)).toBe(false);
expect(isPathFilmSimulation(PATH_TAG_SHARE)).toBe(false);
expect(isPathFilmSimulationShare(PATH_TAG)).toBe(false);
expect(isPathFilmSimulationPhoto(PATH_PHOTO_SHARE)).toBe(false);
expect(isPathFilmSimulationPhotoShare(PATH_PHOTO)).toBe(false);
expect(isPathFocalLength(PATH_FILM_SIMULATION)).toBe(false);
expect(isPathFocalLengthShare(PATH_FILM_SIMULATION_SHARE)).toBe(false);
expect(isPathFocalLengthPhoto(PATH_FILM_SIMULATION_PHOTO)).toBe(false);
expect(isPathFocalLengthPhotoShare(PATH_FILM_SIMULATION_PHOTO_SHARE)).toBe(false);
});
it('can be parsed', () => {
// Core
@ -135,69 +87,38 @@ describe('Paths', () => {
expect(getPathComponents(PATH_PHOTO)).toEqual({
photoId: PHOTO_ID,
});
expect(getPathComponents(PATH_PHOTO_SHARE)).toEqual({
photoId: PHOTO_ID,
});
// Tag
expect(getPathComponents(PATH_TAG)).toEqual({
tag: TAG,
});
expect(getPathComponents(PATH_TAG_SHARE)).toEqual({
tag: TAG,
});
expect(getPathComponents(PATH_TAG_PHOTO)).toEqual({
photoId: PHOTO_ID,
tag: TAG,
});
expect(getPathComponents(PATH_TAG_PHOTO_SHARE)).toEqual({
photoId: PHOTO_ID,
tag: TAG,
});
// Camera
expect(getPathComponents(PATH_CAMERA)).toEqual({
camera: CAMERA_OBJECT,
});
expect(getPathComponents(PATH_CAMERA_SHARE)).toEqual({
camera: CAMERA_OBJECT,
});
expect(getPathComponents(PATH_CAMERA_PHOTO)).toEqual({
photoId: PHOTO_ID,
camera: CAMERA_OBJECT,
});
expect(getPathComponents(PATH_CAMERA_PHOTO_SHARE)).toEqual({
photoId: PHOTO_ID,
camera: CAMERA_OBJECT,
});
// Film Simulation
expect(getPathComponents(PATH_FILM_SIMULATION)).toEqual({
simulation: FILM_SIMULATION,
});
expect(getPathComponents(PATH_FILM_SIMULATION_SHARE)).toEqual({
simulation: FILM_SIMULATION,
});
expect(getPathComponents(PATH_FILM_SIMULATION_PHOTO)).toEqual({
photoId: PHOTO_ID,
simulation: FILM_SIMULATION,
});
expect(getPathComponents(PATH_FILM_SIMULATION_PHOTO_SHARE)).toEqual({
photoId: PHOTO_ID,
simulation: FILM_SIMULATION,
});
// Focal Length
expect(getPathComponents(PATH_FOCAL_LENGTH)).toEqual({
focal: FOCAL_LENGTH,
});
expect(getPathComponents(PATH_FOCAL_LENGTH_SHARE)).toEqual({
focal: FOCAL_LENGTH,
});
expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO)).toEqual({
photoId: PHOTO_ID,
focal: FOCAL_LENGTH,
});
expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toEqual({
photoId: PHOTO_ID,
focal: FOCAL_LENGTH,
});
});
it('can be escaped', () => {
// Root
@ -207,26 +128,17 @@ describe('Paths', () => {
expect(getEscapePath(PATH_ADMIN)).toEqual(undefined);
// Photo
expect(getEscapePath(PATH_PHOTO)).toEqual(PATH_ROOT);
expect(getEscapePath(PATH_PHOTO_SHARE)).toEqual(PATH_PHOTO);
// Tag
expect(getEscapePath(PATH_TAG)).toEqual(PATH_ROOT);
expect(getEscapePath(PATH_TAG_SHARE)).toEqual(PATH_TAG);
expect(getEscapePath(PATH_TAG_PHOTO)).toEqual(PATH_TAG);
expect(getEscapePath(PATH_TAG_PHOTO_SHARE)).toEqual(PATH_TAG_PHOTO);
// Camera
expect(getEscapePath(PATH_CAMERA)).toEqual(PATH_ROOT);
expect(getEscapePath(PATH_CAMERA_SHARE)).toEqual(PATH_CAMERA);
expect(getEscapePath(PATH_CAMERA_PHOTO)).toEqual(PATH_CAMERA);
expect(getEscapePath(PATH_CAMERA_PHOTO_SHARE)).toEqual(PATH_CAMERA_PHOTO);
// Film Simulation
expect(getEscapePath(PATH_FILM_SIMULATION)).toEqual(PATH_ROOT);
expect(getEscapePath(PATH_FILM_SIMULATION_SHARE)).toEqual(PATH_FILM_SIMULATION);
expect(getEscapePath(PATH_FILM_SIMULATION_PHOTO)).toEqual(PATH_FILM_SIMULATION);
expect(getEscapePath(PATH_FILM_SIMULATION_PHOTO_SHARE)).toEqual(PATH_FILM_SIMULATION_PHOTO);
// Focal Length
expect(getEscapePath(PATH_FOCAL_LENGTH)).toEqual(PATH_ROOT);
expect(getEscapePath(PATH_FOCAL_LENGTH_SHARE)).toEqual(PATH_FOCAL_LENGTH);
expect(getEscapePath(PATH_FOCAL_LENGTH_PHOTO)).toEqual(PATH_FOCAL_LENGTH);
expect(getEscapePath(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toEqual(PATH_FOCAL_LENGTH_PHOTO);
});
});

View File

@ -14,7 +14,7 @@ const eslintConfig = [
rules: {
'@next/next/no-img-element': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'no-unused-expressions': 'warn',
'no-unused-expressions': ['warn'],
'@typescript-eslint/no-unused-vars': [
'warn', {
'argsIgnorePattern': '^_',

View File

@ -9,9 +9,9 @@
"analyze": "ANALYZE=true next build"
},
"dependencies": {
"@ai-sdk/openai": "^1.0.13",
"@aws-sdk/client-s3": "3.722.0",
"@aws-sdk/s3-request-presigner": "3.722.0",
"@ai-sdk/openai": "^1.0.18",
"@aws-sdk/client-s3": "3.726.1",
"@aws-sdk/s3-request-presigner": "3.726.1",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-visually-hidden": "^1.1.1",
@ -20,14 +20,14 @@
"@vercel/blob": "^0.27.0",
"@vercel/kv": "^3.0.0",
"@vercel/speed-insights": "^1.1.0",
"ai": "^4.0.26",
"ai": "^4.0.33",
"camelcase-keys": "^9.1.3",
"cmdk": "^1.0.4",
"date-fns": "^4.1.0",
"exifr": "^7.1.3",
"framer-motion": "^11.15.0",
"framer-motion": "^11.17.0",
"nanoid": "^5.0.9",
"next": "15.1.3",
"next": "15.1.4",
"next-auth": "5.0.0-beta.25",
"next-themes": "^0.4.4",
"pg": "^8.13.1",
@ -42,25 +42,25 @@
"use-debounce": "^10.0.4"
},
"devDependencies": {
"@next/bundle-analyzer": "15.1.3",
"@next/bundle-analyzer": "15.1.4",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/forms": "^0.5.10",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.5",
"@types/pg": "^8.11.10",
"@types/react": "19.0.2",
"@types/react": "19.0.4",
"@types/react-dom": "19.0.2",
"@types/sanitize-html": "^2.13.0",
"autoprefixer": "10.4.20",
"clsx": "^2.1.1",
"eslint": "9.17.0",
"eslint-config-next": "15.1.3",
"eslint": "9.18.0",
"eslint-config-next": "15.1.4",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "8.4.49",
"tailwindcss": "3.4.17",
"typescript": "5.7.2"
"typescript": "5.7.3"
}
}

2305
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -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,
}} />
</>;
}

View File

@ -1,3 +1,89 @@
export default function Page() {
return null;
import {
RELATED_GRID_PHOTOS_TO_SHOW,
descriptionForPhoto,
titleForPhoto,
} from '@/photo';
import { Metadata } from 'next/types';
import { redirect } from 'next/navigation';
import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/site/paths';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { FilmSimulation } from '@/simulation';
import {
getPhotosMetaCached,
getPhotosNearIdCached,
} from '@/photo/cache';
import { cache } from 'react';
const getPhotosNearIdCachedCached = cache((
photoId: string,
simulation: FilmSimulation,
) =>
getPhotosNearIdCached(
photoId,
{ simulation, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 },
));
interface PhotoFilmSimulationProps {
params: Promise<{ photoId: string, simulation: FilmSimulation }>
}
export async function generateMetadata({
params,
}: PhotoFilmSimulationProps): Promise<Metadata> {
const { photoId, simulation } = await params;
const { photo } = await getPhotosNearIdCachedCached(photoId, simulation);
if (!photo) { return {}; }
const title = titleForPhoto(photo);
const description = descriptionForPhoto(photo);
const images = absolutePathForPhotoImage(photo);
const url = absolutePathForPhoto({ photo, simulation });
return {
title,
description,
openGraph: {
title,
images,
description,
url,
},
twitter: {
title,
description,
images,
card: 'summary_large_image',
},
};
}
export default async function PhotoFilmSimulationPage({
params,
}: PhotoFilmSimulationProps) {
const { photoId, simulation } = await params;
const { photo, photos, photosGrid, indexNumber } =
await getPhotosNearIdCachedCached(photoId, simulation);
if (!photo) { redirect(PATH_ROOT); }
const { count, dateRange } = await getPhotosMetaCached({ simulation });
return (
<PhotoDetailPage {...{
photo,
photos,
photosGrid,
simulation,
indexNumber,
count,
dateRange,
}} />
);
}

View File

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

View File

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

View File

@ -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,
}} />
</>;
}

View File

@ -1,3 +1,88 @@
export default function Page() {
return null;
import {
RELATED_GRID_PHOTOS_TO_SHOW,
descriptionForPhoto,
titleForPhoto,
} from '@/photo';
import { Metadata } from 'next/types';
import { redirect } from 'next/navigation';
import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/site/paths';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { getPhotosNearIdCached } from '@/photo/cache';
import { cache } from 'react';
import { getPhotosMeta } from '@/photo/db/query';
import { getFocalLengthFromString } from '@/focal';
const getPhotosNearIdCachedCached = cache((photoId: string, focal: number) =>
getPhotosNearIdCached(
photoId,
{ focal, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 },
));
interface PhotoFocalLengthProps {
params: Promise<{ photoId: string, focal: string }>
}
export async function generateMetadata({
params,
}: PhotoFocalLengthProps): Promise<Metadata> {
const { photoId, focal: focalString } = await params;
const focal = getFocalLengthFromString(focalString);
const { photo } = await getPhotosNearIdCachedCached(photoId, focal);
if (!photo) { return {}; }
const title = titleForPhoto(photo);
const description = descriptionForPhoto(photo);
const images = absolutePathForPhotoImage(photo);
const url = absolutePathForPhoto({ photo, focal });
return {
title,
description,
openGraph: {
title,
images,
description,
url,
},
twitter: {
title,
description,
images,
card: 'summary_large_image',
},
};
}
export default async function PhotoFocalLengthPage({
params,
}: PhotoFocalLengthProps) {
const { photoId, focal: focalString } = await params;
const focal = getFocalLengthFromString(focalString);
const { photo, photos, photosGrid, indexNumber } =
await getPhotosNearIdCachedCached(photoId, focal);
if (!photo) { redirect(PATH_ROOT); }
const { count, dateRange } = await getPhotosMeta({ focal });
return (
<PhotoDetailPage {...{
photo,
photos,
photosGrid,
focal,
indexNumber,
count,
dateRange,
}} />
);
}

View File

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

View File

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

View File

@ -19,6 +19,7 @@ import Footer from '@/site/Footer';
import CommandK from '@/site/CommandK';
import SwrConfigClient from '../state/SwrConfigClient';
import AdminBatchEditPanel from '@/admin/AdminBatchEditPanel';
import ShareModals from '@/share/ShareModals';
import '../site/globals.css';
import '../site/sonner.css';
@ -97,6 +98,7 @@ export default function RootLayout({
'min-h-[16rem] sm:min-h-[30rem]',
'mb-12',
)}>
<ShareModals />
{children}
</div>
<Footer />

View File

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

View File

@ -1,3 +1,82 @@
export default function Page() {
return null;
import {
RELATED_GRID_PHOTOS_TO_SHOW,
descriptionForPhoto,
titleForPhoto,
} from '@/photo';
import { Metadata } from 'next/types';
import { redirect } from 'next/navigation';
import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/site/paths';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { getPhotosNearIdCached } from '@/photo/cache';
import { IS_PRODUCTION, STATICALLY_OPTIMIZED_PAGES } from '@/site/config';
import { getPhotoIds } from '@/photo/db/query';
import { GENERATE_STATIC_PARAMS_LIMIT } from '@/photo/db';
import { cache } from 'react';
export const maxDuration = 60;
const getPhotosNearIdCachedCached = cache((photoId: string) =>
getPhotosNearIdCached(photoId, { limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 }));
export let generateStaticParams:
(() => Promise<{ photoId: string }[]>) | undefined = undefined;
if (STATICALLY_OPTIMIZED_PAGES && IS_PRODUCTION) {
generateStaticParams = async () => {
const photos = await getPhotoIds({ limit: GENERATE_STATIC_PARAMS_LIMIT });
return photos.map(photoId => ({ photoId }));
};
}
interface PhotoProps {
params: Promise<{ photoId: string }>
}
export async function generateMetadata({
params,
}:PhotoProps): Promise<Metadata> {
const { photoId } = await params;
const { photo } = await getPhotosNearIdCachedCached(photoId);
if (!photo) { return {}; }
const title = titleForPhoto(photo);
const description = descriptionForPhoto(photo);
const images = absolutePathForPhotoImage(photo);
const url = absolutePathForPhoto({ photo });
return {
title,
description,
openGraph: {
title,
images,
description,
url,
},
twitter: {
title,
description,
images,
card: 'summary_large_image',
},
};
}
export default async function PhotoPage({
params,
}: PhotoProps) {
const { photoId } = await params;
const { photo, photos, photosGrid } =
await getPhotosNearIdCachedCached(photoId);
if (!photo) { redirect(PATH_ROOT); }
return (
<PhotoDetailPage {...{ photo, photos, photosGrid }} />
);
}

View File

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

View File

@ -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,
}} />
</>;
}

View File

@ -1,3 +1,97 @@
export default function Page() {
return null;
import {
RELATED_GRID_PHOTOS_TO_SHOW,
descriptionForPhoto,
titleForPhoto,
} from '@/photo';
import { Metadata } from 'next/types';
import { redirect } from 'next/navigation';
import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/site/paths';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import {
getPhotosMetaCached,
getPhotosNearIdCached,
} from '@/photo/cache';
import {
PhotoCameraProps,
cameraFromPhoto,
getCameraFromParams,
} from '@/camera';
import { cache } from 'react';
const getPhotosNearIdCachedCached = cache((
photoId: string,
make: string,
model: string,
) =>
getPhotosNearIdCached(
photoId, {
camera: getCameraFromParams({ make, model }),
limit: RELATED_GRID_PHOTOS_TO_SHOW + 2,
},
));
export async function generateMetadata({
params,
}: PhotoCameraProps): Promise<Metadata> {
const { photoId, make, model } = await params;
const { photo } = await getPhotosNearIdCachedCached(photoId, make, model);
if (!photo) { return {}; }
const title = titleForPhoto(photo);
const description = descriptionForPhoto(photo);
const images = absolutePathForPhotoImage(photo);
const url = absolutePathForPhoto({
photo,
camera: cameraFromPhoto(photo, { make, model }),
});
return {
title,
description,
openGraph: {
title,
images,
description,
url,
},
twitter: {
title,
description,
images,
card: 'summary_large_image',
},
};
}
export default async function PhotoCameraPage({
params,
}: PhotoCameraProps) {
const { photoId, make, model } = await params;
const { photo, photos, photosGrid, indexNumber } =
await getPhotosNearIdCachedCached(photoId, make, model);
if (!photo) { redirect(PATH_ROOT); }
const camera = cameraFromPhoto(photo, { make, model });
const { count, dateRange } = await getPhotosMetaCached({ camera });
return (
<PhotoDetailPage {...{
photo,
photos,
photosGrid,
camera,
indexNumber,
count,
dateRange,
}} />
);
}

View File

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

View File

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

View File

@ -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,
}} />
</>;
}

View File

@ -1,3 +1,82 @@
export default function Page() {
return null;
import {
RELATED_GRID_PHOTOS_TO_SHOW,
descriptionForPhoto,
titleForPhoto,
} from '@/photo';
import { Metadata } from 'next/types';
import { redirect } from 'next/navigation';
import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/site/paths';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { getPhotosNearIdCached } from '@/photo/cache';
import { cache } from 'react';
import { getPhotosMeta } from '@/photo/db/query';
const getPhotosNearIdCachedCached = cache((photoId: string, tag: string) =>
getPhotosNearIdCached(
photoId,
{ tag, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 },
));
interface PhotoTagProps {
params: Promise<{ photoId: string, tag: string }>
}
export async function generateMetadata({
params,
}: PhotoTagProps): Promise<Metadata> {
const { photoId, tag } = await params;
const { photo } = await getPhotosNearIdCachedCached(photoId, tag);
if (!photo) { return {}; }
const title = titleForPhoto(photo);
const description = descriptionForPhoto(photo);
const images = absolutePathForPhotoImage(photo);
const url = absolutePathForPhoto({ photo, tag });
return {
title,
description,
openGraph: {
title,
images,
description,
url,
},
twitter: {
title,
description,
images,
card: 'summary_large_image',
},
};
}
export default async function PhotoTagPage({
params,
}: PhotoTagProps) {
const { photoId, tag } = await params;
const { photo, photos, photosGrid, indexNumber } =
await getPhotosNearIdCachedCached(photoId, tag);
if (!photo) { redirect(PATH_ROOT); }
const { count, dateRange } = await getPhotosMeta({ tag });
return (
<PhotoDetailPage {...{
photo,
photos,
photosGrid,
tag,
indexNumber,
count,
dateRange,
}} />
);
}

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import { Photo, PhotoDateRange } from '@/photo';
import { pathForCameraShare } from '@/site/paths';
import PhotoHeader from '@/photo/PhotoHeader';
import { Camera, cameraFromPhoto } from '.';
import PhotoCamera from './PhotoCamera';
@ -29,10 +28,10 @@ export default function CameraHeader({
descriptionForCameraPhotos(photos, undefined, count, dateRange)}
photos={photos}
selectedPhoto={selectedPhoto}
sharePath={pathForCameraShare(camera)}
indexNumber={indexNumber}
count={count}
dateRange={dateRange}
includeShareButton
/>
);
}

View File

@ -1,6 +1,6 @@
import { absolutePathForCamera, pathForCamera } from '@/site/paths';
import { Photo, PhotoDateRange } from '../photo';
import ShareModal from '@/components/ShareModal';
import { absolutePathForCamera } from '@/site/paths';
import { PhotoSetAttributes } from '../photo';
import ShareModal from '@/share/ShareModal';
import CameraOGTile from './CameraOGTile';
import { Camera } from '.';
import { shareTextForCamera } from './meta';
@ -12,14 +12,10 @@ export default function CameraShareModal({
dateRange,
}: {
camera: Camera
photos: Photo[]
count: number
dateRange?: PhotoDateRange,
}) {
} & PhotoSetAttributes) {
return (
<ShareModal
pathShare={absolutePathForCamera(camera)}
pathClose={pathForCamera(camera)}
socialText={shareTextForCamera(camera, photos)}
>
<CameraOGTile {...{ camera, photos, count, dateRange }} />

View File

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

View File

@ -1,6 +1,5 @@
import { Photo, PhotoDateRange } from '@/photo';
import { descriptionForFocalLengthPhotos } from '.';
import { pathForFocalLengthShare } from '@/site/paths';
import PhotoHeader from '@/photo/PhotoHeader';
import PhotoFocalLength from './PhotoFocalLength';
@ -30,10 +29,10 @@ export default function FocalLengthHeader({
)}
photos={photos}
selectedPhoto={selectedPhoto}
sharePath={pathForFocalLengthShare(focal)}
indexNumber={indexNumber}
count={count}
dateRange={dateRange}
includeShareButton
/>
);
}

View File

@ -1,6 +1,6 @@
import { absolutePathForFocalLength, pathForFocalLength } from '@/site/paths';
import { Photo, PhotoDateRange } from '../photo';
import ShareModal from '@/components/ShareModal';
import { absolutePathForFocalLength } from '@/site/paths';
import { PhotoSetAttributes } from '../photo';
import ShareModal from '@/share/ShareModal';
import FocalLengthOGTile from './FocalLengthOGTile';
import { shareTextFocalLength } from '.';
@ -11,14 +11,10 @@ export default function FocalLengthShareModal({
dateRange,
}: {
focal: number
photos: Photo[]
count?: number
dateRange?: PhotoDateRange
}) {
} & PhotoSetAttributes) {
return (
<ShareModal
pathShare={absolutePathForFocalLength(focal)}
pathClose={pathForFocalLength(focal)}
socialText={shareTextFocalLength(focal)}
>
<FocalLengthOGTile {...{ focal, photos, count, dateRange }} />

View File

@ -10,7 +10,7 @@ import {
import SiteGrid from '@/components/SiteGrid';
import Spinner from '@/components/Spinner';
import { getPhotosCachedAction, getPhotosAction } from '@/photo/actions';
import { Photo, PhotoSetAttributes } from '.';
import { Photo, PhotoSetCategory } from '.';
import { clsx } from 'clsx/lite';
import { useAppState } from '@/state/AppState';
import { GetPhotosOptions } from './db';
@ -45,7 +45,7 @@ export default function InfinitePhotoScroll({
onLastPhotoVisible: () => void
revalidatePhoto?: RevalidatePhoto
}) => ReactNode
} & PhotoSetAttributes) {
} & PhotoSetCategory) {
const { swrTimestamp, isUserSignedIn } = useAppState();
const key = `${swrTimestamp}-${cacheKey}`;

View File

@ -1,5 +1,5 @@
import AnimateItems from '@/components/AnimateItems';
import { Photo, PhotoDateRange, PhotoSetAttributes } from '.';
import { Photo, PhotoDateRange, PhotoSetCategory } from '.';
import PhotoLarge from './PhotoLarge';
import SiteGrid from '@/components/SiteGrid';
import PhotoGrid from './PhotoGrid';
@ -34,7 +34,7 @@ export default function PhotoDetailPage({
dateRange?: PhotoDateRange
shouldShare?: boolean
includeFavoriteInAdminMenu?: boolean
} & PhotoSetAttributes) {
} & PhotoSetCategory) {
let customHeader: JSX.Element | undefined;
if (tag) {

View File

@ -1,6 +1,6 @@
'use client';
import { Photo, PhotoSetAttributes } from '.';
import { Photo, PhotoSetCategory } from '.';
import PhotoMedium from './PhotoMedium';
import { clsx } from 'clsx/lite';
import AnimateItems from '@/components/AnimateItems';
@ -41,7 +41,7 @@ export default function PhotoGrid({
canSelect?: boolean
onLastPhotoVisible?: () => void
onAnimationComplete?: () => void
} & PhotoSetAttributes) {
} & PhotoSetCategory) {
const {
isUserSignedIn,
selectedPhotoIds,

View File

@ -4,11 +4,11 @@ import { clsx } from 'clsx/lite';
import {
Photo,
PhotoDateRange,
PhotoSetAttributes,
PhotoSetCategory,
dateRangeForPhotos,
titleForPhoto,
} from '.';
import ShareButton from '@/components/ShareButton';
import ShareButton from '@/share/ShareButton';
import AnimateItems from '@/components/AnimateItems';
import { ReactNode } from 'react';
import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid';
@ -27,21 +27,21 @@ export default function PhotoHeader({
entity,
entityVerb = 'PHOTO',
entityDescription,
sharePath,
indexNumber,
count,
dateRange,
includeShareButton,
}: {
photos: Photo[]
selectedPhoto?: Photo
entity?: ReactNode
entityVerb?: string
entityDescription?: string
sharePath?: string
indexNumber?: number
count?: number
dateRange?: PhotoDateRange
} & PhotoSetAttributes) {
includeShareButton?: boolean
} & PhotoSetCategory) {
const { isGridHighDensity } = useAppState();
const { start, end } = dateRangeForPhotos(photos, dateRange);
@ -138,10 +138,17 @@ export default function PhotoHeader({
{headerType === 'photo-set'
? <>
{entityDescription}
{sharePath &&
{includeShareButton &&
<ShareButton
photos={photos}
tag={tag}
camera={camera}
simulation={simulation}
focal={focal}
count={count}
dateRange={dateRange}
className="translate-y-[1.5px]"
path={sharePath}
prefetch
dim
/>}
</>

View File

@ -15,10 +15,9 @@ import Link from 'next/link';
import {
pathForFocalLength,
pathForPhoto,
pathForPhotoShare,
} from '@/site/paths';
import PhotoTags from '@/tag/PhotoTags';
import ShareButton from '@/components/ShareButton';
import ShareButton from '@/share/ShareButton';
import DownloadButton from '@/components/DownloadButton';
import PhotoCamera from '../camera/PhotoCamera';
import { cameraFromPhoto } from '@/camera';
@ -54,7 +53,6 @@ export default function PhotoLarge({
shouldShareCamera,
shouldShareSimulation,
shouldShareFocalLength,
shouldScrollOnShare,
includeFavoriteInAdminMenu,
onVisible,
}: {
@ -258,17 +256,14 @@ export default function PhotoLarge({
)}>
{shouldShare &&
<ShareButton
path={pathForPhotoShare({
photo,
tag: shouldShareTag ? primaryTag : undefined,
camera: shouldShareCamera ? camera : undefined,
// eslint-disable-next-line max-len
simulation: shouldShareSimulation ? photo.filmSimulation : undefined,
// eslint-disable-next-line max-len
focal: shouldShareFocalLength ? photo.focalLength : undefined,
})}
photo={photo}
tag={shouldShareTag ? primaryTag : undefined}
camera={shouldShareCamera ? camera : undefined}
// eslint-disable-next-line max-len
simulation={shouldShareSimulation? photo.filmSimulation : undefined}
// eslint-disable-next-line max-len
focal={shouldShareFocalLength ? photo.focalLength : undefined}
prefetch={prefetchRelatedLinks}
shouldScroll={shouldScrollOnShare}
/>}
{ALLOW_PUBLIC_DOWNLOADS &&
<DownloadButton

View File

@ -1,7 +1,7 @@
'use client';
import { ReactNode } from 'react';
import { Photo, PhotoSetAttributes, titleForPhoto } from '@/photo';
import { Photo, PhotoSetCategory, titleForPhoto } from '@/photo';
import Link from 'next/link';
import { AnimationConfig } from '../components/AnimateItems';
import { useAppState } from '@/state/AppState';
@ -26,7 +26,7 @@ export default function PhotoLink({
nextPhotoAnimation?: AnimationConfig
className?: string
children?: ReactNode
} & PhotoSetAttributes) {
} & PhotoSetCategory) {
const { setNextPhotoAnimation } = useAppState();
return (

View File

@ -2,7 +2,7 @@
import {
Photo,
PhotoSetAttributes,
PhotoSetCategory,
altTextForPhoto,
doesPhotoNeedBlurCompatibility,
} from '.';
@ -32,7 +32,7 @@ export default function PhotoMedium({
prefetch?: boolean
className?: string
onVisible?: () => void
} & PhotoSetAttributes) {
} & PhotoSetCategory) {
const ref = useRef<HTMLAnchorElement>(null);
useOnVisible(ref, onVisible);

View File

@ -3,7 +3,7 @@
import { useEffect } from 'react';
import {
Photo,
PhotoSetAttributes,
PhotoSetCategory,
getNextPhoto,
getPreviousPhoto,
} from '@/photo';
@ -32,7 +32,7 @@ export default function PhotoPrevNext({
photo?: Photo
photos?: Photo[]
className?: string
} & PhotoSetAttributes) {
} & PhotoSetCategory) {
const router = useRouter();
const {

View File

@ -1,15 +1,14 @@
import PhotoOGTile from '@/photo/PhotoOGTile';
import { absolutePathForPhoto, pathForPhoto } from '@/site/paths';
import { Photo, PhotoSetAttributes } from '.';
import ShareModal from '@/components/ShareModal';
import { absolutePathForPhoto } from '@/site/paths';
import { Photo, PhotoSetCategory } from '.';
import ShareModal from '@/share/ShareModal';
export default function PhotoShareModal(props: {
photo: Photo
} & PhotoSetAttributes) {
} & PhotoSetCategory) {
return (
<ShareModal
pathShare={absolutePathForPhoto(props)}
pathClose={pathForPhoto(props)}
socialText="Check out this photo"
>
<PhotoOGTile photo={props.photo} />

View File

@ -1,6 +1,6 @@
import {
Photo,
PhotoSetAttributes,
PhotoSetCategory,
altTextForPhoto,
doesPhotoNeedBlurCompatibility,
} from '.';
@ -28,7 +28,7 @@ export default function PhotoSmall({
className?: string
prefetch?: boolean
onVisible?: () => void
} & PhotoSetAttributes) {
} & PhotoSetCategory) {
const ref = useRef<HTMLAnchorElement>(null);
useOnVisible(ref, onVisible);

View File

@ -1,6 +1,6 @@
import { PRIORITY_ORDER_ENABLED } from '@/site/config';
import { parameterize } from '@/utility/string';
import { PhotoSetAttributes } from '..';
import { PhotoSetCategory } from '..';
export const GENERATE_STATIC_PARAMS_LIMIT = 1000;
export const PHOTO_DEFAULT_LIMIT = 100;
@ -14,7 +14,7 @@ export type GetPhotosOptions = {
takenAfterInclusive?: Date
updatedBefore?: Date
hidden?: 'exclude' | 'include' | 'only'
} & PhotoSetAttributes;
} & PhotoSetCategory;
export const areOptionsSensitive = (options: GetPhotosOptions) =>
options.hidden === 'include' || options.hidden === 'only';

View File

@ -102,7 +102,7 @@ export interface Photo extends PhotoDb {
takenAtNaiveFormatted: string
}
export interface PhotoSetAttributes {
export interface PhotoSetCategory {
tag?: string
camera?: Camera
simulation?: FilmSimulation
@ -110,6 +110,12 @@ export interface PhotoSetAttributes {
lens?: Lens // Unimplemented as a set
}
export interface PhotoSetAttributes {
photos: Photo[]
count?: number
dateRange?: PhotoDateRange
}
export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
const photoDb = camelcaseKeys(
photoDbRaw as unknown as Record<string, unknown>,

48
src/share/ShareButton.tsx Normal file
View 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"
/>
);
}

View File

@ -10,20 +10,21 @@ import { toastSuccess } from '@/toast';
import { PiXLogo } from 'react-icons/pi';
import { SHOW_SOCIAL } from '@/site/config';
import { generateXPostText } from '@/utility/social';
import { useAppState } from '@/state/AppState';
export default function ShareModal({
title,
pathShare,
pathClose,
socialText,
children,
}: {
title?: string
pathShare: string
pathClose: string
socialText: string
children: ReactNode
}) {
const { setShareModalProps } = useAppState();
const renderIcon = (
icon: JSX.Element,
action: () => void,
@ -44,7 +45,7 @@ export default function ShareModal({
</div>;
return (
<Modal onClosePath={pathClose}>
<Modal onClose={() => setShareModalProps?.(undefined)}>
<div className="space-y-3 md:space-y-4 w-full">
{title &&
<div className={clsx(

38
src/share/ShareModals.tsx Normal file
View 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
View 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);
}
};

View File

@ -1,6 +1,5 @@
import { Photo, PhotoDateRange } from '@/photo';
import { FilmSimulation, descriptionForFilmSimulationPhotos } from '.';
import { pathForFilmSimulationShare } from '@/site/paths';
import PhotoHeader from '@/photo/PhotoHeader';
import PhotoFilmSimulation from
'@/simulation/PhotoFilmSimulation';
@ -28,10 +27,10 @@ export default function FilmSimulationHeader({
photos, undefined, count, dateRange)}
photos={photos}
selectedPhoto={selectedPhoto}
sharePath={pathForFilmSimulationShare(simulation)}
indexNumber={indexNumber}
count={count}
dateRange={dateRange}
includeShareButton
/>
);
}

View File

@ -1,9 +1,6 @@
import {
absolutePathForFilmSimulation,
pathForFilmSimulation,
} from '@/site/paths';
import { Photo, PhotoDateRange } from '../photo';
import ShareModal from '@/components/ShareModal';
import { absolutePathForFilmSimulation } from '@/site/paths';
import { PhotoSetAttributes } from '../photo';
import ShareModal from '@/share/ShareModal';
import FilmSimulationOGTile from './FilmSimulationOGTile';
import { FilmSimulation, shareTextForFilmSimulation } from '.';
@ -14,14 +11,10 @@ export default function FilmSimulationShareModal({
dateRange,
}: {
simulation: FilmSimulation
photos: Photo[]
count?: number
dateRange?: PhotoDateRange
}) {
} & PhotoSetAttributes) {
return (
<ShareModal
pathShare={absolutePathForFilmSimulation(simulation)}
pathClose={pathForFilmSimulation(simulation)}
socialText={shareTextForFilmSimulation(simulation)}
>
<FilmSimulationOGTile {...{ simulation, photos, count, dateRange }} />

View File

@ -1,4 +1,4 @@
import { Photo, PhotoSetAttributes } from '@/photo';
import { Photo, PhotoSetCategory } from '@/photo';
import { BASE_URL, GRID_HOMEPAGE_ENABLED } from './config';
import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation';
@ -51,7 +51,6 @@ export const PATH_API_VERCEL_BLOB_UPLOAD = `${PATH_API_STORAGE}/vercel-blob`;
export const PATH_API_PRESIGNED_URL = `${PATH_API_STORAGE}/presigned-url`;
// Modifiers
const SHARE = 'share';
const EDIT = 'edit';
export const PATHS_ADMIN = [
@ -75,7 +74,7 @@ export const PATHS_TO_CACHE = [
...PATHS_ADMIN,
];
type PhotoPathParams = { photo: PhotoOrPhotoId } & PhotoSetAttributes;
type PhotoPathParams = { photo: PhotoOrPhotoId } & PhotoSetCategory;
// Absolute paths
export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`;
@ -113,33 +112,18 @@ export const pathForPhoto = ({
? `${pathForFocalLength(focal)}/${getPhotoId(photo)}`
: `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
export const pathForPhotoShare = (params: PhotoPathParams) =>
`${pathForPhoto(params)}/${SHARE}`;
export const pathForTag = (tag: string) =>
`${PREFIX_TAG}/${tag}`;
export const pathForTagShare = (tag: string) =>
`${pathForTag(tag)}/${SHARE}`;
export const pathForCamera = ({ make, model }: Camera) =>
`${PREFIX_CAMERA}/${parameterize(make, true)}/${parameterize(model, true)}`;
export const pathForCameraShare = (camera: Camera) =>
`${pathForCamera(camera)}/${SHARE}`;
export const pathForFilmSimulation = (simulation: FilmSimulation) =>
`${PREFIX_FILM_SIMULATION}/${simulation}`;
export const pathForFilmSimulationShare = (simulation: FilmSimulation) =>
`${pathForFilmSimulation(simulation)}/${SHARE}`;
export const pathForFocalLength = (focal: number) =>
`${PREFIX_FOCAL_LENGTH}/${focal}mm`;
export const pathForFocalLengthShare = (focal: number) =>
`${pathForFocalLength(focal)}/${SHARE}`;;
export const absolutePathForPhoto = (params: PhotoPathParams) =>
`${BASE_URL}${pathForPhoto(params)}`;
@ -176,76 +160,38 @@ export const absolutePathForFocalLengthImage =
export const isPathPhoto = (pathname = '') =>
new RegExp(`^${PREFIX_PHOTO}/[^/]+/?$`).test(pathname);
// p/[photoId]/share
export const isPathPhotoShare = (pathname = '') =>
new RegExp(`^${PREFIX_PHOTO}/[^/]+/${SHARE}/?$`).test(pathname);
// tag/[tag]
export const isPathTag = (pathname = '') =>
new RegExp(`^${PREFIX_TAG}/[^/]+/?$`).test(pathname);
// tag/[tag]/share
export const isPathTagShare = (pathname = '') =>
new RegExp(`^${PREFIX_TAG}/[^/]+/${SHARE}/?$`).test(pathname);
new RegExp(`^${PREFIX_TAG}/[^/]+/?$`).test(pathname);;
// tag/[tag]/[photoId]
export const isPathTagPhoto = (pathname = '') =>
new RegExp(`^${PREFIX_TAG}/[^/]+/[^/]+/?$`).test(pathname);
// tag/[tag]/[photoId]/share
export const isPathTagPhotoShare = (pathname = '') =>
new RegExp(`^${PREFIX_TAG}/[^/]+/[^/]+/${SHARE}/?$`).test(pathname);
// shot-on/[make]/[model]
export const isPathCamera = (pathname = '') =>
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/?$`).test(pathname);
// shot-on/[make]/[model]/share
export const isPathCameraShare = (pathname = '') =>
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/${SHARE}/?$`).test(pathname);
// shot-on/[make]/[model]/[photoId]
export const isPathCameraPhoto = (pathname = '') =>
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/[^/]+/?$`).test(pathname);
// shot-on/[make]/[model]/[photoId]/share
export const isPathCameraPhotoShare = (pathname = '') =>
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/[^/]+/${SHARE}/?$`).test(pathname);
// film/[simulation]
export const isPathFilmSimulation = (pathname = '') =>
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/?$`).test(pathname);
// film/[simulation]/share
export const isPathFilmSimulationShare = (pathname = '') =>
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/${SHARE}/?$`).test(pathname);
// film/[simulation]/[photoId]
export const isPathFilmSimulationPhoto = (pathname = '') =>
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/[^/]+/?$`).test(pathname);
// film/[simulation]/[photoId]/share
export const isPathFilmSimulationPhotoShare = (pathname = '') =>
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/[^/]+/${SHARE}/?$`)
.test(pathname);
// focal/[focal]
export const isPathFocalLength = (pathname = '') =>
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[^/]+/?$`).test(pathname);
// focal/[focal]/share
export const isPathFocalLengthShare = (pathname = '') =>
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[^/]+/${SHARE}/?$`).test(pathname);
// focal/[focal]/[photoId]
export const isPathFocalLengthPhoto = (pathname = '') =>
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[^/]+/[^/]+/?$`).test(pathname);
// focal/[focal]/[photoId]/share
export const isPathFocalLengthPhotoShare = (pathname = '') =>
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[^/]+/[^/]+/${SHARE}/?$`)
.test(pathname);
export const checkPathPrefix = (pathname = '', prefix: string) =>
pathname.toLowerCase().startsWith(prefix);
@ -274,17 +220,17 @@ export const isPathProtected = (pathname?: string) =>
export const getPathComponents = (pathname = ''): {
photoId?: string
} & PhotoSetAttributes => {
} & PhotoSetCategory => {
const photoIdFromPhoto = pathname.match(
new RegExp(`^${PREFIX_PHOTO}/([^/]+)`))?.[1];
const photoIdFromTag = pathname.match(
new RegExp(`^${PREFIX_TAG}/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
new RegExp(`^${PREFIX_TAG}/[^/]+/([^/]+)`))?.[1];
const photoIdFromCamera = pathname.match(
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/([^/]+)`))?.[1];
const photoIdFromFilmSimulation = pathname.match(
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/([^/]+)`))?.[1];
const photoIdFromFocalLength = pathname.match(
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[0-9]+mm/((?!${SHARE})[^/]+)`))?.[1];
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[0-9]+mm/([^/]+)`))?.[1];
const tag = pathname.match(
new RegExp(`^${PREFIX_TAG}/([^/]+)`))?.[1];
const cameraMake = pathname.match(
@ -334,35 +280,13 @@ export const getEscapePath = (pathname?: string) => {
(focal && isPathFocalLength(pathname))
) {
return PATH_ROOT;
} else if (photoId && isPathTagPhotoShare(pathname)) {
return pathForPhoto({ photo: photoId, tag });
} else if (photoId && isPathCameraPhotoShare(pathname)) {
return pathForPhoto({ photo: photoId, camera });
} else if (photoId && isPathFilmSimulationPhotoShare(pathname)) {
return pathForPhoto({ photo: photoId, simulation });
} else if (photoId && isPathFocalLengthPhotoShare(pathname)) {
return pathForPhoto({ photo: photoId, focal });
} else if (photoId && isPathPhotoShare(pathname)) {
return pathForPhoto({ photo: photoId });
} else if (tag && (
isPathTagPhoto(pathname) ||
isPathTagShare(pathname)
)) {
} else if (tag && isPathTagPhoto(pathname)) {
return pathForTag(tag);
} else if (camera && (
isPathCameraPhoto(pathname) ||
isPathCameraShare(pathname)
)) {
} else if (camera && isPathCameraPhoto(pathname)) {
return pathForCamera(camera);
} else if (simulation && (
isPathFilmSimulationPhoto(pathname) ||
isPathFilmSimulationShare(pathname)
)) {
} else if (simulation && isPathFilmSimulationPhoto(pathname)) {
return pathForFilmSimulation(simulation);
} else if (focal && (
isPathFocalLengthPhoto(pathname) ||
isPathFocalLengthShare(pathname)
)) {
} else if (focal && isPathFocalLengthPhoto(pathname)) {
return pathForFocalLength(focal);
}
};

View File

@ -1,5 +1,6 @@
import { Dispatch, SetStateAction, createContext, useContext } from 'react';
import { AnimationConfig } from '@/components/AnimateItems';
import { ShareModalProps } from '@/share';
export interface AppStateContext {
// CORE
@ -13,9 +14,12 @@ export interface AppStateContext {
clearNextPhotoAnimation?: () => void
shouldRespondToKeyboardCommands?: boolean
setShouldRespondToKeyboardCommands?: Dispatch<SetStateAction<boolean>>
// MODAL
isCommandKOpen?: boolean
setIsCommandKOpen?: Dispatch<SetStateAction<boolean>>
// ADMIN
shareModalProps?: ShareModalProps
setShareModalProps?: Dispatch<SetStateAction<ShareModalProps | undefined>>
// ADMIN
userEmail?: string
setUserEmail?: Dispatch<SetStateAction<string | undefined>>
isUserSignedIn?: boolean

View File

@ -8,6 +8,7 @@ import { getAuthAction } from '@/auth/actions';
import useSWR from 'swr';
import { HIGH_DENSITY_GRID, MATTE_PHOTOS } from '@/site/config';
import { getPhotosHiddenMetaCachedAction } from '@/photo/actions';
import { ShareModalProps } from '@/share';
export default function AppStateProvider({
children,
@ -25,8 +26,11 @@ export default function AppStateProvider({
useState<AnimationConfig>();
const [shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands] =
useState(true);
// MODAL
const [isCommandKOpen, setIsCommandKOpen] =
useState(false);
const [shareModalProps, setShareModalProps] =
useState<ShareModalProps>();
// ADMIN
const [userEmail, setUserEmail] =
useState<string>();
@ -89,8 +93,11 @@ export default function AppStateProvider({
clearNextPhotoAnimation: () => setNextPhotoAnimation?.(undefined),
shouldRespondToKeyboardCommands,
setShouldRespondToKeyboardCommands,
// MODAL
isCommandKOpen,
setIsCommandKOpen,
shareModalProps,
setShareModalProps,
// ADMIN
userEmail,
setUserEmail,

View File

@ -1,7 +1,6 @@
import { Photo, PhotoDateRange } from '@/photo';
import PhotoTag from './PhotoTag';
import { descriptionForTaggedPhotos, isTagFavs } from '.';
import { pathForTagShare } from '@/site/paths';
import PhotoHeader from '@/photo/PhotoHeader';
import FavsTag from './FavsTag';
@ -30,10 +29,10 @@ export default function TagHeader({
entityDescription={descriptionForTaggedPhotos(photos, undefined, count)}
photos={photos}
selectedPhoto={selectedPhoto}
sharePath={pathForTagShare(tag)}
indexNumber={indexNumber}
count={count}
dateRange={dateRange}
includeShareButton
/>
);
}

View File

@ -1,6 +1,6 @@
import { absolutePathForTag, pathForTag } from '@/site/paths';
import { Photo, PhotoDateRange } from '../photo';
import ShareModal from '@/components/ShareModal';
import { absolutePathForTag } from '@/site/paths';
import { PhotoSetAttributes } from '../photo';
import ShareModal from '@/share/ShareModal';
import TagOGTile from './TagOGTile';
import { shareTextForTag } from '.';
@ -11,14 +11,10 @@ export default function TagShareModal({
dateRange,
}: {
tag: string
photos: Photo[]
count?: number
dateRange?: PhotoDateRange
}) {
} & PhotoSetAttributes) {
return (
<ShareModal
pathShare={absolutePathForTag(tag)}
pathClose={pathForTag(tag)}
socialText={shareTextForTag(tag)}
>
<TagOGTile {...{ tag, photos, count, dateRange }} />