Refine camera/lens query handling

This commit is contained in:
Sam Becker 2025-03-16 16:48:48 -05:00
parent 0ca8823dae
commit 3966a6437a
8 changed files with 61 additions and 26 deletions

10
__tests__/lens.test.ts Normal file
View File

@ -0,0 +1,10 @@
/* eslint-disable max-len */
import { formatLensText, Lens } from '@/lens';
const IPHONE_15_PRO_FRONT: Lens = { make: 'Apple', model: 'iPhone 15 Pro front TrueDepth camera 2.69mm f/1.9' };
describe('Lens', () => {
it('correctly formats iPhone lenses', () => {
expect(formatLensText(IPHONE_15_PRO_FRONT)).toBe('Front Camera');
});
});

View File

@ -7,7 +7,6 @@ import PhotoSmall from '@/photo/PhotoSmall';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
import { pathForAdminPhotoEdit, pathForPhoto } from '@/app/paths'; import { pathForAdminPhotoEdit, pathForPhoto } from '@/app/paths';
import Link from 'next/link'; import Link from 'next/link';
import { AiOutlineEyeInvisible } from 'react-icons/ai';
import PhotoDate from '@/photo/PhotoDate'; import PhotoDate from '@/photo/PhotoDate';
import EditButton from './EditButton'; import EditButton from './EditButton';
import { useAppState } from '@/state/AppState'; import { useAppState } from '@/state/AppState';
@ -15,6 +14,7 @@ import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll';
import PhotoSyncButton from './PhotoSyncButton'; import PhotoSyncButton from './PhotoSyncButton';
import DeletePhotoButton from './DeletePhotoButton'; import DeletePhotoButton from './DeletePhotoButton';
import { Timezone } from '@/utility/timezone'; import { Timezone } from '@/utility/timezone';
import IconHidden from '@/components/icons/IconHidden';
export default function AdminPhotosTable({ export default function AdminPhotosTable({
photos, photos,
@ -71,7 +71,7 @@ export default function AdminPhotosTable({
{titleForPhoto(photo)} {titleForPhoto(photo)}
{photo.hidden && <span className="whitespace-nowrap"> {photo.hidden && <span className="whitespace-nowrap">
{' '} {' '}
<AiOutlineEyeInvisible <IconHidden
className="inline translate-y-[-0.5px]" className="inline translate-y-[-0.5px]"
size={16} size={16}
/> />

View File

@ -140,13 +140,13 @@ export const pathForTag = (tag: string) =>
`${PREFIX_TAG}/${tag}`; `${PREFIX_TAG}/${tag}`;
export const pathForCamera = ({ make, model }: Camera) => export const pathForCamera = ({ make, model }: Camera) =>
`${PREFIX_CAMERA}/${parameterize(make, true)}/${parameterize(model, true)}`; `${PREFIX_CAMERA}/${parameterize(make)}/${parameterize(model)}`;
export const pathForFilmSimulation = (simulation: FilmSimulation) => export const pathForFilmSimulation = (simulation: FilmSimulation) =>
`${PREFIX_FILM_SIMULATION}/${simulation}`; `${PREFIX_FILM_SIMULATION}/${simulation}`;
export const pathForLens = ({ make, model }: Lens) => export const pathForLens = ({ make, model }: Lens) =>
`${PREFIX_LENS}/${parameterize(make, true)}/${parameterize(model, true)}`; `${PREFIX_LENS}/${parameterize(make)}/${parameterize(model)}`;
export const pathForFocalLength = (focal: number) => export const pathForFocalLength = (focal: number) =>
`${PREFIX_FOCAL_LENGTH}/${focal}mm`; `${PREFIX_FOCAL_LENGTH}/${focal}mm`;

View File

@ -28,7 +28,7 @@ export type Cameras = CameraWithCount[];
// Support keys for make-only and model-only camera queries // Support keys for make-only and model-only camera queries
export const createCameraKey = ({ make, model }: Partial<Camera>) => export const createCameraKey = ({ make, model }: Partial<Camera>) =>
parameterize(`${make ?? 'ANY'}-${model ?? 'ANY'}`, true); parameterize(`${make ?? 'ANY'}-${model ?? 'ANY'}`);
export const getCameraFromParams = ({ export const getCameraFromParams = ({
make, make,
@ -37,8 +37,8 @@ export const getCameraFromParams = ({
make: string, make: string,
model: string, model: string,
}): Camera => ({ }): Camera => ({
make: parameterize(make, true), make: parameterize(make),
model: parameterize(model, true), model: parameterize(model),
}); });
export const sortCamerasWithCount = ( export const sortCamerasWithCount = (

View File

@ -28,7 +28,7 @@ export type Lenses = LensWithCount[];
// Support keys for make-only and model-only lens queries // Support keys for make-only and model-only lens queries
export const createLensKey = ({ make, model }: Partial<Lens>) => export const createLensKey = ({ make, model }: Partial<Lens>) =>
parameterize(`${make ?? 'ANY'}-${model ?? 'ANY'}`, true); parameterize(`${make ?? 'ANY'}-${model ?? 'ANY'}`);
export const getLensFromParams = ({ export const getLensFromParams = ({
make, make,
@ -37,8 +37,8 @@ export const getLensFromParams = ({
make: string, make: string,
model: string, model: string,
}): Lens => ({ }): Lens => ({
make: parameterize(make, true), make: parameterize(make),
model: parameterize(model, true), model: parameterize(model),
}); });
export const lensFromPhoto = ( export const lensFromPhoto = (
@ -55,13 +55,20 @@ const isLensMakeApple = (make?: string) =>
export const isLensApple = ({ make }: Lens) => export const isLensApple = ({ make }: Lens) =>
isLensMakeApple(make); isLensMakeApple(make);
const formatAppleLensText = (model: string) => {
if (model.includes('front TrueDepth camera')) {
return 'Front Camera';
}
return model;
};
export const formatLensText = ( export const formatLensText = (
{ make, model: modelRaw }: Lens, { make, model: modelRaw }: Lens,
length: length:
'long' | // Unmodified make and model 'long' | // Unmodified make and model
'medium' | // Make and model, with modifiers removed 'medium' | // Make and model, with modifiers removed
'short' // Model only 'short' // Model only
= 'medium', = 'short',
) => { ) => {
// Capture simple make without modifiers like 'Corporation' or 'Company' // Capture simple make without modifiers like 'Corporation' or 'Company'
const makeSimple = make.match(/^(\S+)/)?.[1]; const makeSimple = make.match(/^(\S+)/)?.[1];
@ -69,7 +76,11 @@ export const formatLensText = (
makeSimple && makeSimple &&
modelRaw.toLocaleLowerCase().startsWith(makeSimple.toLocaleLowerCase()) modelRaw.toLocaleLowerCase().startsWith(makeSimple.toLocaleLowerCase())
); );
const model = modelRaw;
const model = isLensMakeApple(make)
? formatAppleLensText(modelRaw)
: modelRaw;
switch (length) { switch (length) {
case 'long': case 'long':
case 'medium': case 'medium':

View File

@ -9,10 +9,11 @@ export const PHOTO_DEFAULT_LIMIT = 100;
// Trim whitespace // Trim whitespace
// Make lowercase // Make lowercase
// Replace spaces with dashes // Remove commas
// Remove periods and commas // Replace spaces, slashes with dashes
const parameterizeForDb = (text: string) => const parameterizeForDb = (field: string) =>
`REPLACE(REPLACE(REPLACE(LOWER(TRIM(${text})), '.', ''), ',', ''), ' ', '-')`; // eslint-disable-next-line max-len
`REPLACE(REPLACE(REPLACE(LOWER(TRIM(${field})), ',', ''), '/', '-'), ' ', '-')`;
export type GetPhotosOptions = { export type GetPhotosOptions = {
sortBy?: 'createdAt' | 'createdAtAsc' | 'takenAt' | 'priority' sortBy?: 'createdAt' | 'createdAtAsc' | 'takenAt' | 'priority'
@ -64,6 +65,19 @@ export const getWheresFromOptions = (
break; break;
} }
if (options.lens?.make) {
console.log('LENS MODEL QUERY',{
db: parameterizeForDb('lens_make'),
args: parameterize(options.lens.make),
});
}
if (options.lens?.model) {
console.log('LENS MODEL QUERY',{
db: parameterizeForDb('lens_model'),
args: parameterize(options.lens.model),
});
}
if (takenBefore) { if (takenBefore) {
wheres.push(`taken_at < $${valuesIndex++}`); wheres.push(`taken_at < $${valuesIndex++}`);
wheresValues.push(takenBefore.toISOString()); wheresValues.push(takenBefore.toISOString());
@ -87,19 +101,19 @@ export const getWheresFromOptions = (
} }
if (camera?.make) { if (camera?.make) {
wheres.push(`${parameterizeForDb('make')}=$${valuesIndex++}`); wheres.push(`${parameterizeForDb('make')}=$${valuesIndex++}`);
wheresValues.push(parameterize(camera.make, true)); wheresValues.push(parameterize(camera.make));
} }
if (camera?.model) { if (camera?.model) {
wheres.push(`${parameterizeForDb('model')}=$${valuesIndex++}`); wheres.push(`${parameterizeForDb('model')}=$${valuesIndex++}`);
wheresValues.push(parameterize(camera.model, true)); wheresValues.push(parameterize(camera.model));
} }
if (lens?.make) { if (lens?.make) {
wheres.push(`${parameterizeForDb('lens_make')}=$${valuesIndex++}`); wheres.push(`${parameterizeForDb('lens_make')}=$${valuesIndex++}`);
wheresValues.push(parameterize(lens.make, true)); wheresValues.push(parameterize(lens.make));
} }
if (lens?.model) { if (lens?.model) {
wheres.push(`${parameterizeForDb('lens_model')}=$${valuesIndex++}`); wheres.push(`${parameterizeForDb('lens_model')}=$${valuesIndex++}`);
wheresValues.push(parameterize(lens.model, true)); wheresValues.push(parameterize(lens.model));
} }
if (tag) { if (tag) {
wheres.push(`$${valuesIndex++}=ANY(tags)`); wheres.push(`$${valuesIndex++}=ANY(tags)`);

View File

@ -1,9 +1,9 @@
import { TAG_HIDDEN } from '.'; import { TAG_HIDDEN } from '.';
import { pathForTag } from '@/app/paths'; import { pathForTag } from '@/app/paths';
import IconHidden from '@/components/icons/IconHidden';
import EntityLink, { import EntityLink, {
EntityLinkExternalProps, EntityLinkExternalProps,
} from '@/components/primitives/EntityLink'; } from '@/components/primitives/EntityLink';
import { AiOutlineEyeInvisible } from 'react-icons/ai';
export default function HiddenTag({ export default function HiddenTag({
type, type,
@ -20,14 +20,14 @@ export default function HiddenTag({
label={badged label={badged
? <span className="inline-flex items-center gap-1"> ? <span className="inline-flex items-center gap-1">
{TAG_HIDDEN} {TAG_HIDDEN}
<AiOutlineEyeInvisible <IconHidden
size={13} size={13}
className="translate-y-[-0.5px]" className="translate-y-[-0.5px]"
/> />
</span> </span>
: TAG_HIDDEN} : TAG_HIDDEN}
href={pathForTag(TAG_HIDDEN)} href={pathForTag(TAG_HIDDEN)}
icon={!badged && <AiOutlineEyeInvisible size={16} />} icon={!badged && <IconHidden size={16} />}
type={type} type={type}
className={className} className={className}
hoverEntity={countOnHover} hoverEntity={countOnHover}

View File

@ -22,10 +22,10 @@ export const parameterize = (
) => ) =>
string string
.trim() .trim()
// Replaces spaces, underscores, and dashes with dashes // Replaces spaces, underscores, slashes,and dashes with dashes
.replaceAll(/[\s_]/gi, '-') .replaceAll(/[\s_/]/gi, '-')
// Removes punctuation // Removes punctuation
.replaceAll(/['"!@#$%^&*()_+=[\]{};:/?,.<>\\|`~]/gi, '') .replaceAll(/['"!@#$%^&*()_+=[\]{};:/?,<>\\|`~]/gi, '')
// Removes all non-alphanumeric characters // Removes all non-alphanumeric characters
.replaceAll( .replaceAll(
shouldRemoveNonAlphanumeric shouldRemoveNonAlphanumeric