Merge pull request #228 from sambecker/universal-films

Prepare for universal films
This commit is contained in:
Sam Becker 2025-03-30 00:57:54 -05:00 committed by GitHub
commit 1a6d65add3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 552 additions and 672 deletions

View File

@ -1,11 +1,10 @@
/* eslint-disable max-len */
import {
getEscapePath,
getPathComponents,
isPathCamera,
isPathCameraPhoto,
isPathFilmSimulation,
isPathFilmSimulationPhoto,
isPathFilm,
isPathFilmPhoto,
isPathFocalLength,
isPathFocalLengthPhoto,
isPathPhoto,
@ -20,34 +19,34 @@ const TAG = 'tag-name';
const CAMERA_MAKE = 'fujifilm';
const CAMERA_MODEL = 'x-t1';
const CAMERA_OBJECT = { make: CAMERA_MAKE, model: CAMERA_MODEL };
const FILM_SIMULATION = 'acros';
const FILM = 'acros';
const FOCAL_LENGTH = 90;
const FOCAL_LENGTH_STRING = `${FOCAL_LENGTH}mm`;
const PATH_ROOT = '/';
const PATH_GRID = '/grid';
const PATH_FEED = '/feed';
const PATH_ADMIN = '/admin/photos';
const PATH_OG = '/og';
const PATH_OG_ALL = `${PATH_OG}/all`;
const PATH_OG_SAMPLE = `${PATH_OG}/sample`;
const PATH_ROOT = '/';
const PATH_GRID = '/grid';
const PATH_FEED = '/feed';
const PATH_ADMIN = '/admin/photos';
const PATH_OG = '/og';
const PATH_OG_ALL = `${PATH_OG}/all`;
const PATH_OG_SAMPLE = `${PATH_OG}/sample`;
const PATH_PHOTO = `/p/${PHOTO_ID}`;
const PATH_PHOTO = `/p/${PHOTO_ID}`;
const PATH_TAG = `/tag/${TAG}`;
const PATH_TAG_PHOTO = `${PATH_TAG}/${PHOTO_ID}`;
const PATH_TAG = `/tag/${TAG}`;
const PATH_TAG_PHOTO = `${PATH_TAG}/${PHOTO_ID}`;
const PATH_TAG_HIDDEN = `/tag/${TAG_HIDDEN}`;
const PATH_TAG_HIDDEN_PHOTO = `${PATH_TAG_HIDDEN}/${PHOTO_ID}`;
const PATH_TAG_HIDDEN = `/tag/${TAG_HIDDEN}`;
const PATH_TAG_HIDDEN_PHOTO = `${PATH_TAG_HIDDEN}/${PHOTO_ID}`;
const PATH_CAMERA = `/shot-on/${CAMERA_MAKE}/${CAMERA_MODEL}`;
const PATH_CAMERA_PHOTO = `${PATH_CAMERA}/${PHOTO_ID}`;
const PATH_CAMERA = `/shot-on/${CAMERA_MAKE}/${CAMERA_MODEL}`;
const PATH_CAMERA_PHOTO = `${PATH_CAMERA}/${PHOTO_ID}`;
const PATH_FILM_SIMULATION = `/film/${FILM_SIMULATION}`;
const PATH_FILM_SIMULATION_PHOTO = `${PATH_FILM_SIMULATION}/${PHOTO_ID}`;
const PATH_FILM = `/film/${FILM}`;
const PATH_FILM_PHOTO = `${PATH_FILM}/${PHOTO_ID}`;
const PATH_FOCAL_LENGTH = `/focal/${FOCAL_LENGTH_STRING}`;
const PATH_FOCAL_LENGTH_PHOTO = `${PATH_FOCAL_LENGTH}/${PHOTO_ID}`;
const PATH_FOCAL_LENGTH = `/focal/${FOCAL_LENGTH_STRING}`;
const PATH_FOCAL_LENGTH_PHOTO = `${PATH_FOCAL_LENGTH}/${PHOTO_ID}`;
describe('Paths', () => {
it('can be protected', () => {
@ -57,7 +56,7 @@ describe('Paths', () => {
expect(isPathProtected(PATH_TAG)).toBe(false);
expect(isPathProtected(PATH_TAG_PHOTO)).toBe(false);
expect(isPathProtected(PATH_CAMERA)).toBe(false);
expect(isPathProtected(PATH_FILM_SIMULATION)).toBe(false);
expect(isPathProtected(PATH_FILM)).toBe(false);
// Private
expect(isPathProtected(PATH_ADMIN)).toBe(true);
expect(isPathProtected(PATH_OG)).toBe(true);
@ -73,13 +72,13 @@ describe('Paths', () => {
expect(isPathTagPhoto(PATH_TAG_PHOTO)).toBe(true);
expect(isPathCamera(PATH_CAMERA)).toBe(true);
expect(isPathCameraPhoto(PATH_CAMERA_PHOTO)).toBe(true);
expect(isPathFilmSimulation(PATH_FILM_SIMULATION)).toBe(true);
expect(isPathFilmSimulationPhoto(PATH_FILM_SIMULATION_PHOTO)).toBe(true);
expect(isPathFilm(PATH_FILM)).toBe(true);
expect(isPathFilmPhoto(PATH_FILM_PHOTO)).toBe(true);
expect(isPathFocalLength(PATH_FOCAL_LENGTH)).toBe(true);
expect(isPathFocalLengthPhoto(PATH_FOCAL_LENGTH_PHOTO)).toBe(true);
// Negative
expect(isPathFocalLength(PATH_FILM_SIMULATION)).toBe(false);
expect(isPathFocalLengthPhoto(PATH_FILM_SIMULATION_PHOTO)).toBe(false);
expect(isPathFocalLength(PATH_FILM)).toBe(false);
expect(isPathFocalLengthPhoto(PATH_FILM_PHOTO)).toBe(false);
});
it('can be parsed', () => {
// Core
@ -103,13 +102,13 @@ describe('Paths', () => {
photoId: PHOTO_ID,
camera: CAMERA_OBJECT,
});
// Film Simulation
expect(getPathComponents(PATH_FILM_SIMULATION)).toEqual({
simulation: FILM_SIMULATION,
// Film
expect(getPathComponents(PATH_FILM)).toEqual({
film: FILM,
});
expect(getPathComponents(PATH_FILM_SIMULATION_PHOTO)).toEqual({
expect(getPathComponents(PATH_FILM_PHOTO)).toEqual({
photoId: PHOTO_ID,
simulation: FILM_SIMULATION,
film: FILM,
});
// Focal Length
expect(getPathComponents(PATH_FOCAL_LENGTH)).toEqual({
@ -134,9 +133,9 @@ describe('Paths', () => {
// Camera
expect(getEscapePath(PATH_CAMERA)).toEqual(PATH_ROOT);
expect(getEscapePath(PATH_CAMERA_PHOTO)).toEqual(PATH_CAMERA);
// Film Simulation
expect(getEscapePath(PATH_FILM_SIMULATION)).toEqual(PATH_ROOT);
expect(getEscapePath(PATH_FILM_SIMULATION_PHOTO)).toEqual(PATH_FILM_SIMULATION);
// Film
expect(getEscapePath(PATH_FILM)).toEqual(PATH_ROOT);
expect(getEscapePath(PATH_FILM_PHOTO)).toEqual(PATH_FILM);
// Focal Length
expect(getEscapePath(PATH_FOCAL_LENGTH)).toEqual(PATH_ROOT);
expect(getEscapePath(PATH_FOCAL_LENGTH_PHOTO)).toEqual(PATH_FOCAL_LENGTH);

View File

@ -7,7 +7,7 @@ import FieldSetWithStatus from '@/components/FieldSetWithStatus';
import AppGrid from '@/components/AppGrid';
import EntityLink from '@/components/primitives/EntityLink';
import LabeledIcon from '@/components/primitives/LabeledIcon';
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
import PhotoFilmIcon from '@/film/PhotoFilmIcon';
import { useAppState } from '@/state/AppState';
import { clsx } from 'clsx/lite';
import { useEffect, useState } from 'react';
@ -129,7 +129,7 @@ export default function ComponentsPage() {
</div>
<div>
<EntityLink
icon={<PhotoFilmSimulationIcon simulation="astia" />}
icon={<PhotoFilmIcon film="astia" />}
label="Astia/Soft"
type="icon-last"
iconWide
@ -149,7 +149,7 @@ export default function ComponentsPage() {
</div>
<div>
<EntityLink
icon={<PhotoFilmSimulationIcon simulation="astia" />}
icon={<PhotoFilmIcon film="astia" />}
label="Astia/Soft"
type="icon-last"
iconWide
@ -184,7 +184,7 @@ export default function ComponentsPage() {
</div>
<div>
<EntityLink
icon={<PhotoFilmSimulationIcon simulation="astia" />}
icon={<PhotoFilmIcon film="astia" />}
label="Astia/Soft"
type="icon-last"
iconWide

View File

@ -32,7 +32,7 @@ export default async function RecipePageEdit({
const {
recipeData,
filmSimulation,
film,
} = getPhotoWithRecipeFromPhotos(photos) ?? {};
if (count === 0) { redirect(PATH_ADMIN); }
@ -42,11 +42,11 @@ export default async function RecipePageEdit({
backPath={PATH_ADMIN_RECIPES}
backLabel="Recipes"
breadcrumb={<AdminRecipeBadge {...{ recipe, count, hideBadge: true }} />}
accessory={recipeData && filmSimulation &&
accessory={recipeData && film &&
<AdminShowRecipeButton
title={recipe}
recipe={recipeData}
simulation={filmSimulation}
film={film}
/>
}
>

View File

@ -10,7 +10,7 @@ import {
} from '@/app/config';
import ErrorNote from '@/components/ErrorNote';
import { getRecipeTitleForData } from '@/photo/db/query';
import { FilmSimulation } from '@/simulation';
import { FilmSimulation } from '@/film';
export const maxDuration = 60;
@ -49,10 +49,10 @@ export default async function UploadPage({ params }: Params) {
] = await Promise.all([
getUniqueTagsCached(),
getUniqueRecipesCached(),
formDataFromExif?.recipeData && formDataFromExif.filmSimulation
formDataFromExif?.recipeData && formDataFromExif.film
? getRecipeTitleForData(
formDataFromExif.recipeData,
formDataFromExif.filmSimulation as FilmSimulation,
formDataFromExif.film as FilmSimulation,
)
: undefined,
]);

View File

@ -5,7 +5,7 @@ import { clsx } from 'clsx/lite';
import {
FILM_SIMULATION_FORM_INPUT_OPTIONS,
} from '@/platforms/fujifilm/simulation';
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
import PhotoFilm from '@/film/PhotoFilm';
import { useEffect, useState } from 'react';
export default function FilmPage() {
@ -27,8 +27,8 @@ export default function FilmPage() {
<div className="dark:text-gray-500/50 uppercase">
Film Simulation:
</div>
<PhotoFilmSimulation
simulation={FILM_SIMULATION_FORM_INPUT_OPTIONS[index].value}
<PhotoFilm
film={FILM_SIMULATION_FORM_INPUT_OPTIONS[index].value}
type="icon-first"
/>
<div className="mt-4 text-dim relative">

View File

@ -1,15 +1,15 @@
import {
FILM_SIMULATION_FORM_INPUT_OPTIONS,
} from '@/platforms/fujifilm/simulation';
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
import PhotoFilm from '@/film/PhotoFilm';
export default function FilmPage() {
return (
<div className="space-y-1 my-12">
{FILM_SIMULATION_FORM_INPUT_OPTIONS.map(({ value }) =>
<div key={value}>
<PhotoFilmSimulation
simulation={value}
<PhotoFilm
film={value}
type="icon-first"
/>
</div>)}

View File

@ -11,7 +11,7 @@ import {
absolutePathForPhotoImage,
} from '@/app/paths';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { FilmSimulation } from '@/simulation';
import { FilmSimulation } from '@/film';
import {
getPhotosMetaCached,
getPhotosNearIdCached,
@ -20,30 +20,30 @@ import { cache } from 'react';
const getPhotosNearIdCachedCached = cache((
photoId: string,
simulation: FilmSimulation,
film: FilmSimulation,
) =>
getPhotosNearIdCached(
photoId,
{ simulation, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 },
{ film, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 },
));
interface PhotoFilmSimulationProps {
params: Promise<{ photoId: string, simulation: FilmSimulation }>
interface PhotoFilmProps {
params: Promise<{ photoId: string, film: FilmSimulation }>
}
export async function generateMetadata({
params,
}: PhotoFilmSimulationProps): Promise<Metadata> {
const { photoId, simulation } = await params;
}: PhotoFilmProps): Promise<Metadata> {
const { photoId, film } = await params;
const { photo } = await getPhotosNearIdCachedCached(photoId, simulation);
const { photo } = await getPhotosNearIdCachedCached(photoId, film);
if (!photo) { return {}; }
const title = titleForPhoto(photo);
const description = descriptionForPhoto(photo);
const images = absolutePathForPhotoImage(photo);
const url = absolutePathForPhoto({ photo, simulation });
const url = absolutePathForPhoto({ photo, film: film });
return {
title,
@ -63,24 +63,24 @@ export async function generateMetadata({
};
}
export default async function PhotoFilmSimulationPage({
export default async function PhotoFilmPage({
params,
}: PhotoFilmSimulationProps) {
const { photoId, simulation } = await params;
}: PhotoFilmProps) {
const { photoId, film } = await params;
const { photo, photos, photosGrid, indexNumber } =
await getPhotosNearIdCachedCached(photoId, simulation);
await getPhotosNearIdCachedCached(photoId, film);
if (!photo) { redirect(PATH_ROOT); }
const { count, dateRange } = await getPhotosMetaCached({ simulation });
const { count, dateRange } = await getPhotosMetaCached({ film: film });
return (
<PhotoDetailPage {...{
photo,
photos,
photosGrid,
simulation,
film: film,
indexNumber,
count,
dateRange,

View File

@ -3,34 +3,37 @@ import {
IMAGE_OG_DIMENSION_SMALL,
MAX_PHOTOS_TO_SHOW_PER_CATEGORY,
} from '@/image-response';
import FilmSimulationImageResponse from
'@/image-response/FilmSimulationImageResponse';
import { FilmSimulation } from '@/simulation';
import FilmImageResponse from
'@/image-response/FilmImageResponse';
import { FilmSimulation } from '@/film';
import { getIBMPlexMono } from '@/app/font';
import { ImageResponse } from 'next/og';
import { getImageResponseCacheControlHeaders } from '@/image-response/cache';
import { getUniqueFilmSimulations } from '@/photo/db/query';
import { getUniqueFilms } from '@/photo/db/query';
import { staticallyGenerateCategoryIfConfigured } from '@/app/static';
export const generateStaticParams = staticallyGenerateCategoryIfConfigured(
'films',
'image',
getUniqueFilmSimulations,
simulations => simulations.map(({ simulation }) => ({ simulation })),
getUniqueFilms,
films => films.map(({ film }) => ({ film })),
);
export async function GET(
_: Request,
context: { params: Promise<{ simulation: FilmSimulation }> },
context: { params: Promise<{ film: FilmSimulation }> },
) {
const { simulation } = await context.params;
const { film } = await context.params;
const [
photos,
{ fontFamily, fonts },
headers,
] = await Promise.all([
getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_PER_CATEGORY, simulation }),
getPhotosCached({
limit: MAX_PHOTOS_TO_SHOW_PER_CATEGORY,
film: film,
}),
getIBMPlexMono(),
getImageResponseCacheControlHeaders(),
]);
@ -38,8 +41,8 @@ export async function GET(
const { width, height } = IMAGE_OG_DIMENSION_SMALL;
return new ImageResponse(
<FilmSimulationImageResponse {...{
simulation,
<FilmImageResponse {...{
film,
photos,
width,
height,

View File

@ -1,38 +1,38 @@
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
import { getUniqueFilmSimulations } from '@/photo/db/query';
import { FilmSimulation, generateMetaForFilmSimulation } from '@/simulation';
import FilmSimulationOverview from '@/simulation/FilmSimulationOverview';
import { getPhotosFilmSimulationDataCached } from '@/simulation/data';
import { getUniqueFilms } from '@/photo/db/query';
import { FilmSimulation, generateMetaForFilm } from '@/film';
import FilmOverview from '@/film/FilmOverview';
import { getPhotosFilmDataCached } from '@/film/data';
import { Metadata } from 'next/types';
import { cache } from 'react';
import { PATH_ROOT } from '@/app/paths';
import { redirect } from 'next/navigation';
import { staticallyGenerateCategoryIfConfigured } from '@/app/static';
const getPhotosFilmSimulationDataCachedCached =
cache(getPhotosFilmSimulationDataCached);
const getPhotosFilmDataCachedCached =
cache(getPhotosFilmDataCached);
export const generateStaticParams = staticallyGenerateCategoryIfConfigured(
'films',
'page',
getUniqueFilmSimulations,
simulations => simulations.map(({ simulation }) => ({ simulation })),
getUniqueFilms,
films => films.map(({ film }) => ({ film })),
);
interface FilmSimulationProps {
params: Promise<{ simulation: FilmSimulation }>
interface FilmProps {
params: Promise<{ film: FilmSimulation }>
}
export async function generateMetadata({
params,
}: FilmSimulationProps): Promise<Metadata> {
const { simulation } = await params;
}: FilmProps): Promise<Metadata> {
const { film } = await params;
const [
photos,
{ count, dateRange },
] = await getPhotosFilmSimulationDataCachedCached({
simulation,
] = await getPhotosFilmDataCachedCached({
film,
limit: INFINITE_SCROLL_GRID_INITIAL,
});
@ -43,7 +43,7 @@ export async function generateMetadata({
title,
description,
images,
} = generateMetaForFilmSimulation(simulation, photos, count, dateRange);
} = generateMetaForFilm(film, photos, count, dateRange);
return {
title,
@ -62,24 +62,24 @@ export async function generateMetadata({
};
}
export default async function FilmSimulationPage({
export default async function FilmPage({
params,
}: FilmSimulationProps) {
const { simulation } = await params;
}: FilmProps) {
const { film } = await params;
const [
photos,
{ count, dateRange },
] = await getPhotosFilmSimulationDataCachedCached({
simulation,
] = await getPhotosFilmDataCachedCached({
film,
limit: INFINITE_SCROLL_GRID_INITIAL,
});
if (photos.length === 0) { redirect(PATH_ROOT); }
return (
<FilmSimulationOverview {...{
simulation,
<FilmOverview {...{
film,
photos,
count,
dateRange,

View File

@ -29,7 +29,7 @@ export default async function GridPage() {
lenses,
tags,
recipes,
simulations,
films,
focalLengths,
] = await Promise.all([
getPhotosCached()
@ -49,7 +49,7 @@ export default async function GridPage() {
cameras,
lenses,
tags,
simulations,
films,
recipes,
focalLengths,
}}

View File

@ -16,7 +16,7 @@ import { Metadata } from 'next/types';
import { ThemeProvider } from 'next-themes';
import Nav from '@/app/Nav';
import Footer from '@/app/Footer';
import CommandK from '@/app/CommandK';
import CommandK from '@/cmdk/CommandK';
import SwrConfigClient from '@/state/SwrConfigClient';
import AdminBatchEditPanel from '@/admin/AdminBatchEditPanel';
import ShareModals from '@/share/ShareModals';

View File

@ -2,14 +2,14 @@ import CameraOGTile from '@/camera/CameraOGTile';
import FocalLengthOGTile from '@/focal/FocalLengthOGTile';
import PhotoOGTile from '@/photo/PhotoOGTile';
import { getPhotosCached } from '@/photo/cache';
import FilmSimulationOGTile from '@/simulation/FilmSimulationOGTile';
import FilmOGTile from '@/film/FilmOGTile';
import { TAG_FAVS } from '@/tag';
import TagOGTile from '@/tag/TagOGTile';
const tag = 'cicadas';
const camera = { make: 'Fujifilm', model: 'X-T5' };
const cameraIcon = { make: 'Apple', model: 'iPhone 13 Pro' };
const simulation = 'acros';
const film = 'acros';
const focal = 90;
export default async function OGOverviewPage() {
@ -19,7 +19,7 @@ export default async function OGOverviewPage() {
photosTag,
photosFavs,
photosCamera,
photosSimulation,
photosFilm,
photosFocal,
] = await Promise.all([
getPhotosCached({ limit: 1 }).then(photos => photos[0])
@ -32,7 +32,7 @@ export default async function OGOverviewPage() {
.catch(() => []),
getPhotosCached({ limit: 1, camera })
.catch(() => []),
getPhotosCached({ limit: 1, simulation })
getPhotosCached({ limit: 1, film })
.catch(() => []),
getPhotosCached({ limit: 1, focal })
.catch(() => []),
@ -45,7 +45,7 @@ export default async function OGOverviewPage() {
<TagOGTile tag={tag} photos={photosTag} />
<TagOGTile tag={TAG_FAVS} photos={photosFavs} />
<CameraOGTile camera={camera} photos={photosCamera} />
<FilmSimulationOGTile simulation={simulation} photos={photosSimulation} />
<FilmOGTile film={film} photos={photosFilm} />
<FocalLengthOGTile focal={focal} photos={photosFocal} />
</div>
);

View File

@ -35,7 +35,7 @@ export default async function HomePage() {
lenses,
tags,
recipes,
simulations,
films,
focalLengths,
] = await Promise.all([
getPhotosCached()
@ -59,7 +59,7 @@ export default async function HomePage() {
lenses,
tags,
recipes,
simulations,
films,
focalLengths,
}}
/>

View File

@ -9,11 +9,11 @@ import { TbChecklist } from 'react-icons/tb';
export default function AdminShowRecipeButton({
title,
recipe,
simulation,
film,
}: {
title: string
recipe: FujifilmRecipe
simulation: FujifilmSimulation
film: FujifilmSimulation
}) {
const { setRecipeModalProps } = useAppState();
@ -26,7 +26,7 @@ export default function AdminShowRecipeButton({
onClick={() => setRecipeModalProps?.({
title,
recipe,
simulation,
film,
})}
>
Preview

View File

@ -1,7 +1,7 @@
import {
getPhotosMeta,
getUniqueCameras,
getUniqueFilmSimulations,
getUniqueFilms,
getUniqueFocalLengths,
getUniqueLenses,
getUniqueRecipes,
@ -34,7 +34,7 @@ export default async function AdminAppInsights() {
lenses,
tags,
recipes,
filmSimulations,
films,
focalLengths,
] = await Promise.all([
getPhotosMeta({ hidden: 'include' }),
@ -46,7 +46,7 @@ export default async function AdminAppInsights() {
getUniqueLenses(),
getUniqueTags(),
getUniqueRecipes(),
getUniqueFilmSimulations(),
getUniqueFilms(),
getUniqueFocalLengths(),
]);
@ -94,7 +94,7 @@ export default async function AdminAppInsights() {
lensesCount: lenses.length,
tagsCount: tags.length,
recipesCount: recipes.length,
filmSimulationsCount: filmSimulations.length,
filmsCount: films.length,
focalLengthsCount: focalLengths.length,
dateRange,
}}

View File

@ -41,7 +41,7 @@ import ScoreCardContainer from '@/components/ScoreCardContainer';
import IconLens from '@/components/icons/IconLens';
import IconCamera from '@/components/icons/IconCamera';
import IconRecipe from '@/components/icons/IconRecipe';
import IconFilmSimulation from '@/components/icons/IconFilmSimulation';
import IconFilm from '@/components/icons/IconFilm';
import IconFocalLength from '@/components/icons/IconFocalLength';
import IconTag from '@/components/icons/IconTag';
import IconPhoto from '@/components/icons/IconPhoto';
@ -96,7 +96,7 @@ export default function AdminAppInsightsClient({
lensesCount,
tagsCount,
recipesCount,
filmSimulationsCount,
filmsCount,
focalLengthsCount,
dateRange,
},
@ -487,11 +487,11 @@ export default function AdminAppInsightsClient({
/>
: null;
case 'films':
return filmSimulationsCount > 0
return filmsCount > 0
? <ScoreCardRow
key={category}
icon={<IconFilmSimulation size={15} />}
content={pluralize(filmSimulationsCount, 'film simulation')}
icon={<IconFilm size={15} />}
content={pluralize(filmsCount, 'film')}
/>
: null;
case 'focal-lengths':

View File

@ -53,7 +53,7 @@ export interface PhotoStats {
lensesCount: number
tagsCount: number
recipesCount: number
filmSimulationsCount: number
filmsCount: number
focalLengthsCount: number
dateRange?: PhotoDateRange
}

View File

@ -249,7 +249,7 @@ export const SHOW_TAGS =
CATEGORY_VISIBILITY.includes('tags');
export const SHOW_RECIPES =
CATEGORY_VISIBILITY.includes('recipes');
export const SHOW_FILM_SIMULATIONS =
export const SHOW_FILMS =
CATEGORY_VISIBILITY.includes('films');
export const SHOW_FOCAL_LENGTHS =
CATEGORY_VISIBILITY.includes('focal-lengths');

View File

@ -2,7 +2,7 @@ import { Photo } from '@/photo';
import { PhotoSetCategory } from '@/category';
import { BASE_URL, GRID_HOMEPAGE_ENABLED } from './config';
import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation';
import { FilmSimulation } from '@/film';
import { parameterize } from '@/utility/string';
import { TAG_HIDDEN } from '@/tag';
import { Lens } from '@/lens';
@ -26,7 +26,7 @@ export const PREFIX_CAMERA = '/shot-on';
export const PREFIX_LENS = '/lens';
export const PREFIX_TAG = '/tag';
export const PREFIX_RECIPE = '/recipe';
export const PREFIX_FILM_SIMULATION = '/film';
export const PREFIX_FILM = '/film';
export const PREFIX_FOCAL_LENGTH = '/focal';
// Dynamic paths
@ -34,8 +34,7 @@ const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/[photoId]`;
const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[make]/[model]`;
const PATH_LENS_DYNAMIC = `${PREFIX_LENS}/[make]/[model]`;
const PATH_TAG_DYNAMIC = `${PREFIX_TAG}/[tag]`;
// eslint-disable-next-line max-len
const PATH_FILM_SIMULATION_DYNAMIC = `${PREFIX_FILM_SIMULATION}/[simulation]`;
const PATH_FILM_DYNAMIC = `${PREFIX_FILM}/[film]`;
const PATH_FOCAL_LENGTH_DYNAMIC = `${PREFIX_FOCAL_LENGTH}/[focal]`;
const PATH_RECIPE_DYNAMIC = `${PREFIX_RECIPE}/[recipe]`;
@ -86,7 +85,7 @@ export const PATHS_TO_CACHE = [
PATH_CAMERA_DYNAMIC,
PATH_LENS_DYNAMIC,
PATH_TAG_DYNAMIC,
PATH_FILM_SIMULATION_DYNAMIC,
PATH_FILM_DYNAMIC,
PATH_FOCAL_LENGTH_DYNAMIC,
PATH_RECIPE_DYNAMIC,
...PATHS_ADMIN,
@ -121,7 +120,7 @@ export const pathForPhoto = ({
camera,
lens,
tag,
simulation,
film,
focal,
recipe,
}: PhotoPathParams) => {
@ -135,8 +134,8 @@ export const pathForPhoto = ({
prefix = pathForLens(lens);
} else if (tag) {
prefix = pathForTag(tag);
} else if (simulation) {
prefix = pathForFilmSimulation(simulation);
} else if (film) {
prefix = pathForFilm(film);
} else if (recipe) {
prefix = pathForRecipe(recipe);
} else if (focal) {
@ -152,8 +151,8 @@ export const pathForTag = (tag: string) =>
export const pathForCamera = ({ make, model }: Camera) =>
`${PREFIX_CAMERA}/${parameterize(make)}/${parameterize(model)}`;
export const pathForFilmSimulation = (simulation: FilmSimulation) =>
`${PREFIX_FILM_SIMULATION}/${simulation}`;
export const pathForFilm = (film: FilmSimulation) =>
`${PREFIX_FILM}/${film}`;
export const pathForLens = ({ make, model }: Lens) =>
make
@ -178,8 +177,8 @@ export const absolutePathForCamera= (camera: Camera) =>
export const absolutePathForLens= (lens: Lens) =>
`${BASE_URL}${pathForLens(lens)}`;
export const absolutePathForFilmSimulation = (simulation: FilmSimulation) =>
`${BASE_URL}${pathForFilmSimulation(simulation)}`;
export const absolutePathForFilm = (film: FilmSimulation) =>
`${BASE_URL}${pathForFilm(film)}`;
export const absolutePathForRecipe = (recipe: string) =>
`${BASE_URL}${pathForRecipe(recipe)}`;
@ -199,9 +198,8 @@ export const absolutePathForCameraImage= (camera: Camera) =>
export const absolutePathForLensImage= (lens: Lens) =>
`${absolutePathForLens(lens)}/image`;
export const absolutePathForFilmSimulationImage =
(simulation: FilmSimulation) =>
`${absolutePathForFilmSimulation(simulation)}/image`;
export const absolutePathForFilmImage = (film: FilmSimulation) =>
`${absolutePathForFilm(film)}/image`;
export const absolutePathForRecipeImage = (recipe: string) =>
`${absolutePathForRecipe(recipe)}/image`;
@ -230,13 +228,13 @@ export const isPathCamera = (pathname = '') =>
export const isPathCameraPhoto = (pathname = '') =>
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/[^/]+/?$`).test(pathname);
// film/[simulation]
export const isPathFilmSimulation = (pathname = '') =>
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/?$`).test(pathname);
// film/[film]
export const isPathFilm = (pathname = '') =>
new RegExp(`^${PREFIX_FILM}/[^/]+/?$`).test(pathname);
// film/[simulation]/[photoId]
export const isPathFilmSimulationPhoto = (pathname = '') =>
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/[^/]+/?$`).test(pathname);
// film/[film]/[photoId]
export const isPathFilmPhoto = (pathname = '') =>
new RegExp(`^${PREFIX_FILM}/[^/]+/[^/]+/?$`).test(pathname);
// focal/[focal]
export const isPathFocalLength = (pathname = '') =>
@ -299,8 +297,8 @@ export const getPathComponents = (pathname = ''): {
new RegExp(`^${PREFIX_TAG}/[^/]+/([^/]+)`))?.[1];
const photoIdFromCamera = pathname.match(
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/([^/]+)`))?.[1];
const photoIdFromFilmSimulation = pathname.match(
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/([^/]+)`))?.[1];
const photoIdFromFilm = pathname.match(
new RegExp(`^${PREFIX_FILM}/[^/]+/([^/]+)`))?.[1];
const photoIdFromFocalLength = pathname.match(
new RegExp(`^${PREFIX_FOCAL_LENGTH}/[0-9]+mm/([^/]+)`))?.[1];
const tag = pathname.match(
@ -309,8 +307,8 @@ export const getPathComponents = (pathname = ''): {
new RegExp(`^${PREFIX_CAMERA}/([^/]+)`))?.[1];
const cameraModel = pathname.match(
new RegExp(`^${PREFIX_CAMERA}/[^/]+/([^/]+)`))?.[1];
const simulation = pathname.match(
new RegExp(`^${PREFIX_FILM_SIMULATION}/([^/]+)`))?.[1] as FilmSimulation;
const film = pathname.match(
new RegExp(`^${PREFIX_FILM}/([^/]+)`))?.[1] as FilmSimulation;
const focalString = pathname.match(
new RegExp(`^${PREFIX_FOCAL_LENGTH}/([0-9]+)mm`))?.[1];
@ -325,12 +323,12 @@ export const getPathComponents = (pathname = ''): {
photoIdFromPhoto ||
photoIdFromTag ||
photoIdFromCamera ||
photoIdFromFilmSimulation ||
photoIdFromFilm ||
photoIdFromFocalLength
),
tag,
camera,
simulation,
film,
focal,
};
};
@ -340,7 +338,7 @@ export const getEscapePath = (pathname?: string) => {
photoId,
tag,
camera,
simulation,
film,
focal,
} = getPathComponents(pathname);
@ -348,7 +346,7 @@ export const getEscapePath = (pathname?: string) => {
(photoId && isPathPhoto(pathname)) ||
(tag && isPathTag(pathname)) ||
(camera && isPathCamera(pathname)) ||
(simulation && isPathFilmSimulation(pathname)) ||
(film && isPathFilm(pathname)) ||
(focal && isPathFocalLength(pathname))
) {
return PATH_ROOT;
@ -356,8 +354,8 @@ export const getEscapePath = (pathname?: string) => {
return pathForTag(tag);
} else if (camera && isPathCameraPhoto(pathname)) {
return pathForCamera(camera);
} else if (simulation && isPathFilmSimulationPhoto(pathname)) {
return pathForFilmSimulation(simulation);
} else if (film && isPathFilmPhoto(pathname)) {
return pathForFilm(film);
} else if (focal && isPathFocalLengthPhoto(pathname)) {
return pathForFocalLength(focal);
}

View File

@ -1,13 +1,13 @@
import {
getUniqueCameras,
getUniqueFilmSimulations,
getUniqueFilms,
getUniqueFocalLengths,
getUniqueLenses,
getUniqueRecipes,
getUniqueTags,
} from '@/photo/db/query';
import {
SHOW_FILM_SIMULATIONS,
SHOW_FILMS,
SHOW_FOCAL_LENGTHS,
SHOW_LENSES,
SHOW_RECIPES,
@ -40,8 +40,8 @@ export const getDataForCategories = () => [
.then(sortCategoriesByCount)
.catch(() => [])
: [],
SHOW_FILM_SIMULATIONS
? getUniqueFilmSimulations()
SHOW_FILMS
? getUniqueFilms()
.then(sortCategoriesByCount)
.catch(() => [])
: [],
@ -58,7 +58,7 @@ export const getCountsForCategories = async () => {
lenses,
tags,
recipes,
filmSimulations,
films,
focalLengths,
] = await Promise.all(getDataForCategories());
@ -79,8 +79,8 @@ export const getCountsForCategories = async () => {
acc[recipe.recipe] = recipe.count;
return acc;
}, {} as Record<string, number>),
filmSimulations: filmSimulations.reduce((acc, filmSimulation) => {
acc[filmSimulation.simulation] = filmSimulation.count;
films: films.reduce((acc, film) => {
acc[film.film] = film.count;
return acc;
}, {} as Record<string, number>),
focalLengths: focalLengths.reduce((acc, focalLength) => {

View File

@ -1,7 +1,7 @@
import { Photo } from '../photo';
import { Camera, Cameras } from '@/camera';
import { PhotoDateRange } from '../photo';
import { FilmSimulation, FilmSimulations } from '@/simulation';
import { FilmSimulation, Films } from '@/film';
import { Lens, Lenses } from '@/lens';
import { Tags } from '@/tag';
import { FocalLengths } from '@/focal';
@ -39,7 +39,7 @@ export interface PhotoSetCategory {
lens?: Lens
tag?: string
recipe?: string
simulation?: FilmSimulation
film?: FilmSimulation
focal?: number
}
@ -48,7 +48,7 @@ export interface PhotoSetCategories {
lenses: Lenses
tags: Tags
recipes: Recipes
simulations: FilmSimulations
films: Films
focalLengths: FocalLengths
}
@ -80,11 +80,9 @@ export const sortCategoriesByCount = <T extends { count: number }>(
const convertCategoryKeysToCategoryNames =
(categoryKeys: CategoryKeys): (keyof PhotoSetCategories)[] => {
return categoryKeys.map(key => {
return key === 'films'
? 'simulations'
: key === 'focal-lengths'
? 'focalLengths'
: key;
return key === 'focal-lengths'
? 'focalLengths'
: key;
});
};

View File

@ -4,6 +4,7 @@ import { Camera } from '@/camera';
import { Lens } from '@/lens';
import { useAppState } from '@/state/AppState';
import { useCallback } from 'react';
import { FujifilmSimulation } from '@/platforms/fujifilm/simulation';
export default function useCategoryCounts() {
const { categoriesWithCounts } = useAppState();
@ -28,9 +29,9 @@ export default function useCategoryCounts() {
return recipeCounts[recipe];
}, [categoriesWithCounts]);
const getFilmSimulationCount = useCallback((simulation: string) => {
const filmSimulationCounts = categoriesWithCounts?.filmSimulations ?? {};
return filmSimulationCounts[simulation];
const getFilmCount = useCallback((film: FujifilmSimulation) => {
const filmCounts = categoriesWithCounts?.films ?? {};
return filmCounts[film];
}, [categoriesWithCounts]);
const getFocalLengthCount = useCallback((focalLength: number) => {
@ -43,7 +44,7 @@ export default function useCategoryCounts() {
getLensCount,
getTagCount,
getRecipeCount,
getFilmSimulationCount,
getFilmCount,
getFocalLengthCount,
};
}

View File

@ -10,7 +10,7 @@ export default function useCategoryCountsForPhoto(photo: Photo) {
getLensCount,
getTagCount,
getRecipeCount,
getFilmSimulationCount,
getFilmCount,
getFocalLengthCount,
} = useCategoryCounts();
@ -25,21 +25,20 @@ export default function useCategoryCountsForPhoto(photo: Photo) {
return acc;
}, {} as Record<string, number>),
recipeCount: photo.recipeTitle ? getRecipeCount(photo.recipeTitle) : 0,
simulationCount:
photo.filmSimulation ? getFilmSimulationCount(photo.filmSimulation) : 0,
filmCount: photo.film ? getFilmCount(photo.film) : 0,
focalCount: photo.focalLength ? getFocalLengthCount(photo.focalLength) : 0,
}), [
getCameraCount,
getLensCount,
getRecipeCount,
getFilmSimulationCount,
getFilmCount,
getFocalLengthCount,
getTagCount,
camera,
lens,
photo.tags,
photo.recipeTitle,
photo.filmSimulation,
photo.film,
photo.focalLength,
]);

View File

@ -1,7 +1,7 @@
import CommandKClient from '@/components/cmdk/CommandKClient';
import CommandKClient from './CommandKClient';
import { getPhotosMetaCached } from '@/photo/cache';
import { photoQuantityText } from '@/photo';
import { ADMIN_DEBUG_TOOLS_ENABLED } from './config';
import { ADMIN_DEBUG_TOOLS_ENABLED } from '../app/config';
import { getDataForCategories } from '@/category/data';
export default async function CommandK() {
@ -11,7 +11,7 @@ export default async function CommandK() {
lenses,
tags,
recipes,
filmSimulations,
films,
focalLengths,
] = await Promise.all([
getPhotosMetaCached()
@ -24,7 +24,7 @@ export default async function CommandK() {
cameras={cameras}
lenses={lenses}
tags={tags}
simulations={filmSimulations}
films={films}
recipes={recipes}
focalLengths={focalLengths}
showDebugTools={ADMIN_DEBUG_TOOLS_ENABLED}

View File

@ -25,17 +25,17 @@ import {
PATH_ROOT,
PATH_SIGN_IN,
pathForCamera,
pathForFilmSimulation,
pathForFilm,
pathForFocalLength,
pathForLens,
pathForPhoto,
pathForRecipe,
pathForTag,
} from '../../app/paths';
import Modal from '../Modal';
} from '../app/paths';
import Modal from '../components/Modal';
import { clsx } from 'clsx/lite';
import { useDebounce } from 'use-debounce';
import Spinner from '../Spinner';
import Spinner from '../components/Spinner';
import { usePathname, useRouter } from 'next/navigation';
import { useTheme } from 'next-themes';
import { BiDesktop, BiLockAlt, BiMoon, BiSun } from 'react-icons/bi';
@ -59,20 +59,20 @@ import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
import InsightsIndicatorDot from '@/admin/insights/InsightsIndicatorDot';
import { PhotoSetCategories } from '@/category';
import { formatCameraText } from '@/camera';
import { labelForFilmSimulation } from '@/platforms/fujifilm/simulation';
import { labelForFilm } from '@/platforms/fujifilm/simulation';
import { formatFocalLength } from '@/focal';
import { formatRecipe } from '@/recipe';
import IconLens from '../icons/IconLens';
import IconLens from '../components/icons/IconLens';
import { formatLensText } from '@/lens';
import IconTag from '../icons/IconTag';
import IconCamera from '../icons/IconCamera';
import IconPhoto from '../icons/IconPhoto';
import IconRecipe from '../icons/IconRecipe';
import IconFocalLength from '../icons/IconFocalLength';
import IconFilmSimulation from '../icons/IconFilmSimulation';
import IconLock from '../icons/IconLock';
import IconTag from '../components/icons/IconTag';
import IconCamera from '../components/icons/IconCamera';
import IconPhoto from '../components/icons/IconPhoto';
import IconRecipe from '../components/icons/IconRecipe';
import IconFocalLength from '../components/icons/IconFocalLength';
import IconFilm from '../components/icons/IconFilm';
import IconLock from '../components/icons/IconLock';
import useVisualViewportHeight from '@/utility/useVisualViewport';
import useMaskedScroll from '../useMaskedScroll';
import useMaskedScroll from '../components/useMaskedScroll';
const DIALOG_TITLE = 'Global Command-K Menu';
const DIALOG_DESCRIPTION = 'For searching photos, views, and settings';
@ -112,7 +112,7 @@ export default function CommandKClient({
lenses,
tags,
recipes,
simulations,
films,
focalLengths,
showDebugTools,
footer,
@ -333,13 +333,13 @@ export default function CommandKClient({
})),
};
case 'films': return {
heading: 'Film Simulations',
accessory: <IconFilmSimulation size={14} />,
items: simulations.map(({ simulation, count }) => ({
label: labelForFilmSimulation(simulation).medium,
heading: 'Films',
accessory: <IconFilm size={14} />,
items: films.map(({ film, count }) => ({
label: labelForFilm(film).medium,
annotation: formatCount(count),
annotationAria: formatCountDescriptive(count),
path: pathForFilmSimulation(simulation),
path: pathForFilm(film),
})),
};
case 'focal-lengths': return {
@ -355,7 +355,7 @@ export default function CommandKClient({
}
})
.filter(Boolean) as CommandKSection[]
, [tagsIncludingHidden, cameras, lenses, recipes, simulations, focalLengths]);
, [tagsIncludingHidden, cameras, lenses, recipes, films, focalLengths]);
const clientSections: CommandKSection[] = [{
heading: 'Theme',

View File

@ -1,7 +1,7 @@
import { clsx } from 'clsx/lite';
import { Command } from 'cmdk';
import { ReactNode } from 'react';
import Spinner from '../Spinner';
import Spinner from '../components/Spinner';
export default function CommandKItem({
label,

View File

@ -1,6 +1,6 @@
import { IconBaseProps } from 'react-icons';
import { IoFilmOutline } from 'react-icons/io5';
export default function IconFilmSimulation(props: IconBaseProps) {
export default function IconFilm(props: IconBaseProps) {
return <IoFilmOutline {...props} />;
}

View File

@ -1,18 +1,17 @@
import { Photo, PhotoDateRange } from '@/photo';
import { FilmSimulation, descriptionForFilmSimulationPhotos } from '.';
import { FilmSimulation, descriptionForFilmPhotos } from '.';
import PhotoHeader from '@/photo/PhotoHeader';
import PhotoFilmSimulation from
'@/simulation/PhotoFilmSimulation';
import PhotoFilm from '@/film/PhotoFilm';
export default function FilmSimulationHeader({
simulation,
export default function FilmHeader({
film,
photos,
selectedPhoto,
indexNumber,
count,
dateRange,
}: {
simulation: FilmSimulation
film: FilmSimulation
photos: Photo[]
selectedPhoto?: Photo
indexNumber?: number
@ -21,9 +20,9 @@ export default function FilmSimulationHeader({
}) {
return (
<PhotoHeader
simulation={simulation}
entity={<PhotoFilmSimulation {...{ simulation }} />}
entityDescription={descriptionForFilmSimulationPhotos(
film={film}
entity={<PhotoFilm {...{ film }} />}
entityDescription={descriptionForFilmPhotos(
photos, undefined, count, dateRange)}
photos={photos}
selectedPhoto={selectedPhoto}

View File

@ -1,19 +1,19 @@
import { Photo, PhotoDateRange } from '@/photo';
import {
absolutePathForFilmSimulationImage,
pathForFilmSimulation,
absolutePathForFilmImage,
pathForFilm,
} from '@/app/paths';
import OGTile from '@/components/OGTile';
import {
FilmSimulation,
descriptionForFilmSimulationPhotos,
titleForFilmSimulation,
descriptionForFilmPhotos,
titleForFilm,
} from '.';
export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed';
export default function FilmSimulationOGTile({
simulation,
export default function FilmOGTile({
film,
photos,
loadingState: loadingStateExternal,
riseOnHover,
@ -23,7 +23,7 @@ export default function FilmSimulationOGTile({
count,
dateRange,
}: {
simulation: FilmSimulation
film: FilmSimulation
photos: Photo[]
loadingState?: OGLoadingState
onLoad?: () => void
@ -35,11 +35,11 @@ export default function FilmSimulationOGTile({
}) {
return (
<OGTile {...{
title: titleForFilmSimulation(simulation, photos, count),
title: titleForFilm(film, photos, count),
description:
descriptionForFilmSimulationPhotos(photos, true, count, dateRange),
path: pathForFilmSimulation(simulation),
pathImageAbsolute: absolutePathForFilmSimulationImage(simulation),
descriptionForFilmPhotos(photos, true, count, dateRange),
path: pathForFilm(film),
pathImageAbsolute: absolutePathForFilmImage(film),
loadingState: loadingStateExternal,
onLoad,
onFail,

View File

@ -1,16 +1,16 @@
import { Photo, PhotoDateRange } from '@/photo';
import FilmSimulationHeader from './FilmSimulationHeader';
import FilmHeader from './FilmHeader';
import { FilmSimulation } from '.';
import PhotoGridContainer from '@/photo/PhotoGridContainer';
export default function FilmSimulationOverview({
simulation,
export default function FilmOverview({
film,
photos,
count,
dateRange,
animateOnFirstLoadOnly,
}: {
simulation: FilmSimulation,
film: FilmSimulation,
photos: Photo[],
count: number,
dateRange?: PhotoDateRange,
@ -18,12 +18,12 @@ export default function FilmSimulationOverview({
}) {
return (
<PhotoGridContainer {...{
cacheKey: `simulation-${simulation}`,
cacheKey: `film-${film}`,
photos,
count,
simulation,
header: <FilmSimulationHeader {...{
simulation,
film,
header: <FilmHeader {...{
film,
photos,
count,
dateRange,

View File

@ -0,0 +1,23 @@
import { absolutePathForFilm } from '@/app/paths';
import { PhotoSetAttributes } from '../category';
import ShareModal from '@/share/ShareModal';
import FilmOGTile from './FilmOGTile';
import { FilmSimulation, shareTextForFilm } from '.';
export default function FilmShareModal({
film,
photos,
count,
dateRange,
}: {
film: FilmSimulation
} & PhotoSetAttributes) {
return (
<ShareModal
pathShare={absolutePathForFilm(film)}
socialText={shareTextForFilm(film)}
>
<FilmOGTile {...{ film, photos, count, dateRange }} />
</ShareModal>
);
};

View File

@ -1,6 +1,6 @@
import { labelForFilmSimulation } from '@/platforms/fujifilm/simulation';
import PhotoFilmSimulationIcon from './PhotoFilmSimulationIcon';
import { pathForFilmSimulation } from '@/app/paths';
import { labelForFilm } from '@/platforms/fujifilm/simulation';
import PhotoFilmIcon from './PhotoFilmIcon';
import { pathForFilm } from '@/app/paths';
import { FilmSimulation } from '.';
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
import EntityLink, {
@ -8,28 +8,28 @@ import EntityLink, {
} from '@/components/primitives/EntityLink';
import clsx from 'clsx/lite';
export default function PhotoFilmSimulation({
simulation,
export default function PhotoFilm({
film,
type = 'icon-last',
badged = true,
contrast = 'low',
countOnHover,
...props
}: {
simulation: FilmSimulation
film: FilmSimulation
countOnHover?: number
recipe?: FujifilmRecipe
} & EntityLinkExternalProps) {
const { small, medium, large } = labelForFilmSimulation(simulation);
const { small, medium, large } = labelForFilm(film);
return (
<EntityLink
{...props}
label={medium}
labelSmall={small}
href={pathForFilmSimulation(simulation)}
icon={<PhotoFilmSimulationIcon
simulation={simulation}
href={pathForFilm(film)}
icon={<PhotoFilmIcon
film={film}
className={clsx(
contrast === 'frosted' && 'text-black',
type === 'icon-only'
@ -37,7 +37,7 @@ export default function PhotoFilmSimulation({
: 'translate-y-[-1px]',
)}
/>}
title={`Film Simulation: ${large}`}
title={`Film: ${large}`}
type={type}
badged={badged}
contrast={contrast}

View File

@ -1,18 +1,18 @@
/* eslint-disable max-len */
import { labelForFilmSimulation } from '@/platforms/fujifilm/simulation';
import { labelForFilm } from '@/platforms/fujifilm/simulation';
import { CSSProperties } from 'react';
import { FilmSimulation } from '.';
const INTRINSIC_WIDTH = 28;
const INTRINSIC_HEIGHT = 16;
export default function PhotoFilmSimulationIcon({
simulation,
export default function PhotoFilmIcon({
film,
height = INTRINSIC_HEIGHT,
className,
style,
}: {
simulation?: FilmSimulation
film?: FilmSimulation
height?: number
className?: string
style?: CSSProperties
@ -21,9 +21,9 @@ export default function PhotoFilmSimulationIcon({
<svg
className={className}
style={style}
aria-description={simulation
? labelForFilmSimulation(simulation).large
: 'Film Simulation'}
aria-description={film
? labelForFilm(film).large
: 'Film'}
width={INTRINSIC_WIDTH * height / INTRINSIC_HEIGHT}
height={height}
viewBox="0 0 28 16"
@ -33,7 +33,7 @@ export default function PhotoFilmSimulationIcon({
{(() => {
// Self-calling switch function and non-fragment groups
// necessary for ImageResponse compatibility
switch (simulation) {
switch (film) {
case 'monochrome': return <g>
<path fillRule="evenodd" clipRule="evenodd" d="M16.25 14H22.5C22.6381 14 22.75 13.8881 22.75 13.75V10.5202C22.75 10.4539 22.7763 10.3903 22.8232 10.3434L25.677 7.48989C25.7238 7.44301 25.7502 7.37942 25.7502 7.31311V4.25C25.7502 4.11193 25.6383 4 25.5002 4H16.25C16.1119 4 16 4.11194 16 4.25002L16.0002 6.49998C16.0002 6.63806 15.8882 6.75 15.7502 6.75H14.7502C14.6121 6.75 14.5002 6.86192 14.5002 6.99999L14.5 11C14.5 11.1381 14.6119 11.25 14.75 11.25H15.75C15.8881 11.25 16 11.3619 16 11.5V13.75C16 13.8881 16.1119 14 16.25 14ZM18.75 5H17V6.5H18.75V5ZM17 11.5H18.75V13H17V11.5ZM21.75 5H20V6.5H21.75V5ZM20 11.5H21.75V13H20V11.5ZM24.75 5H23V6.5H24.75V5Z" fill="currentColor"/>
<path fillRule="evenodd" clipRule="evenodd" d="M5.25 3.49999L2 3.49999C1.86193 3.49999 1.75 3.61192 1.75 3.74999V5.24998C1.75 5.38806 1.86193 5.49998 2 5.49998H3.00001C3.13808 5.49998 3.25001 5.61191 3.25001 5.74999L3.25 12.25C3.25 12.3881 3.13807 12.5 3 12.5H2C1.86193 12.5 1.75 12.6119 1.75 12.75V14.25C1.75 14.3881 1.86193 14.5 2 14.5H14.25C14.3881 14.5 14.5 14.3881 14.5 14.25V12.75C14.5 12.6119 14.3881 12.5 14.25 12.5H13.25C13.1119 12.5 13 12.3881 13 12.25L13 5.75C13 5.61193 13.112 5.5 13.25 5.5H14.25C14.3881 5.5 14.5 5.38807 14.5 5.25V3.74999C14.5 3.61192 14.3881 3.49999 14.25 3.49999L11 3.49999C10.8619 3.49998 10.75 3.38806 10.75 3.24999V1.74998C10.75 1.61191 10.6381 1.49998 10.5 1.49998H5.75C5.61193 1.49998 5.5 1.61191 5.5 1.74998V3.24999C5.5 3.38806 5.38807 3.49998 5.25 3.49999ZM9.03232 4.7985H5.38982V13H9.36132C9.65899 13 9.92532 12.9412 10.1603 12.8237C10.3953 12.6984 10.5951 12.53 10.7596 12.3185C10.9241 12.107 11.0494 11.8602 11.1356 11.5782C11.2217 11.2884 11.2648 10.9829 11.2648 10.6617C11.2648 10.3328 11.2139 10.0547 11.1121 9.8275C11.0102 9.5925 10.8771 9.4045 10.7126 9.2635C10.5481 9.11467 10.364 9.00892 10.1603 8.94625C9.96449 8.88358 9.77257 8.85225 9.58457 8.85225V8.66425C9.7569 8.65642 9.92532 8.62508 10.0898 8.57025C10.2543 8.50758 10.3992 8.41358 10.5246 8.28825C10.6577 8.15508 10.7635 7.97883 10.8418 7.7595C10.928 7.53233 10.9711 7.24642 10.9711 6.90175C10.9711 6.26725 10.8105 5.75808 10.4893 5.37425C10.176 4.99042 9.69032 4.7985 9.03232 4.7985ZM8.89132 11.5782H7.07007V9.55725H8.89132C9.07932 9.55725 9.22815 9.61208 9.33782 9.72175C9.45532 9.83142 9.51407 10.0155 9.51407 10.274V10.8615C9.51407 11.12 9.45532 11.3041 9.33782 11.4137C9.22815 11.5234 9.07932 11.5782 8.89132 11.5782ZM8.62107 8.17075H7.07007V6.22025H8.62107C8.8169 6.22025 8.96965 6.27508 9.07932 6.38475C9.18899 6.49442 9.24382 6.67458 9.24382 6.92525V7.46575C9.24382 7.71642 9.18899 7.89658 9.07932 8.00625C8.96965 8.11592 8.8169 8.17075 8.62107 8.17075Z" fill="currentColor"/>

17
src/film/data.ts Normal file
View File

@ -0,0 +1,17 @@
import {
getPhotosCached,
getPhotosMetaCached,
} from '@/photo/cache';
import { FilmSimulation } from '.';
export const getPhotosFilmDataCached = ({
film,
limit,
}: {
film: FilmSimulation,
limit?: number,
}) =>
Promise.all([
getPhotosCached({ film, limit }),
getPhotosMetaCached({ film }),
]);

84
src/film/index.ts Normal file
View File

@ -0,0 +1,84 @@
import {
Photo,
PhotoDateRange,
descriptionForPhotoSet,
photoQuantityText,
} from '@/photo';
import {
absolutePathForFilm,
absolutePathForFilmImage,
} from '@/app/paths';
import {
FujifilmSimulation,
labelForFilm,
} from '@/platforms/fujifilm/simulation';
export type FilmSimulation = FujifilmSimulation;
export type FilmWithCount = {
film: FilmSimulation
count: number
}
export type Films = FilmWithCount[]
export const sortFilms = (
films: Films,
) => films.sort(sortFilmsWithCount);
export const sortFilmsWithCount = (
a: FilmWithCount,
b: FilmWithCount,
) => {
const aLabel = labelForFilm(a.film).large;
const bLabel = labelForFilm(b.film).large;
return aLabel.localeCompare(bLabel);
};
export const titleForFilm = (
film: FilmSimulation,
photos: Photo[],
explicitCount?: number,
) => [
labelForFilm(film).large,
photoQuantityText(explicitCount ?? photos.length),
].join(' ');
export const shareTextForFilm = (
film: FilmSimulation,
) =>
`Photos shot on Fujifilm ${labelForFilm(film).large}`;
export const descriptionForFilmPhotos = (
photos: Photo[],
dateBased?: boolean,
explicitCount?: number,
explicitDateRange?: PhotoDateRange,
) =>
descriptionForPhotoSet(
photos,
undefined,
dateBased,
explicitCount,
explicitDateRange,
);
export const generateMetaForFilm = (
film: FilmSimulation,
photos: Photo[],
explicitCount?: number,
explicitDateRange?: PhotoDateRange,
) => ({
url: absolutePathForFilm(film),
title: titleForFilm(film, photos, explicitCount),
description: descriptionForFilmPhotos(
photos,
true,
explicitCount,
explicitDateRange,
),
images: absolutePathForFilmImage(film),
});
export const photoHasFilmData = (photo: Photo) =>
Boolean(photo.film);

View File

@ -3,21 +3,21 @@ import ImageCaption from './components/ImageCaption';
import ImagePhotoGrid from './components/ImagePhotoGrid';
import ImageContainer from './components/ImageContainer';
import {
labelForFilmSimulation,
labelForFilm,
} from '@/platforms/fujifilm/simulation';
import PhotoFilmSimulationIcon from
'@/simulation/PhotoFilmSimulationIcon';
import { FilmSimulation } from '@/simulation';
import PhotoFilmIcon from
'@/film/PhotoFilmIcon';
import { FilmSimulation } from '@/film';
import { NextImageSize } from '@/platforms/next-image';
export default function FilmSimulationImageResponse({
simulation,
export default function FilmImageResponse({
film,
photos,
width,
height,
fontFamily,
}: {
simulation: FilmSimulation,
film: FilmSimulation,
photos: Photo[]
width: NextImageSize
height: number
@ -36,12 +36,12 @@ export default function FilmSimulationImageResponse({
width,
height,
fontFamily,
icon: <PhotoFilmSimulationIcon
simulation={simulation}
icon: <PhotoFilmIcon
film={film}
height={height * .081}
style={{ transform: `translateY(${height * .001}px)`}}
/>,
title: labelForFilmSimulation(simulation).medium.toLocaleUpperCase(),
title: labelForFilm(film).medium.toLocaleUpperCase(),
}} />
</ImageContainer>
);

View File

@ -5,7 +5,7 @@ import ImageContainer from './components/ImageContainer';
import type { NextImageSize } from '@/platforms/next-image';
import { formatTag } from '@/tag';
import { generateRecipeText, getPhotoWithRecipeFromPhotos } from '@/recipe';
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
import PhotoFilmIcon from '@/film/PhotoFilmIcon';
import { isStringFilmSimulation } from '@/platforms/fujifilm/simulation';
import IconRecipe from '@/components/icons/IconRecipe';
const MAX_RECIPE_LINES = 8;
@ -27,13 +27,13 @@ export default function RecipeImageResponse({
}) {
const {
recipeData,
filmSimulation,
film,
} = getPhotoWithRecipeFromPhotos(photos) ?? {};
let recipeLines = recipeData && filmSimulation
let recipeLines = recipeData && film
? generateRecipeText({
recipe: recipeData,
simulation: filmSimulation,
film,
}, true)
: [];
@ -109,10 +109,10 @@ export default function RecipeImageResponse({
flexGrow: 1,
}}>
{text}
{isStringFilmSimulation(text) && filmSimulation &&
{isStringFilmSimulation(text) && film &&
<div tw="flex">
<PhotoFilmSimulationIcon
simulation={filmSimulation}
<PhotoFilmIcon
film={film}
height={height * .06}
style={{ transform: `translateY(${-height * .001}px)`}}
/>

View File

@ -29,10 +29,12 @@ export default function InfinitePhotoScroll({
initialOffset,
itemsPerPage,
sortBy,
tag,
camera,
lens,
simulation,
tag,
recipe,
film,
focal,
wrapMoreButtonInGrid,
useCachedPhotos = true,
includeHiddenPhotos,
@ -73,7 +75,9 @@ export default function InfinitePhotoScroll({
camera,
lens,
tag,
simulation,
recipe,
film,
focal,
}, warmOnly)
, [
useCachedPhotos,
@ -84,7 +88,9 @@ export default function InfinitePhotoScroll({
camera,
lens,
tag,
simulation,
recipe,
film,
focal,
]);
const { data, isLoading, isValidating, error, mutate, size, setSize } =

View File

@ -6,7 +6,7 @@ import AppGrid from '@/components/AppGrid';
import PhotoGrid from './PhotoGrid';
import TagHeader from '@/tag/TagHeader';
import CameraHeader from '@/camera/CameraHeader';
import FilmSimulationHeader from '@/simulation/FilmSimulationHeader';
import FilmHeader from '@/film/FilmHeader';
import { TAG_HIDDEN } from '@/tag';
import HiddenHeader from '@/tag/HiddenHeader';
import FocalLengthHeader from '@/focal/FocalLengthHeader';
@ -22,7 +22,7 @@ export default function PhotoDetailPage({
tag,
camera,
lens,
simulation,
film,
recipe,
focal,
indexNumber,
@ -77,9 +77,9 @@ export default function PhotoDetailPage({
count={count}
dateRange={dateRange}
/>;
} else if (simulation) {
customHeader = <FilmSimulationHeader
simulation={simulation}
} else if (film) {
customHeader = <FilmHeader
film={film}
photos={photos}
selectedPhoto={photo}
indexNumber={indexNumber}
@ -129,13 +129,13 @@ export default function PhotoDetailPage({
showTitleAsH1
showCamera={!camera}
showLens={!lens}
showSimulation={!simulation}
showFilm={!film}
showRecipe={!recipe}
shouldShare={shouldShare}
shouldShareCamera={camera !== undefined}
shouldShareLens={lens !== undefined}
shouldShareTag={tag !== undefined}
shouldShareSimulation={simulation !== undefined}
shouldShareFilm={film !== undefined}
shouldShareRecipe={recipe !== undefined}
shouldShareFocalLength={focal !== undefined}
includeFavoriteInAdminMenu={includeFavoriteInAdminMenu}
@ -149,7 +149,7 @@ export default function PhotoDetailPage({
selectedPhoto={photo}
tag={tag}
camera={camera}
simulation={simulation}
film={film}
focal={focal}
animateOnFirstLoadOnly
/>}

View File

@ -5,7 +5,7 @@ import HeaderList from '@/components/HeaderList';
import PhotoTag from '@/tag/PhotoTag';
import { PhotoDateRange, dateRangeForPhotos, photoQuantityText } from '.';
import { TAG_FAVS, TAG_HIDDEN, addHiddenToTags } from '@/tag';
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
import PhotoFilm from '@/film/PhotoFilm';
import FavsTag from '../tag/FavsTag';
import { useAppState } from '@/state/AppState';
import { useMemo, useRef } from 'react';
@ -20,7 +20,7 @@ import PhotoRecipe from '@/recipe/PhotoRecipe';
import IconCamera from '@/components/icons/IconCamera';
import IconRecipe from '@/components/icons/IconRecipe';
import IconTag from '@/components/icons/IconTag';
import IconFilmSimulation from '@/components/icons/IconFilmSimulation';
import IconFilm from '@/components/icons/IconFilm';
import IconLens from '@/components/icons/IconLens';
import PhotoLens from '@/lens/PhotoLens';
import IconFocalLength from '@/components/icons/IconFocalLength';
@ -48,7 +48,7 @@ export default function PhotoGridSidebar({
cameras,
lenses,
tags,
simulations,
films,
recipes,
focalLengths,
} = categories;
@ -192,17 +192,17 @@ export default function PhotoGridSidebar({
/>
: null;
const filmsContent = simulations.length > 0
const filmsContent = films.length > 0
? <HeaderList
key="films"
title="Films"
icon={<IconFilmSimulation size={15} />}
icon={<IconFilm size={15} />}
maxItems={maxItemsPerCategory}
items={simulations
.map(({ simulation, count }) =>
<PhotoFilmSimulation
key={simulation}
simulation={simulation}
items={films
.map(({ film, count }) =>
<PhotoFilm
key={film}
film={film}
countOnHover={count}
type="text-only"
prefetch={false}

View File

@ -6,7 +6,7 @@ import {
doesPhotoNeedBlurCompatibility,
shouldShowCameraDataForPhoto,
shouldShowExifDataForPhoto,
shouldShowFilmSimulationDataForPhoto,
shouldShowFilmDataForPhoto,
shouldShowLensDataForPhoto,
shouldShowRecipeDataForPhoto,
titleForPhoto,
@ -21,7 +21,7 @@ import ShareButton from '@/share/ShareButton';
import DownloadButton from '@/components/DownloadButton';
import PhotoCamera from '../camera/PhotoCamera';
import { cameraFromPhoto } from '@/camera';
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
import PhotoFilm from '@/film/PhotoFilm';
import { sortTagsArray } from '@/tag';
import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid';
import PhotoLink from './PhotoLink';
@ -65,7 +65,7 @@ export default function PhotoLarge({
showTitleAsH1,
showCamera = true,
showLens = true,
showSimulation = true,
showFilm = true,
showRecipe = true,
showZoomControls: showZoomControlsProp = true,
shouldZoomOnFKeydown = true,
@ -73,7 +73,7 @@ export default function PhotoLarge({
shouldShareCamera,
shouldShareLens,
shouldShareTag,
shouldShareSimulation,
shouldShareFilm,
shouldShareRecipe,
shouldShareFocalLength,
includeFavoriteInAdminMenu,
@ -90,7 +90,7 @@ export default function PhotoLarge({
showTitleAsH1?: boolean
showCamera?: boolean
showLens?: boolean
showSimulation?: boolean
showFilm?: boolean
showRecipe?: boolean
showZoomControls?: boolean
shouldZoomOnFKeydown?: boolean
@ -98,7 +98,7 @@ export default function PhotoLarge({
shouldShareCamera?: boolean
shouldShareLens?: boolean
shouldShareTag?: boolean
shouldShareSimulation?: boolean
shouldShareFilm?: boolean
shouldShareRecipe?: boolean
shouldShareFocalLength?: boolean
includeFavoriteInAdminMenu?: boolean
@ -120,7 +120,7 @@ export default function PhotoLarge({
lensCount,
tagCounts,
recipeCount,
simulationCount,
filmCount,
} = useCategoryCountsForPhoto(photo);
const showZoomControls = showZoomControlsProp && areZoomControlsShown;
@ -150,8 +150,7 @@ export default function PhotoLarge({
const showTagsContent = tags.length > 0;
const showRecipeContent = showRecipe && shouldShowRecipeDataForPhoto(photo);
const showRecipeButton = shouldShowRecipeDataForPhoto(photo);
const showSimulationContent = showSimulation &&
shouldShowFilmSimulationDataForPhoto(photo);
const showFilmContent = showFilm && shouldShowFilmDataForPhoto(photo);
useVisible({ ref, onVisible });
@ -168,7 +167,7 @@ export default function PhotoLarge({
showLensContent ||
showTagsContent ||
showRecipeContent ||
showSimulationContent ||
showFilmContent ||
showExifContent;
const hasNonDateContent =
@ -226,12 +225,12 @@ export default function PhotoLarge({
<AnimatePresence>
{(shouldShowRecipeOverlay || shouldDebugRecipeOverlays) &&
photo.recipeData &&
photo.filmSimulation &&
photo.film &&
<PhotoRecipeOverlay
ref={refRecipe}
title={photo.recipeTitle}
recipe={photo.recipeData}
simulation={photo.filmSimulation}
film={photo.film}
iso={photo.isoFormatted}
exposure={photo.exposureCompensationFormatted}
onClose={hideRecipeOverlay}
@ -391,13 +390,13 @@ export default function PhotoLarge({
<li>{photo.isoFormatted}</li>
<li>{photo.exposureCompensationFormatted ?? '0ev'}</li>
</ul>
{(showRecipeButton || showSimulationContent) &&
{(showRecipeButton || showFilmContent) &&
<div className="flex items-center gap-2 *:w-auto">
{showSimulationContent && photo.filmSimulation &&
<PhotoFilmSimulation
simulation={photo.filmSimulation}
{showFilmContent && photo.film &&
<PhotoFilm
film={photo.film}
prefetch={prefetchRelatedLinks}
countOnHover={simulationCount}
countOnHover={filmCount}
/>}
{showRecipeButton &&
<Tooltip content="Fujifilm Recipe">
@ -415,7 +414,7 @@ export default function PhotoLarge({
'px-[4px] py-[2.5px] my-[-3px]',
'translate-y-[2px]',
'hover:bg-dim active:bg-main',
!showSimulation && 'translate-x-[-2px]',
!showFilm && 'translate-x-[-2px]',
)}>
{shouldShowRecipeOverlay
? <IoCloseSharp size={15} />
@ -464,8 +463,8 @@ export default function PhotoLarge({
tag={shouldShareTag ? primaryTag : undefined}
camera={shouldShareCamera ? camera : undefined}
lens={shouldShareLens ? lens : undefined}
simulation={shouldShareSimulation
? photo.filmSimulation
film={shouldShareFilm
? photo.film
: undefined}
recipe={shouldShareRecipe
? recipeTitle

View File

@ -60,7 +60,7 @@ import { convertUploadToPhoto } from './storage';
import { UrlAddStatus } from '@/admin/AdminUploadsClient';
import { convertStringToArray } from '@/utility/string';
import { after } from 'next/server';
import { FilmSimulation } from '@/simulation';
import { FilmSimulation } from '@/film';
// Private actions
@ -315,13 +315,13 @@ export const renamePhotoTagGloballyAction = async (formData: FormData) =>
export const getPhotosNeedingRecipeTitleCountAction = async (
recipeData: string,
simulation: FilmSimulation,
film: FilmSimulation,
photoIdToExclude?: string,
) =>
runAuthenticatedAdminServerAction(async () =>
await getPhotosNeedingRecipeTitleCount(
recipeData,
simulation,
film,
photoIdToExclude,
),
);

View File

@ -10,7 +10,7 @@ import {
getUniqueCameras,
getUniqueTags,
getUniqueTagsHidden,
getUniqueFilmSimulations,
getUniqueFilms,
getPhotosNearId,
getPhotosMostRecentUpdate,
getPhotosMeta,
@ -29,7 +29,7 @@ import {
PATH_GRID,
PATH_ROOT,
PREFIX_CAMERA,
PREFIX_FILM_SIMULATION,
PREFIX_FILM,
PREFIX_FOCAL_LENGTH,
PREFIX_LENS,
PREFIX_RECIPE,
@ -45,7 +45,7 @@ const KEY_PHOTO = 'photo';
const KEY_CAMERAS = 'cameras';
const KEY_LENSES = 'lenses';
const KEY_TAGS = 'tags';
const KEY_FILM_SIMULATIONS = 'film-simulations';
const KEY_FILMS = 'films';
const KEY_RECIPES = 'recipes';
const KEY_FOCAL_LENGTHS = 'focal-lengths';
// Type keys
@ -109,8 +109,8 @@ export const revalidateCamerasKey = () =>
export const revalidateLensesKey = () =>
revalidateTag(KEY_LENSES);
export const revalidateFilmSimulationsKey = () =>
revalidateTag(KEY_FILM_SIMULATIONS);
export const revalidateFilmsKey = () =>
revalidateTag(KEY_FILMS);
export const revalidateFocalLengthsKey = () =>
revalidateTag(KEY_FOCAL_LENGTHS);
@ -120,7 +120,7 @@ export const revalidateAllKeys = () => {
revalidateTagsKey();
revalidateCamerasKey();
revalidateLensesKey();
revalidateFilmSimulationsKey();
revalidateFilmsKey();
revalidateRecipesKey();
revalidateFocalLengthsKey();
};
@ -140,7 +140,7 @@ export const revalidatePhoto = (photoId: string) => {
revalidateTagsKey();
revalidateCamerasKey();
revalidateLensesKey();
revalidateFilmSimulationsKey();
revalidateFilmsKey();
revalidateRecipesKey();
revalidateFocalLengthsKey();
// Paths
@ -151,7 +151,7 @@ export const revalidatePhoto = (photoId: string) => {
revalidatePath(PREFIX_TAG, 'layout');
revalidatePath(PREFIX_CAMERA, 'layout');
revalidatePath(PREFIX_LENS, 'layout');
revalidatePath(PREFIX_FILM_SIMULATION, 'layout');
revalidatePath(PREFIX_FILM, 'layout');
revalidatePath(PREFIX_RECIPE, 'layout');
revalidatePath(PREFIX_FOCAL_LENGTH, 'layout');
revalidatePath(PATH_ADMIN, 'layout');
@ -231,10 +231,10 @@ export const getUniqueLensesCached =
[KEY_PHOTOS, KEY_LENSES],
);
export const getUniqueFilmSimulationsCached =
export const getUniqueFilmsCached =
unstable_cache(
getUniqueFilmSimulations,
[KEY_PHOTOS, KEY_FILM_SIMULATIONS],
getUniqueFilms,
[KEY_PHOTOS, KEY_FILMS],
);
export const getUniqueRecipesCached =

View File

@ -47,7 +47,7 @@ export const getWheresFromOptions = (
tag,
camera,
lens,
simulation,
film,
recipe,
focal,
} = options;
@ -108,9 +108,9 @@ export const getWheresFromOptions = (
wheres.push(`$${valuesIndex++}=ANY(tags)`);
wheresValues.push(tag);
}
if (simulation) {
wheres.push(`film_simulation=$${valuesIndex++}`);
wheresValues.push(simulation);
if (film) {
wheres.push(`film=$${valuesIndex++}`);
wheresValues.push(film);
}
if (recipe) {
wheres.push(`recipe_title=$${valuesIndex++}`);

View File

@ -50,6 +50,27 @@ export const MIGRATIONS: Migration[] = [{
ALTER TABLE photos
ADD COLUMN IF NOT EXISTS recipe_title VARCHAR(255)
`,
}, {
label: '05: Universal Film',
fields: ['film'],
run: () => sql`
DO $$
BEGIN
IF EXISTS(
SELECT 1
FROM information_schema.columns
WHERE table_name='photos'
AND column_name='film_simulation'
)
THEN
ALTER TABLE photos
RENAME COLUMN film_simulation TO film;
ELSE
ALTER TABLE photos
ADD COLUMN IF NOT EXISTS film VARCHAR(255);
END IF;
END $$;
`,
}];
export const migrationForError = (e: any) =>

View File

@ -13,7 +13,7 @@ import {
} from '@/photo';
import { Cameras, createCameraKey } from '@/camera';
import { Tags } from '@/tag';
import { FilmSimulation, FilmSimulations } from '@/simulation';
import { FilmSimulation, Films } from '@/film';
import { ADMIN_SQL_DEBUG_ENABLED } from '@/app/config';
import {
GetPhotosOptions,
@ -53,7 +53,7 @@ const createPhotosTable = () =>
location_name VARCHAR(255),
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
film_simulation VARCHAR(255),
film VARCHAR(255),
recipe_title VARCHAR(255),
recipe_data JSONB,
priority_order REAL,
@ -160,7 +160,7 @@ export const insertPhoto = (photo: PhotoDbInsert) =>
location_name,
latitude,
longitude,
film_simulation,
film,
recipe_title,
recipe_data,
priority_order,
@ -191,7 +191,7 @@ export const insertPhoto = (photo: PhotoDbInsert) =>
${photo.locationName},
${photo.latitude},
${photo.longitude},
${photo.filmSimulation},
${photo.film},
${photo.recipeTitle},
${photo.recipeData},
${photo.priorityOrder},
@ -225,7 +225,7 @@ export const updatePhoto = (photo: PhotoDbInsert) =>
location_name=${photo.locationName},
latitude=${photo.latitude},
longitude=${photo.longitude},
film_simulation=${photo.filmSimulation},
film=${photo.film},
recipe_title=${photo.recipeTitle},
recipe_data=${photo.recipeData},
priority_order=${photo.priorityOrder || null},
@ -367,14 +367,14 @@ export const getUniqueRecipes = async () =>
export const getRecipeTitleForData = async (
data: string | object,
simulation: FilmSimulation,
film: FilmSimulation,
) =>
// Includes legacy check on pre-stringified JSON
safelyQueryPhotos(() => sql`
SELECT recipe_title FROM photos
WHERE hidden IS NOT TRUE
AND recipe_data=${typeof data === 'string' ? data : JSON.stringify(data)}
AND film_simulation=${simulation}
AND film=${film}
LIMIT 1
`
.then(({ rows }) => rows[0]?.recipe_title as string | undefined)
@ -382,7 +382,7 @@ export const getRecipeTitleForData = async (
export const getPhotosNeedingRecipeTitleCount = async (
data: string,
simulation: FilmSimulation,
film: FilmSimulation,
photoIdToExclude?: string,
) =>
safelyQueryPhotos(() => sql`
@ -390,7 +390,7 @@ export const getPhotosNeedingRecipeTitleCount = async (
FROM photos
WHERE recipe_title IS NULL
AND recipe_data=${data}
AND film_simulation=${simulation}
AND film=${film}
AND id <> ${photoIdToExclude}
`.then(({ rows }) => parseInt(rows[0].count, 10))
, 'getPhotosNeedingRecipeTitleCount');
@ -398,29 +398,29 @@ export const getPhotosNeedingRecipeTitleCount = async (
export const updateAllMatchingRecipeTitles = (
title: string,
data: string,
simulation: FilmSimulation,
film: FilmSimulation,
) =>
safelyQueryPhotos(() => sql`
UPDATE photos
SET recipe_title=${title}
WHERE recipe_title IS NULL
AND recipe_data=${data}
AND film_simulation=${simulation}
AND film=${film}
`, 'updateAllMatchingRecipeTitles');
export const getUniqueFilmSimulations = async () =>
export const getUniqueFilms = async () =>
safelyQueryPhotos(() => sql`
SELECT DISTINCT film_simulation, COUNT(*)
SELECT DISTINCT film, COUNT(*)
FROM photos
WHERE hidden IS NOT TRUE AND film_simulation IS NOT NULL
GROUP BY film_simulation
ORDER BY film_simulation ASC
`.then(({ rows }): FilmSimulations => rows
.map(({ film_simulation, count }) => ({
simulation: film_simulation as FilmSimulation,
WHERE hidden IS NOT TRUE AND film IS NOT NULL
GROUP BY film
ORDER BY film ASC
`.then(({ rows }): Films => rows
.map(({ film, count }) => ({
film: film as FilmSimulation,
count: parseInt(count, 10),
})))
, 'getUniqueFilmSimulations');
, 'getUniqueFilms');
export const getUniqueFocalLengths = async () =>
safelyQueryPhotos(() => sql`

View File

@ -1,14 +1,14 @@
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
import { ComponentProps, useEffect, useState } from 'react';
import { getPhotosNeedingRecipeTitleCountAction } from '../actions';
import { FilmSimulation } from '@/simulation';
import { FilmSimulation } from '@/film';
export default function ApplyRecipeTitleGloballyCheckbox({
photoId,
recipeTitle,
hasRecipeTitleChanged,
recipeData,
simulation,
film,
onMatchResults,
...props
}: ComponentProps<typeof FieldSetWithStatus> & {
@ -16,7 +16,7 @@ export default function ApplyRecipeTitleGloballyCheckbox({
recipeTitle?: string
hasRecipeTitleChanged?: boolean
recipeData?: string
simulation?: FilmSimulation
film?: FilmSimulation
onMatchResults: (didFindMatchingPhotos: boolean) => void
}) {
const [matchingPhotosCount, setMatchingPhotosCount] = useState<number>();
@ -24,14 +24,14 @@ export default function ApplyRecipeTitleGloballyCheckbox({
const loading = matchingPhotosCount === undefined;
useEffect(() => {
if (recipeTitle && hasRecipeTitleChanged && recipeData && simulation) {
if (recipeTitle && hasRecipeTitleChanged && recipeData && film) {
setMatchingPhotosCount(undefined);
getPhotosNeedingRecipeTitleCountAction(recipeData, simulation, photoId)
getPhotosNeedingRecipeTitleCountAction(recipeData, film, photoId)
.then(setMatchingPhotosCount);
} else {
setMatchingPhotosCount(0);
}
}, [recipeTitle, hasRecipeTitleChanged, recipeData, simulation, photoId]);
}, [recipeTitle, hasRecipeTitleChanged, recipeData, film, photoId]);
useEffect(() => {
onMatchResults((matchingPhotosCount ?? 0) > 0);

View File

@ -41,7 +41,7 @@ import ErrorNote from '@/components/ErrorNote';
import { convertRecipesForForm, Recipes } from '@/recipe';
import deepEqual from 'fast-deep-equal/es6/react';
import ApplyRecipeTitleGloballyCheckbox from './ApplyRecipesGloballyCheckbox';
import { FilmSimulation } from '@/simulation';
import { FilmSimulation } from '@/film';
import IconFavs from '@/components/icons/IconFavs';
import IconHidden from '@/components/icons/IconHidden';
@ -414,7 +414,7 @@ export default function PhotoForm({
hasRecipeTitleChanged={
changedFormKeys.includes('recipeTitle')}
recipeData={formData.recipeData}
simulation={formData.filmSimulation as FilmSimulation}
film={formData.film as FilmSimulation}
onMatchResults={onMatchResults}
{...fieldProps}
/>;

View File

@ -19,7 +19,7 @@ import { generateNanoid } from '@/utility/nanoid';
import {
FILM_SIMULATION_FORM_INPUT_OPTIONS,
} from '@/platforms/fujifilm/simulation';
import { FilmSimulation } from '@/simulation';
import { FilmSimulation } from '@/film';
import { GEO_PRIVACY_ENABLED } from '@/app/config';
import { TAG_FAVS, getValidationMessageForTags } from '@/tag';
import { MAKE_FUJIFILM } from '@/platforms/fujifilm';
@ -119,8 +119,8 @@ const FORM_METADATA = (
aspectRatio: { label: 'aspect ratio', readOnly: true },
make: { label: 'camera make' },
model: { label: 'camera model' },
filmSimulation: {
label: 'fujifilm simulation',
film: {
label: 'film',
selectOptions: FILM_SIMULATION_FORM_INPUT_OPTIONS,
selectOptionsDefaultLabel: 'Unknown',
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
@ -274,7 +274,7 @@ export const convertPhotoToFormData = (photo: Photo): PhotoFormData => {
export const convertExifToFormData = (
data: ExifData,
filmSimulation?: FilmSimulation,
film?: FilmSimulation,
recipeData?: FujifilmRecipe,
): Omit<
Record<keyof PhotoExif, string | undefined>,
@ -298,7 +298,7 @@ export const convertExifToFormData = (
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLatitude?.toString() : undefined,
longitude:
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined,
filmSimulation,
film,
recipeData: JSON.stringify(recipeData),
...data.tags?.DateTimeOriginal && {
takenAt: convertTimestampWithOffsetToPostgresString(
@ -345,7 +345,7 @@ export const convertFormDataToPhotoDbInsert = (
return {
...(photoForm as PhotoFormData & {
filmSimulation?: FilmSimulation
film?: FilmSimulation
recipeData?: FujifilmRecipe
}),
...!photoForm.id && { id: generateNanoid() },

View File

@ -1,11 +1,11 @@
import { formatFocalLength } from '@/focal';
import { getNextImageUrlForRequest } from '@/platforms/next-image';
import { FilmSimulation, photoHasFilmSimulationData } from '@/simulation';
import { FilmSimulation, photoHasFilmData } from '@/film';
import {
HIGH_DENSITY_GRID,
IS_PREVIEW,
SHOW_EXIF_DATA,
SHOW_FILM_SIMULATIONS,
SHOW_FILMS,
SHOW_LENSES,
SHOW_RECIPES,
} from '@/app/config';
@ -65,7 +65,7 @@ export interface PhotoExif {
exposureCompensation?: number
latitude?: number
longitude?: number
filmSimulation?: FilmSimulation
film?: FilmSimulation
recipeData?: string
takenAt?: string
takenAtNaive?: string
@ -329,10 +329,10 @@ export const shouldShowRecipeDataForPhoto = (photo: Photo) =>
SHOW_RECIPES &&
photoHasRecipeData(photo);
export const shouldShowFilmSimulationDataForPhoto = (photo: Photo) =>
export const shouldShowFilmDataForPhoto = (photo: Photo) =>
SHOW_EXIF_DATA &&
SHOW_FILM_SIMULATIONS &&
photoHasFilmSimulationData(photo);
SHOW_FILMS &&
photoHasFilmData(photo);
export const shouldShowExifDataForPhoto = (photo: Photo) =>
SHOW_EXIF_DATA && photoHasExifData(photo);

View File

@ -11,7 +11,7 @@ import {
} from '@/platforms/fujifilm/simulation';
import { ExifData, ExifParserFactory } from 'ts-exif-parser';
import { PhotoFormData } from './form';
import { FilmSimulation } from '@/simulation';
import { FilmSimulation } from '@/film';
import sharp, { Sharp } from 'sharp';
import {
GEO_PRIVACY_ENABLED,
@ -58,7 +58,7 @@ export const extractImageDataFromBlobPath = async (
const extension = getExtensionFromStorageUrl(url);
let exifData: ExifData | undefined;
let filmSimulation: FilmSimulation | undefined;
let film: FilmSimulation | undefined;
let recipe: FujifilmRecipe | undefined;
let blurData: string | undefined;
let imageResizedBase64: string | undefined;
@ -89,7 +89,7 @@ export const extractImageDataFromBlobPath = async (
const exifDataBinary = parser.parse();
const makerNote = exifDataBinary.tags?.MakerNote;
if (Buffer.isBuffer(makerNote)) {
filmSimulation = getFujifilmSimulationFromMakerNote(makerNote);
film = getFujifilmSimulationFromMakerNote(makerNote);
recipe = getFujifilmRecipeFromMakerNote(makerNote);
}
}
@ -124,7 +124,7 @@ export const extractImageDataFromBlobPath = async (
url,
},
...generateBlurData && { blurData },
...convertExifToFormData(exifData, filmSimulation, recipe),
...convertExifToFormData(exifData, film, recipe),
},
},
imageResizedBase64,
@ -206,10 +206,10 @@ export const convertFormDataToPhotoDbInsertAndLookupRecipeTitle =
Promise<ReturnType<typeof convertFormDataToPhotoDbInsert>> => {
const photo = convertFormDataToPhotoDbInsert(...args);
if (photo.recipeData && !photo.recipeTitle && photo.filmSimulation) {
if (photo.recipeData && !photo.recipeTitle && photo.film) {
const recipeTitle = await getRecipeTitleForData(
photo.recipeData,
photo.filmSimulation,
photo.film,
);
if (recipeTitle) {
photo.recipeTitle = recipeTitle;
@ -229,12 +229,12 @@ export const propagateRecipeTitleIfNecessary = async (
formData.get('recipeTitle') &&
photo.recipeTitle &&
photo.recipeData &&
photo.filmSimulation
photo.film
) {
await updateAllMatchingRecipeTitles(
photo.recipeTitle,
photo.recipeData,
photo.filmSimulation,
photo.film,
);
}
};

View File

@ -222,11 +222,11 @@ const ALL_POSSIBLE_FILM_SIMULATION_LABELS = Object
large.toLocaleLowerCase(),
]);
export const isStringFilmSimulation = (simulation: string) =>
ALL_POSSIBLE_FILM_SIMULATION_LABELS.includes(simulation.toLocaleLowerCase());
export const isStringFilmSimulation = (film: string) =>
ALL_POSSIBLE_FILM_SIMULATION_LABELS.includes(film.toLocaleLowerCase());
export const labelForFilmSimulation = (simulation: FujifilmSimulation) =>
FILM_SIMULATION_LABELS[simulation];
export const labelForFilm = (film: FujifilmSimulation) =>
FILM_SIMULATION_LABELS[film];
export const getFujifilmSimulationFromMakerNote = (
bytes: Buffer,

View File

@ -1,143 +0,0 @@
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
import { FilmSimulation } from '@/simulation';
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
import clsx from 'clsx/lite';
import { ReactNode, RefObject } from 'react';
import { addSign, formatWhiteBalance } from '.';
export default function PhotoRecipeOGTile({
recipe,
simulation,
iso,
exposure,
}: {
ref?: RefObject<HTMLDivElement | null>
recipe: FujifilmRecipe
simulation: FilmSimulation
iso?: string
exposure?: string
}) {
const {
dynamicRange,
whiteBalance,
highISONoiseReduction,
noiseReductionBasic,
highlight,
shadow,
color,
sharpness,
clarity,
colorChromeEffect,
colorChromeFXBlue,
grainEffect,
bwAdjustment,
bwMagentaGreen,
} = recipe;
const whiteBalanceTypeFormatted = formatWhiteBalance(recipe);
const renderRow = (children: ReactNode, className?: string) =>
<div className={clsx(
'flex gap-2 *:w-full *:grow',
className,
)}>
{children}
</div>;
const renderDataSquare = (
value: ReactNode,
label?: string,
className?: string,
) => (
<div className={clsx(
'flex flex-col items-center justify-center gap-0.5 rounded-md min-w-0',
'rounded-md border',
'border-transparent',
'bg-neutral-100/60',
label && 'p-1',
className,
)}>
<div className="flex truncate max-w-full tracking-wide text-lg">
{typeof value === 'number' ? addSign(value) : value}
</div>
{label && <div className={clsx(
'text-[11px] leading-none tracking-wide font-medium text-black/50',
'uppercase',
)}>
{label}
</div>}
</div>
);
return (
<div
className={clsx(
'flex z-10',
'w-[37rem] p-10 aspect-video',
'text-[13.5px] text-black',
'bg-white/50',
'backdrop-blur-xl saturate-[300%]',
)}
>
<div className="flex flex-col gap-2 w-full">
{renderRow(<>
<div className={clsx(
'flex',
'text-lg leading-none text-black truncate',
)}>
KODAK PORTRA 500
</div>
<PhotoFilmSimulation
contrast="frosted"
simulation={simulation}
className="w-auto! grow-0!"
/>
</>, 'flex items-center gap-4')}
{renderRow(<>
{renderDataSquare(`DR${dynamicRange.development}`)}
{renderDataSquare(iso)}
{renderDataSquare(exposure ?? '0ev')}
</>)}
{renderRow(<>
{renderDataSquare(
whiteBalanceTypeFormatted.toUpperCase(),
`R${addSign(whiteBalance?.red)} / B${addSign(whiteBalance?.blue)}`,
)}
{renderDataSquare(
highISONoiseReduction ?? noiseReductionBasic ?? 'OFF',
'ISO NR',
)}
{renderDataSquare(highlight, 'Highlight')}
{renderDataSquare(shadow, 'Shadow')}
</>)}
{renderRow(<>
{renderDataSquare(color, 'Color')}
{renderDataSquare(sharpness, 'Sharpness')}
{renderDataSquare(clarity, 'Clarity')}
</>)}
{renderRow(<>
{renderDataSquare(
colorChromeEffect?.toLocaleUpperCase() ?? 'N/A',
'Color Chrome',
)}
{renderDataSquare(
colorChromeFXBlue?.toLocaleUpperCase() ?? 'N/A',
'FX Blue',
)}
</>)}
{renderRow(<>
{renderDataSquare(
grainEffect.roughness.toLocaleUpperCase(),
grainEffect.size === 'large'
? 'Large Grain'
: grainEffect.size === 'small'
? 'Small Grain'
: 'Grain',
)}
{renderDataSquare(bwAdjustment ?? 0, 'BW ADJ')}
{renderDataSquare(bwMagentaGreen ?? 0, 'BW M/G')}
</>)}
</div>
</div>
);
}

View File

@ -1,7 +1,7 @@
'use client';
import LoaderButton from '@/components/primitives/LoaderButton';
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
import PhotoFilm from '@/film/PhotoFilm';
import clsx from 'clsx/lite';
import { ReactNode, RefObject } from 'react';
import { IoCloseCircle } from 'react-icons/io5';
@ -15,7 +15,7 @@ import {
generateRecipeText,
RecipeProps,
} from '.';
import { labelForFilmSimulation } from '@/platforms/fujifilm/simulation';
import { labelForFilm } from '@/platforms/fujifilm/simulation';
import { TbChecklist } from 'react-icons/tb';
import CopyButton from '@/components/CopyButton';
import { pathForRecipe } from '@/app/paths';
@ -25,7 +25,7 @@ export default function PhotoRecipeOverlay({
ref,
title,
recipe,
simulation,
film,
onClose,
}: RecipeProps & {
ref?: RefObject<HTMLDivElement | null>
@ -122,7 +122,7 @@ export default function PhotoRecipeOverlay({
label={`${title
? `${formatRecipe(title).toLocaleUpperCase()} recipe`
: 'Recipe'}`}
text={generateRecipeText({ title, recipe, simulation }).join('\n')}
text={generateRecipeText({ title, recipe, film }).join('\n')}
iconSize={17}
className={clsx(
'translate-y-[0.5px]',
@ -147,10 +147,10 @@ export default function PhotoRecipeOverlay({
<div className="col-span-8">
{renderDataSquare(
<div className="flex items-center gap-1.5">
{labelForFilmSimulation(simulation).medium.toLocaleUpperCase()}
<PhotoFilmSimulation
{labelForFilm(film).medium.toLocaleUpperCase()}
<PhotoFilm
contrast="frosted"
simulation={simulation}
film={film}
type="icon-only"
className="opacity-80 translate-y-[-0.5px]"
/>

View File

@ -32,11 +32,11 @@ export default function RecipeHeader({
contrast="high"
recipeOnClick={() => (
photo?.recipeData &&
photo?.filmSimulation
photo?.film
) ? setRecipeModalProps?.({
title: photo.recipeTitle,
recipe: photo.recipeData,
simulation: photo.filmSimulation,
film: photo.film,
iso: photo.isoFormatted,
exposure: photo.exposureTimeFormatted,
})

View File

@ -7,8 +7,8 @@ import {
formatCountDescriptive,
} from '@/utility/string';
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
import { FilmSimulation } from '@/simulation';
import { labelForFilmSimulation } from '@/platforms/fujifilm/simulation';
import { FilmSimulation } from '@/film';
import { labelForFilm } from '@/platforms/fujifilm/simulation';
export type RecipeWithCount = {
recipe: string
@ -20,7 +20,7 @@ export type Recipes = RecipeWithCount[]
export interface RecipeProps {
title?: string
recipe: FujifilmRecipe
simulation: FilmSimulation
film: FilmSimulation
iso?: string
exposure?: string
}
@ -57,12 +57,12 @@ export const descriptionForRecipePhotos = (
export const generateRecipeText = ({
title,
recipe,
simulation,
film,
}: RecipeProps,
abbreviate?: boolean,
) => {
const lines = [
`${labelForFilmSimulation(simulation).small.toLocaleUpperCase()}`,
`${labelForFilm(film).small.toLocaleUpperCase()}`,
// eslint-disable-next-line max-len
`${formatWhiteBalance(recipe).toLocaleUpperCase()} ${formatWhiteBalanceColor(recipe)}`,
];
@ -133,7 +133,7 @@ export const generateMetaForRecipe = (
});
const photoHasRecipe = (photo?: Photo) =>
photo?.filmSimulation && photo?.recipeData;
photo?.film && photo?.recipeData;
export const getPhotoWithRecipeFromPhotos = (
photos: Photo[],

View File

@ -3,7 +3,7 @@
import PhotoShareModal from '@/photo/PhotoShareModal';
import TagShareModal from '@/tag/TagShareModal';
import CameraShareModal from '@/camera/CameraShareModal';
import FilmSimulationShareModal from '@/simulation/FilmSimulationShareModal';
import FilmShareModal from '@/film/FilmShareModal';
import FocalLengthShareModal from '@/focal/FocalLengthShareModal';
import { useAppState } from '@/state/AppState';
import RecipeShareModal from '@/recipe/RecipeShareModal';
@ -20,7 +20,7 @@ export default function ShareModals() {
camera,
lens,
tag,
simulation,
film,
recipe,
focal,
} = shareModalProps;
@ -30,7 +30,7 @@ export default function ShareModals() {
photo,
tag,
camera,
simulation,
film,
recipe,
focal,
}} />;
@ -42,8 +42,8 @@ export default function ShareModals() {
return <CameraShareModal {...{ camera, ...attributes }} />;
} else if (lens) {
return <LensShareModal {...{ lens, ...attributes }} />;
} else if (simulation) {
return <FilmSimulationShareModal {...{ simulation, ...attributes }} />;
} else if (film) {
return <FilmShareModal {...{ film, ...attributes }} />;
} else if (recipe) {
return <RecipeShareModal {...{ recipe, ...attributes }} />;
} else if (focal !== undefined) {

View File

@ -2,7 +2,7 @@ import { Photo } from '@/photo';
import { PhotoSetAttributes, PhotoSetCategory } from '@/category';
import {
absolutePathForCameraImage,
absolutePathForFilmSimulationImage,
absolutePathForFilmImage,
absolutePathForFocalLengthImage,
absolutePathForLensImage,
absolutePathForPhotoImage,
@ -21,7 +21,7 @@ export const getSharePathFromShareModalProps = ({
lens,
tag,
recipe,
simulation,
film,
focal,
}: ShareModalProps) => {
if (photo) {
@ -34,8 +34,8 @@ export const getSharePathFromShareModalProps = ({
return absolutePathForTagImage(tag);
} else if (recipe) {
return absolutePathForRecipeImage(recipe);
} else if (simulation) {
return absolutePathForFilmSimulationImage(simulation);
} else if (film) {
return absolutePathForFilmImage(film);
} else if (focal) {
return absolutePathForFocalLengthImage(focal);
}

View File

@ -1,23 +0,0 @@
import { absolutePathForFilmSimulation } from '@/app/paths';
import { PhotoSetAttributes } from '../category';
import ShareModal from '@/share/ShareModal';
import FilmSimulationOGTile from './FilmSimulationOGTile';
import { FilmSimulation, shareTextForFilmSimulation } from '.';
export default function FilmSimulationShareModal({
simulation,
photos,
count,
dateRange,
}: {
simulation: FilmSimulation
} & PhotoSetAttributes) {
return (
<ShareModal
pathShare={absolutePathForFilmSimulation(simulation)}
socialText={shareTextForFilmSimulation(simulation)}
>
<FilmSimulationOGTile {...{ simulation, photos, count, dateRange }} />
</ShareModal>
);
};

View File

@ -1,17 +0,0 @@
import {
getPhotosCached,
getPhotosMetaCached,
} from '@/photo/cache';
import { FilmSimulation } from '.';
export const getPhotosFilmSimulationDataCached = ({
simulation,
limit,
}: {
simulation: FilmSimulation,
limit?: number,
}) =>
Promise.all([
getPhotosCached({ simulation, limit }),
getPhotosMetaCached({ simulation }),
]);

View File

@ -1,84 +0,0 @@
import {
Photo,
PhotoDateRange,
descriptionForPhotoSet,
photoQuantityText,
} from '@/photo';
import {
absolutePathForFilmSimulation,
absolutePathForFilmSimulationImage,
} from '@/app/paths';
import {
FujifilmSimulation,
labelForFilmSimulation,
} from '@/platforms/fujifilm/simulation';
export type FilmSimulation = FujifilmSimulation;
export type FilmSimulationWithCount = {
simulation: FilmSimulation
count: number
}
export type FilmSimulations = FilmSimulationWithCount[]
export const sortFilmSimulations = (
simulations: FilmSimulations,
) => simulations.sort(sortFilmSimulationsWithCount);
export const sortFilmSimulationsWithCount = (
a: FilmSimulationWithCount,
b: FilmSimulationWithCount,
) => {
const aLabel = labelForFilmSimulation(a.simulation).large;
const bLabel = labelForFilmSimulation(b.simulation).large;
return aLabel.localeCompare(bLabel);
};
export const titleForFilmSimulation = (
simulation: FilmSimulation,
photos: Photo[],
explicitCount?: number,
) => [
labelForFilmSimulation(simulation).large,
photoQuantityText(explicitCount ?? photos.length),
].join(' ');
export const shareTextForFilmSimulation = (
simulation: FilmSimulation,
) =>
`Photos shot on Fujifilm ${labelForFilmSimulation(simulation).large}`;
export const descriptionForFilmSimulationPhotos = (
photos: Photo[],
dateBased?: boolean,
explicitCount?: number,
explicitDateRange?: PhotoDateRange,
) =>
descriptionForPhotoSet(
photos,
undefined,
dateBased,
explicitCount,
explicitDateRange,
);
export const generateMetaForFilmSimulation = (
simulation: FilmSimulation,
photos: Photo[],
explicitCount?: number,
explicitDateRange?: PhotoDateRange,
) => ({
url: absolutePathForFilmSimulation(simulation),
title: titleForFilmSimulation(simulation, photos, explicitCount),
description: descriptionForFilmSimulationPhotos(
photos,
true,
explicitCount,
explicitDateRange,
),
images: absolutePathForFilmSimulationImage(simulation),
});
export const photoHasFilmSimulationData = (photo: Photo) =>
Boolean(photo.filmSimulation);