From 7bf2c45145424a2d1cacad547de923d063a0e49a Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 30 Mar 2025 13:37:06 -0500 Subject: [PATCH 01/14] Show film field when editing non-Fuji photos --- src/photo/form/PhotoForm.tsx | 16 ++++++++++++++++ src/photo/form/index.ts | 7 +------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index 8ea391a0..d7896942 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -44,6 +44,10 @@ import ApplyRecipeTitleGloballyCheckbox from './ApplyRecipesGloballyCheckbox'; import { FilmSimulation } from '@/film'; import IconFavs from '@/components/icons/IconFavs'; import IconHidden from '@/components/icons/IconHidden'; +import { MAKE_FUJIFILM } from '@/platforms/fujifilm'; +import { + FILM_SIMULATION_FORM_INPUT_OPTIONS, +} from '@/platforms/fujifilm/simulation'; const THUMBNAIL_SIZE = 300; @@ -406,6 +410,18 @@ export default function PhotoForm({ }; switch (key) { + case 'film': + return formData.make === MAKE_FUJIFILM + ? + : ; case 'applyRecipeTitleGlobally': return make !== MAKE_FUJIFILM, + note: 'Intended for Fujifilm cameras or film scans', shouldNotOverwriteWithNullDataOnSync: true, }, recipeTitle: { From 3a8af98235c884ce51d8f681caeab04f6baa9d9a Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 30 Mar 2025 13:57:34 -0500 Subject: [PATCH 02/14] Adjust film form text --- src/components/FieldSetWithStatus.tsx | 10 ++++++++-- src/components/primitives/ResponsiveText.tsx | 14 ++++++++++---- src/photo/form/PhotoForm.tsx | 2 ++ src/photo/form/index.ts | 4 +++- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/components/FieldSetWithStatus.tsx b/src/components/FieldSetWithStatus.tsx index d399f8a0..5b023b66 100644 --- a/src/components/FieldSetWithStatus.tsx +++ b/src/components/FieldSetWithStatus.tsx @@ -9,12 +9,14 @@ import TagInput from './TagInput'; import { FiChevronDown } from 'react-icons/fi'; import { parameterize } from '@/utility/string'; import Checkbox from './Checkbox'; +import ResponsiveText from './primitives/ResponsiveText'; export default function FieldSetWithStatus({ id: _id, label, icon, note, + noteShort, error, value, isModified, @@ -40,6 +42,7 @@ export default function FieldSetWithStatus({ label: string icon?: ReactNode note?: string + noteShort?: string error?: string value: string isModified?: boolean @@ -128,9 +131,12 @@ export default function FieldSetWithStatus({ {label} {note && !error && - + ({note}) - } + } {isModified && !error && {/* Short text */} - + {shortText ?? children} {/* Full text */} - + {children} diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index d7896942..09448321 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -336,6 +336,7 @@ export default function PhotoForm({ .map(([key, { label, note, + noteShort, required, selectOptions, selectOptionsDefaultLabel, @@ -363,6 +364,7 @@ export default function PhotoForm({ : '' ), note, + noteShort, error: formErrors[key], value: staticValue ?? formData[key] ?? '', isModified: ( diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index 627af976..b7fab9b6 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -48,6 +48,7 @@ export type AnnotatedTag = { export type FormMeta = { label: string note?: string + noteShort?: string required?: boolean excludeFromInsert?: boolean readOnly?: boolean @@ -118,7 +119,8 @@ const FORM_METADATA = ( model: { label: 'camera model' }, film: { label: 'film', - note: 'Intended for Fujifilm cameras or film scans', + note: 'Intended for Fujifilm simulations and analog scans', + noteShort: 'Fujifilm simulations / analog scans', shouldNotOverwriteWithNullDataOnSync: true, }, recipeTitle: { From 09d7a07722f2932ce52b8fcf484f61928c408eb6 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 30 Mar 2025 20:08:49 -0500 Subject: [PATCH 03/14] Catch up to 3 JIT migrations --- src/photo/db/query.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/photo/db/query.ts b/src/photo/db/query.ts index 9af7bf0c..d6f5ad0f 100644 --- a/src/photo/db/query.ts +++ b/src/photo/db/query.ts @@ -65,7 +65,8 @@ const createPhotosTable = () => ) `; -// Wrapper for most queries for JIT table creation/migration running +// Safe wrapper for most queries with JIT table creation/migration +// Catch up to 3 migrations in older installations const safelyQueryPhotos = async ( callback: () => Promise, debugMessage: string, @@ -78,6 +79,7 @@ const safelyQueryPhotos = async ( try { result = await callback(); } catch (e: any) { + // Catch 1st migration let migration = migrationForError(e); if (migration) { console.log(`Running Migration ${migration.label} ...`); @@ -85,15 +87,26 @@ const safelyQueryPhotos = async ( try { result = await callback(); } catch (e: any) { - // Catch potential second migration, - // which otherwise would not be caught + // Catch 2nd migration migration = migrationForError(e); if (migration) { console.log(`Running Migration ${migration.label} ...`); await migration.run(); result = await callback(); } else { - throw e; + try { + result = await callback(); + } catch (e: any) { + // Catch 3rd migration + migration = migrationForError(e); + if (migration) { + console.log(`Running Migration ${migration.label} ...`); + await migration.run(); + result = await callback(); + } else { + throw e; + } + } } } } else if (/relation "photos" does not exist/i.test(e.message)) { From 5f992788f2bbf5a030f9e58e15e10ce0ed3cdd21 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 31 Mar 2025 00:25:25 -0500 Subject: [PATCH 04/14] Switch film chooser to tag input --- app/admin/photos/[photoId]/edit/page.tsx | 17 +++++++++++------ app/admin/uploads/[uploadPath]/page.tsx | 9 ++++++++- src/film/index.ts | 10 ++++++++++ src/photo/PhotoEditPageClient.tsx | 4 ++++ src/photo/UploadPageClient.tsx | 4 ++++ src/photo/form/PhotoForm.tsx | 21 ++++----------------- src/photo/form/index.ts | 4 ++++ 7 files changed, 45 insertions(+), 24 deletions(-) diff --git a/app/admin/photos/[photoId]/edit/page.tsx b/app/admin/photos/[photoId]/edit/page.tsx index e81de2c7..1cf889f0 100644 --- a/app/admin/photos/[photoId]/edit/page.tsx +++ b/app/admin/photos/[photoId]/edit/page.tsx @@ -1,6 +1,7 @@ import { redirect } from 'next/navigation'; import { getPhotoNoStore, + getUniqueFilmsCached, getUniqueRecipesCached, getUniqueTagsCached, } from '@/photo/cache'; @@ -10,7 +11,6 @@ import { AI_TEXT_GENERATION_ENABLED, BLUR_ENABLED, IS_PREVIEW, - SHOW_RECIPES, } from '@/app/config'; import { blurImageFromUrl, resizeImageFromUrl } from '@/photo/server'; import { getNextImageUrlForManipulation } from '@/platforms/next-image'; @@ -26,11 +26,15 @@ export default async function PhotoEditPage({ if (!photo) { redirect(PATH_ADMIN); } - const uniqueTags = await getUniqueTagsCached(); - - const uniqueRecipes = SHOW_RECIPES - ? await getUniqueRecipesCached() - : []; + const [ + uniqueTags, + uniqueRecipes, + uniqueFilms, + ] = await Promise.all([ + getUniqueTagsCached(), + getUniqueRecipesCached(), + getUniqueFilmsCached(), + ]); const hasAiTextGeneration = AI_TEXT_GENERATION_ENABLED; @@ -52,6 +56,7 @@ export default async function PhotoEditPage({ photo, uniqueTags, uniqueRecipes, + uniqueFilms, hasAiTextGeneration, imageThumbnailBase64, blurData, diff --git a/app/admin/uploads/[uploadPath]/page.tsx b/app/admin/uploads/[uploadPath]/page.tsx index d55b9912..055be14e 100644 --- a/app/admin/uploads/[uploadPath]/page.tsx +++ b/app/admin/uploads/[uploadPath]/page.tsx @@ -1,7 +1,11 @@ import { PATH_ADMIN } from '@/app/paths'; import { extractImageDataFromBlobPath } from '@/photo/server'; import { redirect } from 'next/navigation'; -import { getUniqueRecipesCached, getUniqueTagsCached } from '@/photo/cache'; +import { + getUniqueFilmsCached, + getUniqueRecipesCached, + getUniqueTagsCached, +} from '@/photo/cache'; import UploadPageClient from '@/photo/UploadPageClient'; import { AI_TEXT_AUTO_GENERATED_FIELDS, @@ -45,10 +49,12 @@ export default async function UploadPage({ params }: Params) { const [ uniqueTags, uniqueRecipes, + uniqueFilms, recipeTitle, ] = await Promise.all([ getUniqueTagsCached(), getUniqueRecipesCached(), + getUniqueFilmsCached(), formDataFromExif?.recipeData && formDataFromExif.film ? getRecipeTitleForData( formDataFromExif.recipeData, @@ -72,6 +78,7 @@ export default async function UploadPage({ params }: Params) { formDataFromExif, uniqueTags, uniqueRecipes, + uniqueFilms, hasAiTextGeneration, textFieldsToAutoGenerate, imageThumbnailBase64, diff --git a/src/film/index.ts b/src/film/index.ts index b360e70f..23882656 100644 --- a/src/film/index.ts +++ b/src/film/index.ts @@ -12,6 +12,8 @@ import { FujifilmSimulation, labelForFilm, } from '@/platforms/fujifilm/simulation'; +import { formatCount } from '@/utility/string'; +import { formatCountDescriptive } from '@/utility/string'; export type FilmSimulation = FujifilmSimulation; @@ -82,3 +84,11 @@ export const generateMetaForFilm = ( export const photoHasFilmData = (photo: Photo) => Boolean(photo.film); + +export const convertFilmsForForm = (films: Films = []) => + sortFilms(films) + .map(({ film, count }) => ({ + value: film, + annotation: formatCount(count), + annotationAria: formatCountDescriptive(count), + })); diff --git a/src/photo/PhotoEditPageClient.tsx b/src/photo/PhotoEditPageClient.tsx index 79d32201..bfc3fe84 100644 --- a/src/photo/PhotoEditPageClient.tsx +++ b/src/photo/PhotoEditPageClient.tsx @@ -11,11 +11,13 @@ import usePhotoFormParent from './form/usePhotoFormParent'; import ExifCaptureButton from '@/admin/ExifCaptureButton'; import { useState } from 'react'; import { Recipes } from '@/recipe'; +import { Films } from '@/film'; export default function PhotoEditPageClient({ photo, uniqueTags, uniqueRecipes, + uniqueFilms, hasAiTextGeneration, imageThumbnailBase64, blurData, @@ -23,6 +25,7 @@ export default function PhotoEditPageClient({ photo: Photo uniqueTags: Tags uniqueRecipes: Recipes + uniqueFilms: Films hasAiTextGeneration: boolean imageThumbnailBase64: string blurData: string @@ -71,6 +74,7 @@ export default function PhotoEditPageClient({ updatedBlurData={blurData} uniqueTags={uniqueTags} uniqueRecipes={uniqueRecipes} + uniqueFilms={uniqueFilms} aiContent={hasAiTextGeneration ? aiContent : undefined} onTitleChange={setUpdatedTitle} onTextContentChange={setHasTextContent} diff --git a/src/photo/UploadPageClient.tsx b/src/photo/UploadPageClient.tsx index 37a1129d..5eebd787 100644 --- a/src/photo/UploadPageClient.tsx +++ b/src/photo/UploadPageClient.tsx @@ -10,12 +10,14 @@ import AiButton from './ai/AiButton'; import { AiAutoGeneratedField } from './ai'; import { useMemo } from 'react'; import { Recipes } from '@/recipe'; +import { Films } from '@/film'; export default function UploadPageClient({ blobId, formDataFromExif, uniqueTags, uniqueRecipes, + uniqueFilms, hasAiTextGeneration, textFieldsToAutoGenerate, imageThumbnailBase64, @@ -25,6 +27,7 @@ export default function UploadPageClient({ formDataFromExif: Partial uniqueTags: Tags uniqueRecipes: Recipes + uniqueFilms: Films hasAiTextGeneration?: boolean textFieldsToAutoGenerate?: AiAutoGeneratedField[], imageThumbnailBase64?: string @@ -65,6 +68,7 @@ export default function UploadPageClient({ initialPhotoForm={initialPhotoForm} uniqueTags={uniqueTags} uniqueRecipes={uniqueRecipes} + uniqueFilms={uniqueFilms} aiContent={hasAiTextGeneration ? aiContent : undefined} shouldStripGpsData={shouldStripGpsData} onTitleChange={setUpdatedTitle} diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index 09448321..c0fdef5a 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -41,13 +41,9 @@ 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 '@/film'; +import { convertFilmsForForm, Films, FilmSimulation } from '@/film'; import IconFavs from '@/components/icons/IconFavs'; import IconHidden from '@/components/icons/IconHidden'; -import { MAKE_FUJIFILM } from '@/platforms/fujifilm'; -import { - FILM_SIMULATION_FORM_INPUT_OPTIONS, -} from '@/platforms/fujifilm/simulation'; const THUMBNAIL_SIZE = 300; @@ -58,6 +54,7 @@ export default function PhotoForm({ updatedBlurData, uniqueTags, uniqueRecipes, + uniqueFilms, aiContent, shouldStripGpsData, onTitleChange, @@ -70,6 +67,7 @@ export default function PhotoForm({ updatedBlurData?: string uniqueTags?: Tags uniqueRecipes?: Recipes + uniqueFilms?: Films aiContent?: AiContent shouldStripGpsData?: boolean onTitleChange?: (updatedTitle: string) => void @@ -330,6 +328,7 @@ export default function PhotoForm({ {FORM_METADATA_ENTRIES( convertTagsForForm(uniqueTags), convertRecipesForForm(uniqueRecipes), + convertFilmsForForm(uniqueFilms), aiContent !== undefined, shouldStripGpsData, ) @@ -412,18 +411,6 @@ export default function PhotoForm({ }; switch (key) { - case 'film': - return formData.make === MAKE_FUJIFILM - ? - : ; case 'applyRecipeTitleGlobally': return => ({ @@ -121,6 +122,9 @@ const FORM_METADATA = ( label: 'film', note: 'Intended for Fujifilm simulations and analog scans', noteShort: 'Fujifilm simulations / analog scans', + tagOptions: filmOptions, + tagOptionsLimitValidationMessage: 'Photos can only have one film', + tagOptionsLimit: 1, shouldNotOverwriteWithNullDataOnSync: true, }, recipeTitle: { From c94e64162788ce5ab72cfc6ca285a291089a5acc Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 31 Mar 2025 00:27:54 -0500 Subject: [PATCH 05/14] Optimize photo edit network request --- app/admin/photos/[photoId]/edit/page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/admin/photos/[photoId]/edit/page.tsx b/app/admin/photos/[photoId]/edit/page.tsx index 1cf889f0..38dda275 100644 --- a/app/admin/photos/[photoId]/edit/page.tsx +++ b/app/admin/photos/[photoId]/edit/page.tsx @@ -22,20 +22,20 @@ export default async function PhotoEditPage({ }) { const { photoId } = await params; - const photo = await getPhotoNoStore(photoId, true); - - if (!photo) { redirect(PATH_ADMIN); } - const [ + photo, uniqueTags, uniqueRecipes, uniqueFilms, ] = await Promise.all([ + getPhotoNoStore(photoId, true), getUniqueTagsCached(), getUniqueRecipesCached(), getUniqueFilmsCached(), ]); + if (!photo) { redirect(PATH_ADMIN); } + const hasAiTextGeneration = AI_TEXT_GENERATION_ENABLED; // Only generate image thumbnails when AI generation is enabled From b943d3c10241c5a1af33aa29d0ed8336a159df27 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 31 Mar 2025 08:47:37 -0500 Subject: [PATCH 06/14] TagInput behaves as dropdown when limit is set to 1 --- src/components/TagInput.tsx | 28 ++++++++++++++++++++-------- src/photo/form/index.ts | 2 -- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/components/TagInput.tsx b/src/components/TagInput.tsx index b4767d2b..ce2939ab 100644 --- a/src/components/TagInput.tsx +++ b/src/components/TagInput.tsx @@ -34,6 +34,8 @@ export default function TagInput({ limit?: number limitValidationMessage?: string }) { + const behaveAsDropdown = limit === 1; + const containerRef = useRef(null); const inputRef = useRef(null); const optionsRef = useRef(null); @@ -51,8 +53,10 @@ export default function TagInput({ , [value]); const hasReachedLimit = useMemo(() => - limit !== undefined && selectedOptions.length >= limit - , [limit, selectedOptions]); + limit !== undefined && + selectedOptions.length >= limit && + !behaveAsDropdown + , [limit, behaveAsDropdown, selectedOptions]); const inputTextFormatted = parameterize(inputText); const isInputTextUnique = @@ -100,21 +104,29 @@ export default function TagInput({ .filter(option => !selectedOptions.includes(option)); if (optionsToAdd.length > 0) { - onChange?.([ - ...selectedOptions, - ...optionsToAdd, - ].join(',')); + if (behaveAsDropdown) { + // If behaving as dropdown, replace contents on add + onChange?.(optionsToAdd[0]); + } else { + onChange?.([ + ...selectedOptions, + ...optionsToAdd, + ].join(',')); + } } setSelectedOptionIndex(undefined); setInputText(''); - if (limit !== undefined && limit - 1 >= selectedOptions.length) { + if ( + behaveAsDropdown || + (limit !== undefined && limit - 1 >= selectedOptions.length) + ) { hideMenu(true); } else { inputRef.current?.focus(); } - }, [limit, selectedOptions, onChange, hideMenu]); + }, [limit, behaveAsDropdown, selectedOptions, onChange, hideMenu]); const removeOption = useCallback((option: string) => { onChange?.(selectedOptions.filter(o => diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index efeb78d7..0b8ca42c 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -123,7 +123,6 @@ const FORM_METADATA = ( note: 'Intended for Fujifilm simulations and analog scans', noteShort: 'Fujifilm simulations / analog scans', tagOptions: filmOptions, - tagOptionsLimitValidationMessage: 'Photos can only have one film', tagOptionsLimit: 1, shouldNotOverwriteWithNullDataOnSync: true, }, @@ -131,7 +130,6 @@ const FORM_METADATA = ( label: 'recipe title', tagOptions: recipeOptions, tagOptionsLimit: 1, - tagOptionsLimitValidationMessage: 'Photos can only have one recipe', spellCheck: false, capitalize: false, shouldHide: ({ make }) => make !== MAKE_FUJIFILM, From 16c58facc92e41d6acbcdf4820d3b70850e73a16 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 31 Mar 2025 09:13:13 -0500 Subject: [PATCH 07/14] Always show all film simulations for fujis --- src/film/index.ts | 36 ++++++++++++++++++++++++++------- src/photo/form/PhotoForm.tsx | 3 ++- src/platforms/fujifilm/index.ts | 5 ++++- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/film/index.ts b/src/film/index.ts index 23882656..c1324fa5 100644 --- a/src/film/index.ts +++ b/src/film/index.ts @@ -9,11 +9,13 @@ import { absolutePathForFilmImage, } from '@/app/paths'; import { + FILM_SIMULATION_FORM_INPUT_OPTIONS, FujifilmSimulation, labelForFilm, } from '@/platforms/fujifilm/simulation'; import { formatCount } from '@/utility/string'; import { formatCountDescriptive } from '@/utility/string'; +import { AnnotatedTag } from '@/photo/form'; export type FilmSimulation = FujifilmSimulation; @@ -85,10 +87,30 @@ export const generateMetaForFilm = ( export const photoHasFilmData = (photo: Photo) => Boolean(photo.film); -export const convertFilmsForForm = (films: Films = []) => - sortFilms(films) - .map(({ film, count }) => ({ - value: film, - annotation: formatCount(count), - annotationAria: formatCountDescriptive(count), - })); +export const convertFilmsForForm = ( + _films: Films = [], + includeAllFujifilmSimulations?: boolean, +): AnnotatedTag[] => { + const films = includeAllFujifilmSimulations + ? FILM_SIMULATION_FORM_INPUT_OPTIONS.map(film => ({ + value: film.value, + } as AnnotatedTag)) + : []; + + _films.forEach(({ film, count }) => { + const index = films.findIndex(f => f.value === film); + const annotation = formatCount(count); + const annotationAria = formatCountDescriptive(count); + if (index !== -1) { + films[index] = { + ...films[index], + annotation, + annotationAria, + }; + } else { + films.push({ value: film, annotation, annotationAria }); + } + }); + + return films.sort((a, b) => a.value.localeCompare(b.value)); +}; diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index c0fdef5a..6b8e5524 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -44,6 +44,7 @@ import ApplyRecipeTitleGloballyCheckbox from './ApplyRecipesGloballyCheckbox'; import { convertFilmsForForm, Films, FilmSimulation } from '@/film'; import IconFavs from '@/components/icons/IconFavs'; import IconHidden from '@/components/icons/IconHidden'; +import { isMakeFujifilm } from '@/platforms/fujifilm'; const THUMBNAIL_SIZE = 300; @@ -328,7 +329,7 @@ export default function PhotoForm({ {FORM_METADATA_ENTRIES( convertTagsForForm(uniqueTags), convertRecipesForForm(uniqueRecipes), - convertFilmsForForm(uniqueFilms), + convertFilmsForForm(uniqueFilms, isMakeFujifilm(formData.make)), aiContent !== undefined, shouldStripGpsData, ) diff --git a/src/platforms/fujifilm/index.ts b/src/platforms/fujifilm/index.ts index 1fe6ad9c..858a7ba2 100644 --- a/src/platforms/fujifilm/index.ts +++ b/src/platforms/fujifilm/index.ts @@ -20,7 +20,10 @@ const BYTES_PER_TAG = 12; const BYTES_PER_TAG_VALUE = 4; export const isExifForFujifilm = (data: ExifData) => - data.tags?.Make === MAKE_FUJIFILM; + data.tags?.Make?.toLocaleUpperCase() === MAKE_FUJIFILM; + +export const isMakeFujifilm = (make?: string) => + make?.toLocaleUpperCase() === MAKE_FUJIFILM; export const parseFujifilmMakerNote = ( bytes: Buffer, From 6f8eb85a866402e3f18552ce086ed80b2b1542b7 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 1 Apr 2025 09:29:41 -0500 Subject: [PATCH 08/14] Use full film simulation name in form --- src/components/TagInput.tsx | 16 +++++++++++++--- src/film/index.ts | 5 +++-- src/photo/form/index.ts | 3 +++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/components/TagInput.tsx b/src/components/TagInput.tsx index ce2939ab..ead6c722 100644 --- a/src/components/TagInput.tsx +++ b/src/components/TagInput.tsx @@ -2,7 +2,6 @@ import { AnnotatedTag } from '@/photo/form'; import { convertStringToArray, parameterize } from '@/utility/string'; import { clsx } from 'clsx/lite'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; - const KEY_KEYDOWN = 'keydown'; const CREATE_LABEL = 'Create'; @@ -244,6 +243,15 @@ export default function TagInput({ limit, ]); + const formatValue = useCallback((value: string) => { + const option = options.find(option => option.value === value); + return <> + {option?.icon} + {option?.label ?? value} + ; + }, + [options]); + return (
+ {/* Selected Options */} {selectedOptions .filter(Boolean) .map(option => @@ -302,7 +311,7 @@ export default function TagInput({ )} onClick={() => removeOption(option)} > - {option} + {formatValue(value)} )} + {/* Menu Options */} {optionsFiltered.map(({ value, annotation, @@ -378,7 +388,7 @@ export default function TagInput({ onFocus={() => setSelectedOptionIndex(index)} > - {value} + {formatValue(value)} {annotation && { const films = includeAllFujifilmSimulations - ? FILM_SIMULATION_FORM_INPUT_OPTIONS.map(film => ({ - value: film.value, + ? FILM_SIMULATION_FORM_INPUT_OPTIONS.map(({ value }) => ({ + value, + label: labelForFilm(value).large, } as AnnotatedTag)) : []; diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index 0b8ca42c..fc7ff3af 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -21,6 +21,7 @@ import { GEO_PRIVACY_ENABLED } from '@/app/config'; import { TAG_FAVS, getValidationMessageForTags } from '@/tag'; import { MAKE_FUJIFILM } from '@/platforms/fujifilm'; import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; +import { ReactNode } from 'react'; type VirtualFields = 'favorite' | @@ -41,6 +42,8 @@ export type FieldSetType = export type AnnotatedTag = { value: string, + label?: string, + icon?: ReactNode annotation?: string, annotationAria?: string, }; From 087a5e223c33cd927dc9a1d08ae9880bbb6f4395 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 1 Apr 2025 20:43:11 -0500 Subject: [PATCH 09/14] Finalize film dropdown icon --- src/components/FieldSetWithStatus.tsx | 3 + src/components/TagInput.tsx | 33 ++- src/film/PhotoFilmIcon.tsx | 258 +++++++++++---------- src/film/{index.ts => index.tsx} | 29 +-- src/image-response/RecipeImageResponse.tsx | 4 +- src/photo/form/PhotoForm.tsx | 11 + src/photo/form/index.ts | 4 +- src/platforms/fujifilm/simulation.ts | 5 +- 8 files changed, 195 insertions(+), 152 deletions(-) rename src/film/{index.ts => index.tsx} (79%) diff --git a/src/components/FieldSetWithStatus.tsx b/src/components/FieldSetWithStatus.tsx index 5b023b66..ee48dbf3 100644 --- a/src/components/FieldSetWithStatus.tsx +++ b/src/components/FieldSetWithStatus.tsx @@ -27,6 +27,7 @@ export default function FieldSetWithStatus({ tagOptions, tagOptionsLimit, tagOptionsLimitValidationMessage, + tagOptionsDefaultIcon, placeholder, loading, required, @@ -53,6 +54,7 @@ export default function FieldSetWithStatus({ tagOptions?: AnnotatedTag[] tagOptionsLimit?: number tagOptionsLimitValidationMessage?: string + tagOptionsDefaultIcon?: ReactNode placeholder?: string loading?: boolean required?: boolean @@ -199,6 +201,7 @@ export default function FieldSetWithStatus({ name={id} value={value} options={tagOptions} + defaultIcon={tagOptionsDefaultIcon} onChange={onChange} showMenuOnDelete={tagOptionsLimit === 1} className={clsx(Boolean(error) && 'error')} diff --git a/src/components/TagInput.tsx b/src/components/TagInput.tsx index ead6c722..7c5cf4b1 100644 --- a/src/components/TagInput.tsx +++ b/src/components/TagInput.tsx @@ -1,7 +1,14 @@ import { AnnotatedTag } from '@/photo/form'; import { convertStringToArray, parameterize } from '@/utility/string'; import { clsx } from 'clsx/lite'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; const KEY_KEYDOWN = 'keydown'; const CREATE_LABEL = 'Create'; @@ -13,6 +20,7 @@ export default function TagInput({ name, value = '', options = [], + defaultIcon, onChange, showMenuOnDelete, className, @@ -25,6 +33,7 @@ export default function TagInput({ name: string value?: string options?: AnnotatedTag[] + defaultIcon?: ReactNode onChange?: (value: string) => void showMenuOnDelete?: boolean className?: string @@ -243,14 +252,19 @@ export default function TagInput({ limit, ]); - const formatValue = useCallback((value: string) => { + const renderOptionContent = useCallback((value: string) => { const option = options.find(option => option.value === value); + const icon = option?.icon ?? defaultIcon; return <> - {option?.icon} - {option?.label ?? value} + + {option?.label ?? value} + + {icon && + {icon} + } ; }, - [options]); + [options, defaultIcon]); return (
removeOption(option)} > - {formatValue(value)} + {renderOptionContent(value)} )} setSelectedOptionIndex(index)} > - - {formatValue(value)} + + {renderOptionContent(value)} {annotation && + +; + export default function PhotoFilmIcon({ film, height = INTRINSIC_HEIGHT, @@ -17,6 +22,130 @@ export default function PhotoFilmIcon({ className?: string style?: CSSProperties }) { + const simulationIcon = useMemo(() => { + // Self-calling switch function and non-fragment groups + // necessary for ImageResponse compatibility + switch (film) { + case 'monochrome': return + + + ; + case 'monochrome-ye': return + + + + ; + case 'monochrome-r': return + + + + ; + case 'monochrome-g': return + + + + ; + case 'sepia': return + + + + ; + case 'acros': return + + + ; + case 'acros-ye': return + + + + ; + case 'acros-r': return + + + + ; + case 'acros-g': return + + + + ; + case 'provia': return + + + + ; + case 'portrait': return + + + ; + case 'portrait-saturation': return + + + + ; + case 'astia': return + + + ; + case 'portrait-sharpness': return + + + + ; + case 'portrait-ex': return + + + + ; + case 'velvia': return + + + ; + case 'pro-neg-std': return + + + + ; + case 'pro-neg-hi': return + + + + ; + case 'classic-chrome': return + + + + ; + case 'eterna': return + + + ; + case 'classic-neg': return + + + + ; + case 'eterna-bleach-bypass': return + + + + ; + case 'nostalgic-neg': return + + + + ; + case 'reala': return + + + ; + } + }, [film]); + + const width = simulationIcon + ? INTRINSIC_WIDTH + : INTRINSIC_WIDTH_FALLBACK; + return ( - {(() => { - // Self-calling switch function and non-fragment groups - // necessary for ImageResponse compatibility - switch (film) { - case 'monochrome': return - - - ; - case 'monochrome-ye': return - - - - ; - case 'monochrome-r': return - - - - ; - case 'monochrome-g': return - - - - ; - case 'sepia': return - - - - ; - case 'acros': return - - - ; - case 'acros-ye': return - - - - ; - case 'acros-r': return - - - - ; - case 'acros-g': return - - - - ; - case 'provia': return - - - - ; - case 'portrait': return - - - ; - case 'portrait-saturation': return - - - - ; - case 'astia': return - - - ; - case 'portrait-sharpness': return - - - - ; - case 'portrait-ex': return - - - - ; - case 'velvia': return - - - ; - case 'pro-neg-std': return - - - - ; - case 'pro-neg-hi': return - - - - ; - case 'classic-chrome': return - - - - ; - case 'eterna': return - - - ; - case 'classic-neg': return - - - - ; - case 'eterna-bleach-bypass': return - - - - ; - case 'nostalgic-neg': return - - - - ; - case 'reala': return - - - ; - default: return - - ; - } - })()} + {simulationIcon ?? FALLBACK_ICON} ); } diff --git a/src/film/index.ts b/src/film/index.tsx similarity index 79% rename from src/film/index.ts rename to src/film/index.tsx index a04a6b10..3433e99d 100644 --- a/src/film/index.ts +++ b/src/film/index.tsx @@ -16,6 +16,7 @@ import { import { formatCount } from '@/utility/string'; import { formatCountDescriptive } from '@/utility/string'; import { AnnotatedTag } from '@/photo/form'; +import PhotoFilmIcon from './PhotoFilmIcon'; export type FilmSimulation = FujifilmSimulation; @@ -92,24 +93,26 @@ export const convertFilmsForForm = ( includeAllFujifilmSimulations?: boolean, ): AnnotatedTag[] => { const films = includeAllFujifilmSimulations - ? FILM_SIMULATION_FORM_INPUT_OPTIONS.map(({ value }) => ({ - value, - label: labelForFilm(value).large, - } as AnnotatedTag)) + ? FILM_SIMULATION_FORM_INPUT_OPTIONS + .map(({ value }) => ({ value } as AnnotatedTag)) : []; _films.forEach(({ film, count }) => { const index = films.findIndex(f => f.value === film); - const annotation = formatCount(count); - const annotationAria = formatCountDescriptive(count); - if (index !== -1) { - films[index] = { - ...films[index], - annotation, - annotationAria, - }; + const fujifilmSimulation = FILM_SIMULATION_FORM_INPUT_OPTIONS + .find(f => f.value === film); + const meta = { + annotation: formatCount(count), + annotationAria: formatCountDescriptive(count), + ...fujifilmSimulation && { + label: labelForFilm(film).large, + icon: , + }, + }; + if (index === -1) { + films.push({ value: film, ...meta }); } else { - films.push({ value: film, annotation, annotationAria }); + films[index] = { ...films[index], ...meta }; } }); diff --git a/src/image-response/RecipeImageResponse.tsx b/src/image-response/RecipeImageResponse.tsx index f3caddf1..c666839e 100644 --- a/src/image-response/RecipeImageResponse.tsx +++ b/src/image-response/RecipeImageResponse.tsx @@ -6,7 +6,7 @@ import type { NextImageSize } from '@/platforms/next-image'; import { formatTag } from '@/tag'; import { generateRecipeText, getPhotoWithRecipeFromPhotos } from '@/recipe'; import PhotoFilmIcon from '@/film/PhotoFilmIcon'; -import { isStringFilmSimulation } from '@/platforms/fujifilm/simulation'; +import { isStringFilmSimulationLabel } from '@/platforms/fujifilm/simulation'; import IconRecipe from '@/components/icons/IconRecipe'; const MAX_RECIPE_LINES = 8; @@ -109,7 +109,7 @@ export default function RecipeImageResponse({ flexGrow: 1, }}> {text} - {isStringFilmSimulation(text) && film && + {isStringFilmSimulationLabel(text) && film &&
+ + } + {...fieldProps} + />; case 'applyRecipeTitleGlobally': return +export const isStringFilmSimulation = (film?: string) => + film && Object.keys(FILM_SIMULATION_LABELS).includes(film); + +export const isStringFilmSimulationLabel = (film: string) => ALL_POSSIBLE_FILM_SIMULATION_LABELS.includes(film.toLocaleLowerCase()); export const labelForFilm = (film: FujifilmSimulation) => From f9db50e41a4200fa1507721acfb657dd8dc6da8c Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 1 Apr 2025 21:08:36 -0500 Subject: [PATCH 10/14] Generalize film type and labeling strategy --- app/admin/uploads/[uploadPath]/page.tsx | 3 +- app/film-demo/animate/page.tsx | 6 +-- app/film-demo/page.tsx | 4 +- app/film/[film]/[photoId]/page.tsx | 5 +-- app/film/[film]/image/route.tsx | 3 +- app/film/[film]/page.tsx | 4 +- src/app/paths.ts | 9 ++--- src/category/index.ts | 4 +- src/cmdk/CommandKClient.tsx | 2 +- src/film/FilmHeader.tsx | 4 +- src/film/FilmOGTile.tsx | 8 +--- src/film/FilmOverview.tsx | 3 +- src/film/FilmShareModal.tsx | 4 +- src/film/PhotoFilm.tsx | 5 +-- src/film/PhotoFilmIcon.tsx | 5 +-- src/film/data.ts | 3 +- src/film/index.tsx | 38 ++++++++++++------- src/image-response/FilmImageResponse.tsx | 7 +--- src/image-response/RecipeImageResponse.tsx | 6 ++- src/photo/actions.ts | 3 +- src/photo/db/query.ts | 10 ++--- .../form/ApplyRecipesGloballyCheckbox.tsx | 3 +- src/photo/form/PhotoForm.tsx | 4 +- src/photo/form/index.ts | 6 +-- src/photo/index.ts | 5 ++- src/photo/server.ts | 4 +- src/platforms/fujifilm/simulation.ts | 22 +++++------ src/recipe/PhotoRecipeOverlay.tsx | 2 +- src/recipe/index.ts | 5 +-- 29 files changed, 92 insertions(+), 95 deletions(-) diff --git a/app/admin/uploads/[uploadPath]/page.tsx b/app/admin/uploads/[uploadPath]/page.tsx index 055be14e..3bc007e9 100644 --- a/app/admin/uploads/[uploadPath]/page.tsx +++ b/app/admin/uploads/[uploadPath]/page.tsx @@ -14,7 +14,6 @@ import { } from '@/app/config'; import ErrorNote from '@/components/ErrorNote'; import { getRecipeTitleForData } from '@/photo/db/query'; -import { FilmSimulation } from '@/film'; export const maxDuration = 60; @@ -58,7 +57,7 @@ export default async function UploadPage({ params }: Params) { formDataFromExif?.recipeData && formDataFromExif.film ? getRecipeTitleForData( formDataFromExif.recipeData, - formDataFromExif.film as FilmSimulation, + formDataFromExif.film, ) : undefined, ]); diff --git a/app/film-demo/animate/page.tsx b/app/film-demo/animate/page.tsx index 0542cda2..7c8c8d5a 100644 --- a/app/film-demo/animate/page.tsx +++ b/app/film-demo/animate/page.tsx @@ -3,7 +3,7 @@ import AppGrid from '@/components/AppGrid'; import { clsx } from 'clsx/lite'; import { - FILM_SIMULATION_FORM_INPUT_OPTIONS, + FUJIFILM_SIMULATION_FORM_INPUT_OPTIONS, } from '@/platforms/fujifilm/simulation'; import PhotoFilm from '@/film/PhotoFilm'; import { useEffect, useState } from 'react'; @@ -13,7 +13,7 @@ export default function FilmPage() { useEffect(() => { const interval = setInterval(() => { - setIndex((index + 1) % FILM_SIMULATION_FORM_INPUT_OPTIONS.length); + setIndex((index + 1) % FUJIFILM_SIMULATION_FORM_INPUT_OPTIONS.length); }, 200); return () => clearInterval(interval); }); @@ -28,7 +28,7 @@ export default function FilmPage() { Film Simulation:
diff --git a/app/film-demo/page.tsx b/app/film-demo/page.tsx index 972e4143..4bb841d0 100644 --- a/app/film-demo/page.tsx +++ b/app/film-demo/page.tsx @@ -1,12 +1,12 @@ import { - FILM_SIMULATION_FORM_INPUT_OPTIONS, + FUJIFILM_SIMULATION_FORM_INPUT_OPTIONS, } from '@/platforms/fujifilm/simulation'; import PhotoFilm from '@/film/PhotoFilm'; export default function FilmPage() { return (
- {FILM_SIMULATION_FORM_INPUT_OPTIONS.map(({ value }) => + {FUJIFILM_SIMULATION_FORM_INPUT_OPTIONS.map(({ value }) =>
getPhotosNearIdCached( photoId, @@ -28,7 +27,7 @@ const getPhotosNearIdCachedCached = cache(( )); interface PhotoFilmProps { - params: Promise<{ photoId: string, film: FilmSimulation }> + params: Promise<{ photoId: string, film: string }> } export async function generateMetadata({ diff --git a/app/film/[film]/image/route.tsx b/app/film/[film]/image/route.tsx index 83a77b22..330d0355 100644 --- a/app/film/[film]/image/route.tsx +++ b/app/film/[film]/image/route.tsx @@ -5,7 +5,6 @@ import { } from '@/image-response'; 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'; @@ -21,7 +20,7 @@ export const generateStaticParams = staticallyGenerateCategoryIfConfigured( export async function GET( _: Request, - context: { params: Promise<{ film: FilmSimulation }> }, + context: { params: Promise<{ film: string }> }, ) { const { film } = await context.params; diff --git a/app/film/[film]/page.tsx b/app/film/[film]/page.tsx index 2fdff053..c521a4ea 100644 --- a/app/film/[film]/page.tsx +++ b/app/film/[film]/page.tsx @@ -1,6 +1,6 @@ import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo'; import { getUniqueFilms } from '@/photo/db/query'; -import { FilmSimulation, generateMetaForFilm } from '@/film'; +import { generateMetaForFilm } from '@/film'; import FilmOverview from '@/film/FilmOverview'; import { getPhotosFilmDataCached } from '@/film/data'; import { Metadata } from 'next/types'; @@ -20,7 +20,7 @@ export const generateStaticParams = staticallyGenerateCategoryIfConfigured( ); interface FilmProps { - params: Promise<{ film: FilmSimulation }> + params: Promise<{ film: string }> } export async function generateMetadata({ diff --git a/src/app/paths.ts b/src/app/paths.ts index 311f1da8..b275a832 100644 --- a/src/app/paths.ts +++ b/src/app/paths.ts @@ -2,7 +2,6 @@ import { Photo } from '@/photo'; import { PhotoSetCategory } from '@/category'; import { BASE_URL, GRID_HOMEPAGE_ENABLED } from './config'; import { Camera } from '@/camera'; -import { FilmSimulation } from '@/film'; import { parameterize } from '@/utility/string'; import { TAG_HIDDEN } from '@/tag'; import { Lens } from '@/lens'; @@ -151,7 +150,7 @@ export const pathForTag = (tag: string) => export const pathForCamera = ({ make, model }: Camera) => `${PREFIX_CAMERA}/${parameterize(make)}/${parameterize(model)}`; -export const pathForFilm = (film: FilmSimulation) => +export const pathForFilm = (film: string) => `${PREFIX_FILM}/${film}`; export const pathForLens = ({ make, model }: Lens) => @@ -177,7 +176,7 @@ export const absolutePathForCamera= (camera: Camera) => export const absolutePathForLens= (lens: Lens) => `${BASE_URL}${pathForLens(lens)}`; -export const absolutePathForFilm = (film: FilmSimulation) => +export const absolutePathForFilm = (film: string) => `${BASE_URL}${pathForFilm(film)}`; export const absolutePathForRecipe = (recipe: string) => @@ -198,7 +197,7 @@ export const absolutePathForCameraImage= (camera: Camera) => export const absolutePathForLensImage= (lens: Lens) => `${absolutePathForLens(lens)}/image`; -export const absolutePathForFilmImage = (film: FilmSimulation) => +export const absolutePathForFilmImage = (film: string) => `${absolutePathForFilm(film)}/image`; export const absolutePathForRecipeImage = (recipe: string) => @@ -308,7 +307,7 @@ export const getPathComponents = (pathname = ''): { const cameraModel = pathname.match( new RegExp(`^${PREFIX_CAMERA}/[^/]+/([^/]+)`))?.[1]; const film = pathname.match( - new RegExp(`^${PREFIX_FILM}/([^/]+)`))?.[1] as FilmSimulation; + new RegExp(`^${PREFIX_FILM}/([^/]+)`))?.[1] as string; const focalString = pathname.match( new RegExp(`^${PREFIX_FOCAL_LENGTH}/([0-9]+)mm`))?.[1]; diff --git a/src/category/index.ts b/src/category/index.ts index 698330cc..4c70238a 100644 --- a/src/category/index.ts +++ b/src/category/index.ts @@ -1,7 +1,7 @@ import { Photo } from '../photo'; import { Camera, Cameras } from '@/camera'; import { PhotoDateRange } from '../photo'; -import { FilmSimulation, Films } from '@/film'; +import { 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 - film?: FilmSimulation + film?: string focal?: number } diff --git a/src/cmdk/CommandKClient.tsx b/src/cmdk/CommandKClient.tsx index aaddd55d..42fd5501 100644 --- a/src/cmdk/CommandKClient.tsx +++ b/src/cmdk/CommandKClient.tsx @@ -59,7 +59,6 @@ import * as VisuallyHidden from '@radix-ui/react-visually-hidden'; import InsightsIndicatorDot from '@/admin/insights/InsightsIndicatorDot'; import { PhotoSetCategories } from '@/category'; import { formatCameraText } from '@/camera'; -import { labelForFilm } from '@/platforms/fujifilm/simulation'; import { formatFocalLength } from '@/focal'; import { formatRecipe } from '@/recipe'; import IconLens from '../components/icons/IconLens'; @@ -73,6 +72,7 @@ import IconFilm from '../components/icons/IconFilm'; import IconLock from '../components/icons/IconLock'; import useVisualViewportHeight from '@/utility/useVisualViewport'; import useMaskedScroll from '../components/useMaskedScroll'; +import { labelForFilm } from '@/film'; const DIALOG_TITLE = 'Global Command-K Menu'; const DIALOG_DESCRIPTION = 'For searching photos, views, and settings'; diff --git a/src/film/FilmHeader.tsx b/src/film/FilmHeader.tsx index ee2b14f6..5e101df8 100644 --- a/src/film/FilmHeader.tsx +++ b/src/film/FilmHeader.tsx @@ -1,5 +1,5 @@ import { Photo, PhotoDateRange } from '@/photo'; -import { FilmSimulation, descriptionForFilmPhotos } from '.'; +import { descriptionForFilmPhotos } from '.'; import PhotoHeader from '@/photo/PhotoHeader'; import PhotoFilm from '@/film/PhotoFilm'; @@ -11,7 +11,7 @@ export default function FilmHeader({ count, dateRange, }: { - film: FilmSimulation + film: string photos: Photo[] selectedPhoto?: Photo indexNumber?: number diff --git a/src/film/FilmOGTile.tsx b/src/film/FilmOGTile.tsx index 19f7843a..399f7d08 100644 --- a/src/film/FilmOGTile.tsx +++ b/src/film/FilmOGTile.tsx @@ -4,11 +4,7 @@ import { pathForFilm, } from '@/app/paths'; import OGTile from '@/components/OGTile'; -import { - FilmSimulation, - descriptionForFilmPhotos, - titleForFilm, -} from '.'; +import { descriptionForFilmPhotos, titleForFilm } from '.'; export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed'; @@ -23,7 +19,7 @@ export default function FilmOGTile({ count, dateRange, }: { - film: FilmSimulation + film: string photos: Photo[] loadingState?: OGLoadingState onLoad?: () => void diff --git a/src/film/FilmOverview.tsx b/src/film/FilmOverview.tsx index 1ff9e59a..6f5f579d 100644 --- a/src/film/FilmOverview.tsx +++ b/src/film/FilmOverview.tsx @@ -1,6 +1,5 @@ import { Photo, PhotoDateRange } from '@/photo'; import FilmHeader from './FilmHeader'; -import { FilmSimulation } from '.'; import PhotoGridContainer from '@/photo/PhotoGridContainer'; export default function FilmOverview({ @@ -10,7 +9,7 @@ export default function FilmOverview({ dateRange, animateOnFirstLoadOnly, }: { - film: FilmSimulation, + film: string, photos: Photo[], count: number, dateRange?: PhotoDateRange, diff --git a/src/film/FilmShareModal.tsx b/src/film/FilmShareModal.tsx index 1c5b3a3c..9fdff02c 100644 --- a/src/film/FilmShareModal.tsx +++ b/src/film/FilmShareModal.tsx @@ -2,7 +2,7 @@ import { absolutePathForFilm } from '@/app/paths'; import { PhotoSetAttributes } from '../category'; import ShareModal from '@/share/ShareModal'; import FilmOGTile from './FilmOGTile'; -import { FilmSimulation, shareTextForFilm } from '.'; +import { shareTextForFilm } from '.'; export default function FilmShareModal({ film, @@ -10,7 +10,7 @@ export default function FilmShareModal({ count, dateRange, }: { - film: FilmSimulation + film: string } & PhotoSetAttributes) { return ( Promise.all([ diff --git a/src/film/index.tsx b/src/film/index.tsx index 3433e99d..30426df7 100644 --- a/src/film/index.tsx +++ b/src/film/index.tsx @@ -9,24 +9,36 @@ import { absolutePathForFilmImage, } from '@/app/paths'; import { - FILM_SIMULATION_FORM_INPUT_OPTIONS, - FujifilmSimulation, - labelForFilm, + FUJIFILM_SIMULATION_FORM_INPUT_OPTIONS, + labelForFujifilmSimulation, } from '@/platforms/fujifilm/simulation'; -import { formatCount } from '@/utility/string'; +import { deparameterize, formatCount } from '@/utility/string'; import { formatCountDescriptive } from '@/utility/string'; import { AnnotatedTag } from '@/photo/form'; import PhotoFilmIcon from './PhotoFilmIcon'; -export type FilmSimulation = FujifilmSimulation; - export type FilmWithCount = { - film: FilmSimulation + film: string count: number } export type Films = FilmWithCount[] +export const labelForFilm = (film: string) => { + // Use Fujifilm simulation text when recognized + const simulationLabel = labelForFujifilmSimulation(film as any); + if (simulationLabel) { + return simulationLabel; + } else { + const filmFormatted = deparameterize(film); + return { + small: filmFormatted, + medium: filmFormatted, + large: filmFormatted, + }; + } +}; + export const sortFilms = ( films: Films, ) => films.sort(sortFilmsWithCount); @@ -41,7 +53,7 @@ export const sortFilmsWithCount = ( }; export const titleForFilm = ( - film: FilmSimulation, + film: string, photos: Photo[], explicitCount?: number, ) => [ @@ -50,9 +62,9 @@ export const titleForFilm = ( ].join(' '); export const shareTextForFilm = ( - film: FilmSimulation, + film: string, ) => - `Photos shot on Fujifilm ${labelForFilm(film).large}`; + `Photos shot on ${labelForFilm(film).large}`; export const descriptionForFilmPhotos = ( photos: Photo[], @@ -69,7 +81,7 @@ export const descriptionForFilmPhotos = ( ); export const generateMetaForFilm = ( - film: FilmSimulation, + film: string, photos: Photo[], explicitCount?: number, explicitDateRange?: PhotoDateRange, @@ -93,13 +105,13 @@ export const convertFilmsForForm = ( includeAllFujifilmSimulations?: boolean, ): AnnotatedTag[] => { const films = includeAllFujifilmSimulations - ? FILM_SIMULATION_FORM_INPUT_OPTIONS + ? FUJIFILM_SIMULATION_FORM_INPUT_OPTIONS .map(({ value }) => ({ value } as AnnotatedTag)) : []; _films.forEach(({ film, count }) => { const index = films.findIndex(f => f.value === film); - const fujifilmSimulation = FILM_SIMULATION_FORM_INPUT_OPTIONS + const fujifilmSimulation = FUJIFILM_SIMULATION_FORM_INPUT_OPTIONS .find(f => f.value === film); const meta = { annotation: formatCount(count), diff --git a/src/image-response/FilmImageResponse.tsx b/src/image-response/FilmImageResponse.tsx index 68bb0cf5..2eb76d7c 100644 --- a/src/image-response/FilmImageResponse.tsx +++ b/src/image-response/FilmImageResponse.tsx @@ -2,13 +2,10 @@ import { Photo } from '../photo'; import ImageCaption from './components/ImageCaption'; import ImagePhotoGrid from './components/ImagePhotoGrid'; import ImageContainer from './components/ImageContainer'; -import { - labelForFilm, -} from '@/platforms/fujifilm/simulation'; import PhotoFilmIcon from '@/film/PhotoFilmIcon'; -import { FilmSimulation } from '@/film'; import { NextImageSize } from '@/platforms/next-image'; +import { labelForFilm } from '@/film'; export default function FilmImageResponse({ film, @@ -17,7 +14,7 @@ export default function FilmImageResponse({ height, fontFamily, }: { - film: FilmSimulation, + film: string, photos: Photo[] width: NextImageSize height: number diff --git a/src/image-response/RecipeImageResponse.tsx b/src/image-response/RecipeImageResponse.tsx index c666839e..a2af4863 100644 --- a/src/image-response/RecipeImageResponse.tsx +++ b/src/image-response/RecipeImageResponse.tsx @@ -6,7 +6,9 @@ import type { NextImageSize } from '@/platforms/next-image'; import { formatTag } from '@/tag'; import { generateRecipeText, getPhotoWithRecipeFromPhotos } from '@/recipe'; import PhotoFilmIcon from '@/film/PhotoFilmIcon'; -import { isStringFilmSimulationLabel } from '@/platforms/fujifilm/simulation'; +import { + isStringFujifilmSimulationLabel, +} from '@/platforms/fujifilm/simulation'; import IconRecipe from '@/components/icons/IconRecipe'; const MAX_RECIPE_LINES = 8; @@ -109,7 +111,7 @@ export default function RecipeImageResponse({ flexGrow: 1, }}> {text} - {isStringFilmSimulationLabel(text) && film && + {isStringFujifilmSimulationLabel(text) && film &&
export const getPhotosNeedingRecipeTitleCountAction = async ( recipeData: string, - film: FilmSimulation, + film: string, photoIdToExclude?: string, ) => runAuthenticatedAdminServerAction(async () => diff --git a/src/photo/db/query.ts b/src/photo/db/query.ts index d6f5ad0f..cf5ff252 100644 --- a/src/photo/db/query.ts +++ b/src/photo/db/query.ts @@ -13,7 +13,7 @@ import { } from '@/photo'; import { Cameras, createCameraKey } from '@/camera'; import { Tags } from '@/tag'; -import { FilmSimulation, Films } from '@/film'; +import { Films } from '@/film'; import { ADMIN_SQL_DEBUG_ENABLED } from '@/app/config'; import { GetPhotosOptions, @@ -380,7 +380,7 @@ export const getUniqueRecipes = async () => export const getRecipeTitleForData = async ( data: string | object, - film: FilmSimulation, + film: string, ) => // Includes legacy check on pre-stringified JSON safelyQueryPhotos(() => sql` @@ -395,7 +395,7 @@ export const getRecipeTitleForData = async ( export const getPhotosNeedingRecipeTitleCount = async ( data: string, - film: FilmSimulation, + film: string, photoIdToExclude?: string, ) => safelyQueryPhotos(() => sql` @@ -411,7 +411,7 @@ export const getPhotosNeedingRecipeTitleCount = async ( export const updateAllMatchingRecipeTitles = ( title: string, data: string, - film: FilmSimulation, + film: string, ) => safelyQueryPhotos(() => sql` UPDATE photos @@ -430,7 +430,7 @@ export const getUniqueFilms = async () => ORDER BY film ASC `.then(({ rows }): Films => rows .map(({ film, count }) => ({ - film: film as FilmSimulation, + film, count: parseInt(count, 10), }))) , 'getUniqueFilms'); diff --git a/src/photo/form/ApplyRecipesGloballyCheckbox.tsx b/src/photo/form/ApplyRecipesGloballyCheckbox.tsx index 6bd5f7ce..38754a77 100644 --- a/src/photo/form/ApplyRecipesGloballyCheckbox.tsx +++ b/src/photo/form/ApplyRecipesGloballyCheckbox.tsx @@ -1,7 +1,6 @@ import FieldSetWithStatus from '@/components/FieldSetWithStatus'; import { ComponentProps, useEffect, useState } from 'react'; import { getPhotosNeedingRecipeTitleCountAction } from '../actions'; -import { FilmSimulation } from '@/film'; export default function ApplyRecipeTitleGloballyCheckbox({ photoId, @@ -16,7 +15,7 @@ export default function ApplyRecipeTitleGloballyCheckbox({ recipeTitle?: string hasRecipeTitleChanged?: boolean recipeData?: string - film?: FilmSimulation + film?: string onMatchResults: (didFindMatchingPhotos: boolean) => void }) { const [matchingPhotosCount, setMatchingPhotosCount] = useState(); diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index 65444b96..75ce5d8c 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -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 { convertFilmsForForm, Films, FilmSimulation } from '@/film'; +import { convertFilmsForForm, Films } from '@/film'; import IconFavs from '@/components/icons/IconFavs'; import IconHidden from '@/components/icons/IconHidden'; import { isMakeFujifilm } from '@/platforms/fujifilm'; @@ -431,7 +431,7 @@ export default function PhotoForm({ hasRecipeTitleChanged={ changedFormKeys.includes('recipeTitle')} recipeData={formData.recipeData} - film={formData.film as FilmSimulation} + film={formData.film} onMatchResults={onMatchResults} {...fieldProps} />; diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index 716fd1bb..3b4f1633 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -16,12 +16,12 @@ import { import { roundToNumber } from '@/utility/number'; import { convertStringToArray, parameterize } from '@/utility/string'; import { generateNanoid } from '@/utility/nanoid'; -import { FilmSimulation } from '@/film'; import { GEO_PRIVACY_ENABLED } from '@/app/config'; import { TAG_FAVS, getValidationMessageForTags } from '@/tag'; import { MAKE_FUJIFILM } from '@/platforms/fujifilm'; import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; import { ReactNode } from 'react'; +import { FujifilmSimulation } from '@/platforms/fujifilm/simulation'; type VirtualFields = 'favorite' | @@ -276,7 +276,7 @@ export const convertPhotoToFormData = (photo: Photo): PhotoFormData => { export const convertExifToFormData = ( data: ExifData, - film?: FilmSimulation, + film?: FujifilmSimulation, recipeData?: FujifilmRecipe, ): Omit< Record, @@ -347,7 +347,7 @@ export const convertFormDataToPhotoDbInsert = ( return { ...(photoForm as PhotoFormData & { - film?: FilmSimulation + film?: FujifilmSimulation recipeData?: FujifilmRecipe }), ...!photoForm.id && { id: generateNanoid() }, diff --git a/src/photo/index.ts b/src/photo/index.ts index 1b0d6a33..3e25eb9d 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -1,6 +1,6 @@ import { formatFocalLength } from '@/focal'; import { getNextImageUrlForRequest } from '@/platforms/next-image'; -import { FilmSimulation, photoHasFilmData } from '@/film'; +import { photoHasFilmData } from '@/film'; import { HIGH_DENSITY_GRID, IS_PREVIEW, @@ -22,6 +22,7 @@ import camelcaseKeys from 'camelcase-keys'; import { isBefore } from 'date-fns'; import type { Metadata } from 'next'; import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; +import { FujifilmSimulation } from '@/platforms/fujifilm/simulation'; // INFINITE SCROLL: FEED export const INFINITE_SCROLL_FEED_INITIAL = @@ -65,7 +66,7 @@ export interface PhotoExif { exposureCompensation?: number latitude?: number longitude?: number - film?: FilmSimulation + film?: FujifilmSimulation recipeData?: string takenAt?: string takenAtNaive?: string diff --git a/src/photo/server.ts b/src/photo/server.ts index 22ab8376..b09a50fa 100644 --- a/src/photo/server.ts +++ b/src/photo/server.ts @@ -7,11 +7,11 @@ import { convertFormDataToPhotoDbInsert, } from '@/photo/form'; import { + FujifilmSimulation, getFujifilmSimulationFromMakerNote, } from '@/platforms/fujifilm/simulation'; import { ExifData, ExifParserFactory } from 'ts-exif-parser'; import { PhotoFormData } from './form'; -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 film: FilmSimulation | undefined; + let film: FujifilmSimulation | undefined; let recipe: FujifilmRecipe | undefined; let blurData: string | undefined; let imageResizedBase64: string | undefined; diff --git a/src/platforms/fujifilm/simulation.ts b/src/platforms/fujifilm/simulation.ts index 59b82035..ae6d1026 100644 --- a/src/platforms/fujifilm/simulation.ts +++ b/src/platforms/fujifilm/simulation.ts @@ -80,7 +80,7 @@ interface FujifilmSimulationLabel { large: string } -const FILM_SIMULATION_LABELS: Record< +const FUJIFILM_SIMULATION_LABELS: Record< FujifilmSimulation, FujifilmSimulationLabel > = { @@ -206,30 +206,30 @@ const FILM_SIMULATION_LABELS: Record< }, }; -export const FILM_SIMULATION_FORM_INPUT_OPTIONS = Object - .entries(FILM_SIMULATION_LABELS) +export const FUJIFILM_SIMULATION_FORM_INPUT_OPTIONS = Object + .entries(FUJIFILM_SIMULATION_LABELS) .map(([value, label]) => ( { value, label: label.large } as { value: FujifilmSimulation, label: string } )) .sort((a, b) => a.label.localeCompare(b.label)); -const ALL_POSSIBLE_FILM_SIMULATION_LABELS = Object - .values(FILM_SIMULATION_LABELS) +const ALL_POSSIBLE_FUJIFILM_SIMULATION_LABELS = Object + .values(FUJIFILM_SIMULATION_LABELS) .flatMap(({ small, medium, large }) => [ small.toLocaleLowerCase(), medium.toLocaleLowerCase(), large.toLocaleLowerCase(), ]); -export const isStringFilmSimulation = (film?: string) => - film && Object.keys(FILM_SIMULATION_LABELS).includes(film); +export const isStringFujifilmSimulation = (film?: string) => + film && Object.keys(FUJIFILM_SIMULATION_LABELS).includes(film); -export const isStringFilmSimulationLabel = (film: string) => - ALL_POSSIBLE_FILM_SIMULATION_LABELS.includes(film.toLocaleLowerCase()); +export const isStringFujifilmSimulationLabel = (film: string) => + ALL_POSSIBLE_FUJIFILM_SIMULATION_LABELS.includes(film.toLocaleLowerCase()); -export const labelForFilm = (film: FujifilmSimulation) => - FILM_SIMULATION_LABELS[film]; +export const labelForFujifilmSimulation = (film: FujifilmSimulation) => + FUJIFILM_SIMULATION_LABELS[film]; export const getFujifilmSimulationFromMakerNote = ( bytes: Buffer, diff --git a/src/recipe/PhotoRecipeOverlay.tsx b/src/recipe/PhotoRecipeOverlay.tsx index 1a140874..e3c02eb8 100644 --- a/src/recipe/PhotoRecipeOverlay.tsx +++ b/src/recipe/PhotoRecipeOverlay.tsx @@ -15,11 +15,11 @@ import { generateRecipeText, RecipeProps, } from '.'; -import { labelForFilm } from '@/platforms/fujifilm/simulation'; import { TbChecklist } from 'react-icons/tb'; import CopyButton from '@/components/CopyButton'; import { pathForRecipe } from '@/app/paths'; import LinkWithStatus from '@/components/LinkWithStatus'; +import { labelForFilm } from '@/film'; export default function PhotoRecipeOverlay({ ref, diff --git a/src/recipe/index.ts b/src/recipe/index.ts index 0d6f102d..616e1849 100644 --- a/src/recipe/index.ts +++ b/src/recipe/index.ts @@ -7,8 +7,7 @@ import { formatCountDescriptive, } from '@/utility/string'; import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; -import { FilmSimulation } from '@/film'; -import { labelForFilm } from '@/platforms/fujifilm/simulation'; +import { labelForFilm } from '@/film'; export type RecipeWithCount = { recipe: string @@ -20,7 +19,7 @@ export type Recipes = RecipeWithCount[] export interface RecipeProps { title?: string recipe: FujifilmRecipe - film: FilmSimulation + film: string iso?: string exposure?: string } From 99dde505b362a02ce9318d2ab9c8f34008144602 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 1 Apr 2025 21:15:55 -0500 Subject: [PATCH 11/14] Standardize recipe props --- .vscode/settings.json | 1 + app/admin/recipes/[recipe]/edit/page.tsx | 2 +- src/admin/AdminShowRecipeButton.tsx | 19 ++------- src/image-response/RecipeImageResponse.tsx | 2 +- src/photo/PhotoLarge.tsx | 2 +- src/recipe/PhotoRecipeOverlay.tsx | 12 +++--- src/recipe/RecipeHeader.tsx | 2 +- src/recipe/index.ts | 46 +++++++++++----------- 8 files changed, 36 insertions(+), 50 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ea94cecd..4a786c5d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "Astia", "authjs", "camelcase", + "CLAR", "cloudflarestorage", "cmdk", "Consolas", diff --git a/app/admin/recipes/[recipe]/edit/page.tsx b/app/admin/recipes/[recipe]/edit/page.tsx index c9db0fbd..9565cc55 100644 --- a/app/admin/recipes/[recipe]/edit/page.tsx +++ b/app/admin/recipes/[recipe]/edit/page.tsx @@ -45,7 +45,7 @@ export default async function RecipePageEdit({ accessory={recipeData && film && } diff --git a/src/admin/AdminShowRecipeButton.tsx b/src/admin/AdminShowRecipeButton.tsx index bfe97689..80fd1ca8 100644 --- a/src/admin/AdminShowRecipeButton.tsx +++ b/src/admin/AdminShowRecipeButton.tsx @@ -1,20 +1,11 @@ 'use client'; import LoaderButton from '@/components/primitives/LoaderButton'; -import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; -import { FujifilmSimulation } from '@/platforms/fujifilm/simulation'; +import { RecipeProps } from '@/recipe'; import { useAppState } from '@/state/AppState'; import { TbChecklist } from 'react-icons/tb'; -export default function AdminShowRecipeButton({ - title, - recipe, - film, -}: { - title: string - recipe: FujifilmRecipe - film: FujifilmSimulation -}) { +export default function AdminShowRecipeButton(props: RecipeProps) { const { setRecipeModalProps } = useAppState(); return ( @@ -23,11 +14,7 @@ export default function AdminShowRecipeButton({ size={17} className="translate-y-[1px]" />} - onClick={() => setRecipeModalProps?.({ - title, - recipe, - film, - })} + onClick={() => setRecipeModalProps?.(props)} > Preview diff --git a/src/image-response/RecipeImageResponse.tsx b/src/image-response/RecipeImageResponse.tsx index a2af4863..ba311a67 100644 --- a/src/image-response/RecipeImageResponse.tsx +++ b/src/image-response/RecipeImageResponse.tsx @@ -34,7 +34,7 @@ export default function RecipeImageResponse({ let recipeLines = recipeData && film ? generateRecipeText({ - recipe: recipeData, + data: recipeData, film, }, true) : []; diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 61a5f9e3..74ca9b53 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -229,7 +229,7 @@ export default function PhotoLarge({ Date: Tue, 1 Apr 2025 22:55:43 -0500 Subject: [PATCH 12/14] Optimize sign out behavior --- src/admin/AdminAppMenu.tsx | 4 ++-- src/app/Footer.tsx | 4 ++-- src/auth/SignInForm.tsx | 17 ++++++++++------- src/cmdk/CommandKClient.tsx | 6 ++++-- src/state/AppState.ts | 2 +- src/state/AppStateProvider.tsx | 10 ++++++---- 6 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx index 5bdc8521..2f53d0a1 100644 --- a/src/admin/AdminAppMenu.tsx +++ b/src/admin/AdminAppMenu.tsx @@ -44,7 +44,7 @@ export default function AdminAppMenu({ startUpload, setSelectedPhotoIds, refreshAdminData, - clearAuthStateAndRedirect, + clearAuthStateAndRedirectIfNecessary, } = useAppState(); const isSelecting = selectedPhotoIds !== undefined; @@ -148,7 +148,7 @@ export default function AdminAppMenu({ }, { label: 'Sign Out', icon: , - action: () => signOutAction().then(clearAuthStateAndRedirect), + action: () => signOutAction().then(clearAuthStateAndRedirectIfNecessary), }); return ( diff --git a/src/app/Footer.tsx b/src/app/Footer.tsx index eb982578..ba9930fc 100644 --- a/src/app/Footer.tsx +++ b/src/app/Footer.tsx @@ -17,7 +17,7 @@ import { useAppState } from '@/state/AppState'; export default function Footer() { const pathname = usePathname(); - const { userEmail, clearAuthStateAndRedirect } = useAppState(); + const { userEmail, clearAuthStateAndRedirectIfNecessary } = useAppState(); const showFooter = !isPathSignIn(pathname); @@ -49,7 +49,7 @@ export default function Footer() { {userEmail}
signOutAction() - .then(clearAuthStateAndRedirect)}> + .then(clearAuthStateAndRedirectIfNecessary)}> Sign out diff --git a/src/auth/SignInForm.tsx b/src/auth/SignInForm.tsx index a59e7119..cc51dc86 100644 --- a/src/auth/SignInForm.tsx +++ b/src/auth/SignInForm.tsx @@ -61,15 +61,18 @@ export default function SignInForm({ password.length > 0; return ( - + {includeTitle &&

@@ -77,7 +80,7 @@ export default function SignInForm({

} -
+
{response === KEY_CREDENTIALS_SIGN_IN_ERROR && Invalid email/password diff --git a/src/cmdk/CommandKClient.tsx b/src/cmdk/CommandKClient.tsx index 42fd5501..ef6d9320 100644 --- a/src/cmdk/CommandKClient.tsx +++ b/src/cmdk/CommandKClient.tsx @@ -124,7 +124,7 @@ export default function CommandKClient({ const { isUserSignedIn, - clearAuthStateAndRedirect, + clearAuthStateAndRedirectIfNecessary, isCommandKOpen: isOpen, startUpload, photosCountHidden, @@ -521,7 +521,9 @@ export default function CommandKClient({ } adminSection.items.push({ label: 'Sign Out', - action: () => signOutAction().then(clearAuthStateAndRedirect), + action: () => signOutAction() + .then(clearAuthStateAndRedirectIfNecessary) + .then(() => setIsOpen?.(false)), }); } else { adminSection.items.push({ diff --git a/src/state/AppState.ts b/src/state/AppState.ts index 0ce7039e..590a6ebc 100644 --- a/src/state/AppState.ts +++ b/src/state/AppState.ts @@ -39,7 +39,7 @@ export type AppStateContextType = { setUserEmail?: Dispatch> isUserSignedIn?: boolean isUserSignedInEager?: boolean - clearAuthStateAndRedirect?: () => void + clearAuthStateAndRedirectIfNecessary?: () => void // ADMIN isCheckingAuth?: boolean adminUpdateTimes?: Date[] diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index 60fca69e..7a18fd76 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -21,7 +21,7 @@ import { hasAuthEmailCookie, } from '@/auth/client'; import { useRouter, usePathname } from 'next/navigation'; -import { isPathAdmin, PATH_SIGN_IN } from '@/app/paths'; +import { isPathAdmin, PATH_ROOT } from '@/app/paths'; import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload'; import { RecipeProps } from '@/recipe'; import { getCountsForCategoriesCachedAction } from '@/category/actions'; @@ -105,6 +105,8 @@ export default function AppStateProvider({ setIsUserSignedInEager(hasAuthEmailCookie()); if (!authError) { setUserEmail(auth?.user?.email ?? undefined); + } else { + setIsUserSignedInEager(false); } }, [auth, authError]); const isUserSignedIn = Boolean(userEmail); @@ -133,11 +135,11 @@ export default function AppStateProvider({ setAdminUpdateTimes(updates => [...updates, new Date()]) , []); - const clearAuthStateAndRedirect = useCallback(() => { + const clearAuthStateAndRedirectIfNecessary = useCallback(() => { setUserEmail(undefined); setIsUserSignedInEager(false); clearAuthEmailCookie(); - if (isPathAdmin(pathname)) { router.push(PATH_SIGN_IN); } + if (isPathAdmin(pathname)) { router.push(PATH_ROOT); } }, [router, pathname]); // Returns false when upload is cancelled @@ -187,7 +189,7 @@ export default function AppStateProvider({ setUserEmail, isUserSignedIn, isUserSignedInEager, - clearAuthStateAndRedirect, + clearAuthStateAndRedirectIfNecessary, // ADMIN adminUpdateTimes, registerAdminUpdate, From df41838e6f177fdff8c662a5c18b8c25e8cb03e0 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 1 Apr 2025 23:35:32 -0500 Subject: [PATCH 13/14] Fix fujifilm simulation input rendering --- src/components/TagInput.tsx | 14 ++++++++------ src/film/PhotoFilmIcon.tsx | 2 +- src/film/index.tsx | 18 +++++++++--------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/components/TagInput.tsx b/src/components/TagInput.tsx index 7c5cf4b1..6e8e2bc0 100644 --- a/src/components/TagInput.tsx +++ b/src/components/TagInput.tsx @@ -252,8 +252,8 @@ export default function TagInput({ limit, ]); - const renderOptionContent = useCallback((value: string) => { - const option = options.find(option => option.value === value); + const renderTag = useCallback((value: string) => { + const option = options.find(o => o.value === value); const icon = option?.icon ?? defaultIcon; return <> @@ -263,8 +263,7 @@ export default function TagInput({ {icon} } ; - }, - [options, defaultIcon]); + }, [options, defaultIcon]); return (
removeOption(option)} > - {renderOptionContent(value)} + {renderTag(option)} )} setSelectedOptionIndex(undefined)} + onClick={() => { + if (!shouldShowMenu) { setShouldShowMenu(true); } + }} aria-autocomplete="list" aria-expanded={shouldShowMenu} aria-haspopup="true" @@ -403,7 +405,7 @@ export default function TagInput({ onFocus={() => setSelectedOptionIndex(index)} > - {renderOptionContent(value)} + {renderTag(value)} {annotation && diff --git a/src/film/index.tsx b/src/film/index.tsx index 30426df7..341acfb1 100644 --- a/src/film/index.tsx +++ b/src/film/index.tsx @@ -104,22 +104,16 @@ export const convertFilmsForForm = ( _films: Films = [], includeAllFujifilmSimulations?: boolean, ): AnnotatedTag[] => { - const films = includeAllFujifilmSimulations + const films: AnnotatedTag[] = includeAllFujifilmSimulations ? FUJIFILM_SIMULATION_FORM_INPUT_OPTIONS - .map(({ value }) => ({ value } as AnnotatedTag)) + .map(({ value }) => ({ value })) : []; _films.forEach(({ film, count }) => { const index = films.findIndex(f => f.value === film); - const fujifilmSimulation = FUJIFILM_SIMULATION_FORM_INPUT_OPTIONS - .find(f => f.value === film); const meta = { annotation: formatCount(count), annotationAria: formatCountDescriptive(count), - ...fujifilmSimulation && { - label: labelForFilm(film).large, - icon: , - }, }; if (index === -1) { films.push({ value: film, ...meta }); @@ -128,5 +122,11 @@ export const convertFilmsForForm = ( } }); - return films.sort((a, b) => a.value.localeCompare(b.value)); + return films + .map(film => ({ + ...film, + label: labelForFilm(film.value).large, + icon: , + })) + .sort((a, b) => a.value.localeCompare(b.value)); }; From 51a614d97f9db6d9d8a6052d4869fd629535ec69 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 1 Apr 2025 23:43:29 -0500 Subject: [PATCH 14/14] Finalize non-fuji film icon treatment --- src/film/PhotoFilm.tsx | 3 ++- src/film/PhotoFilmIcon.tsx | 6 +++--- src/platforms/fujifilm/simulation.ts | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/film/PhotoFilm.tsx b/src/film/PhotoFilm.tsx index c28a115d..98d83eab 100644 --- a/src/film/PhotoFilm.tsx +++ b/src/film/PhotoFilm.tsx @@ -6,6 +6,7 @@ import EntityLink, { } from '@/components/primitives/EntityLink'; import clsx from 'clsx/lite'; import { labelForFilm } from '.'; +import { isStringFujifilmSimulation } from '@/platforms/fujifilm/simulation'; export default function PhotoFilm({ film, @@ -41,7 +42,7 @@ export default function PhotoFilm({ badged={badged} contrast={contrast} hoverEntity={countOnHover} - iconWide + iconWide={isStringFujifilmSimulation(film)} /> ); } diff --git a/src/film/PhotoFilmIcon.tsx b/src/film/PhotoFilmIcon.tsx index 684eeb5d..b412f30d 100644 --- a/src/film/PhotoFilmIcon.tsx +++ b/src/film/PhotoFilmIcon.tsx @@ -1,5 +1,5 @@ /* eslint-disable max-len */ -import { CSSProperties, useMemo } from 'react'; +import { CSSProperties } from 'react'; import { labelForFilm } from '.'; const INTRINSIC_WIDTH = 28; @@ -21,7 +21,7 @@ export default function PhotoFilmIcon({ className?: string style?: CSSProperties }) { - const simulationIcon = useMemo(() => { + const simulationIcon = (() => { // Self-calling switch function and non-fragment groups // necessary for ImageResponse compatibility switch (film) { @@ -139,7 +139,7 @@ export default function PhotoFilmIcon({ ; } - }, [film]); + })(); const width = simulationIcon ? INTRINSIC_WIDTH diff --git a/src/platforms/fujifilm/simulation.ts b/src/platforms/fujifilm/simulation.ts index ae6d1026..1778bbee 100644 --- a/src/platforms/fujifilm/simulation.ts +++ b/src/platforms/fujifilm/simulation.ts @@ -223,7 +223,8 @@ const ALL_POSSIBLE_FUJIFILM_SIMULATION_LABELS = Object ]); export const isStringFujifilmSimulation = (film?: string) => - film && Object.keys(FUJIFILM_SIMULATION_LABELS).includes(film); + film !== undefined && + Object.keys(FUJIFILM_SIMULATION_LABELS).includes(film); export const isStringFujifilmSimulationLabel = (film: string) => ALL_POSSIBLE_FUJIFILM_SIMULATION_LABELS.includes(film.toLocaleLowerCase());