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 (
);
}
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) =>