diff --git a/__tests__/lens.test.ts b/__tests__/lens.test.ts
index 5b392cae..d9a8a302 100644
--- a/__tests__/lens.test.ts
+++ b/__tests__/lens.test.ts
@@ -2,15 +2,15 @@
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' };
-const IPHONE_15_PRO_BACK_WIDE: Lens = { make: 'Apple', model: 'iPhone 15 Pro front TrueDepth camera 2.69mm f/1.9' };
-const IPHONE_15_PRO_BACK_MAIN: Lens = { make: 'Apple', model: 'iPhone 15 Pro front TrueDepth camera 2.69mm f/1.9' };
-const IPHONE_15_PRO_BACK_TELEPHOTO: Lens = { make: 'Apple', model: 'iPhone 15 Pro front TrueDepth camera 2.69mm f/1.9' };
+const IPHONE_15_PRO_BACK_WIDE: Lens = { make: 'Apple', model: 'iPhone 15 Pro back triple camera 6.765mm f/2.2' };
+const IPHONE_15_PRO_BACK_MAIN: Lens = { make: 'Apple', model: 'iPhone 15 Pro back triple camera 6.765mm f/1.78' };
+const IPHONE_15_PRO_BACK_TELEPHOTO: Lens = { make: 'Apple', model: 'iPhone 15 Pro back triple camera 6.765mm f/2.8' };
describe('Lens', () => {
it('correctly formats iPhone lenses', () => {
- expect(formatLensText(IPHONE_15_PRO_FRONT)).toBe('Front Camera');
- expect(formatLensText(IPHONE_15_PRO_BACK_WIDE)).toBe('Wide Camera');
- expect(formatLensText(IPHONE_15_PRO_BACK_MAIN)).toBe('Main Camera');
- expect(formatLensText(IPHONE_15_PRO_BACK_TELEPHOTO)).toBe('Telephoto Camera');
+ expect(formatLensText(IPHONE_15_PRO_FRONT)).toBe('15 Pro front');
+ expect(formatLensText(IPHONE_15_PRO_BACK_WIDE)).toBe('15 Pro Wide (6.765mm)');
+ expect(formatLensText(IPHONE_15_PRO_BACK_MAIN)).toBe('15 Pro Main (6.765mm)');
+ expect(formatLensText(IPHONE_15_PRO_BACK_TELEPHOTO)).toBe('15 Pro Telephoto (6.765mm)');
});
});
diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx
index 0ea73823..68bc42aa 100644
--- a/src/admin/AdminAppMenu.tsx
+++ b/src/admin/AdminAppMenu.tsx
@@ -161,7 +161,8 @@ export default function AdminAppMenu({
header={
Admin menu
}
diff --git a/src/app/paths.ts b/src/app/paths.ts
index d4f85a7c..f73abe29 100644
--- a/src/app/paths.ts
+++ b/src/app/paths.ts
@@ -113,9 +113,9 @@ const getPhotoId = (photoOrPhotoId: PhotoOrPhotoId) =>
export const pathForPhoto = ({
photo,
- tag,
camera,
lens,
+ tag,
simulation,
focal,
recipe,
diff --git a/src/components/cmdk/CommandKClient.tsx b/src/components/cmdk/CommandKClient.tsx
index b84898b0..9da4215f 100644
--- a/src/components/cmdk/CommandKClient.tsx
+++ b/src/components/cmdk/CommandKClient.tsx
@@ -37,12 +37,12 @@ import { useDebounce } from 'use-debounce';
import Spinner from '../Spinner';
import { usePathname, useRouter } from 'next/navigation';
import { useTheme } from 'next-themes';
-import { BiDesktop, BiMoon, BiSun } from 'react-icons/bi';
+import { BiDesktop, BiLockAlt, BiMoon, BiSun } from 'react-icons/bi';
import { IoInvertModeSharp } from 'react-icons/io5';
import { useAppState } from '@/state/AppState';
import { searchPhotosAction } from '@/photo/actions';
import { RiToolsFill } from 'react-icons/ri';
-import { BiLockAlt, BiSolidUser } from 'react-icons/bi';
+import { BiSolidUser } from 'react-icons/bi';
import { HiDocumentText } from 'react-icons/hi';
import { signOutAction } from '@/auth/actions';
import { getKeywordsForPhoto, titleForPhoto } from '@/photo';
@@ -69,6 +69,7 @@ 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';
const DIALOG_TITLE = 'Global Command-K Menu';
const DIALOG_DESCRIPTION = 'For searching photos, views, and settings';
@@ -424,20 +425,20 @@ export default function CommandKClient({
if (isUserSignedIn) {
adminSection.items.push({
label: 'Manage Photos',
- annotation: ,
+ annotation: ,
path: PATH_ADMIN_PHOTOS,
});
if (uploadsCount) {
adminSection.items.push({
label: 'Manage Uploads',
- annotation: ,
+ annotation: ,
path: PATH_ADMIN_UPLOADS,
});
}
if (tagsCount) {
adminSection.items.push({
label: 'Manage Tags',
- annotation: ,
+ annotation: ,
path: PATH_ADMIN_TAGS,
});
}
@@ -448,17 +449,17 @@ export default function CommandKClient({
}
,
keywords: ['app insights'],
- annotation: ,
+ annotation: ,
path: PATH_ADMIN_INSIGHTS,
}, {
label: 'App Config',
- annotation: ,
+ annotation: ,
path: PATH_ADMIN_CONFIGURATION,
}, {
label: selectedPhotoIds === undefined
? 'Select Multiple Photos'
: 'Exit Select Multiple Photos',
- annotation: ,
+ annotation: ,
path: selectedPhotoIds === undefined
? PATH_GRID_INFERRED
: undefined,
diff --git a/src/components/icons/IconLock.tsx b/src/components/icons/IconLock.tsx
index 86aeeb86..05a07c23 100644
--- a/src/components/icons/IconLock.tsx
+++ b/src/components/icons/IconLock.tsx
@@ -1,6 +1,12 @@
import { IconBaseProps } from 'react-icons';
+import { BiLockAlt } from 'react-icons/bi';
import { FiLock } from 'react-icons/fi';
-export default function IconLock(props: IconBaseProps) {
- return ;
+export default function IconLock({
+ narrow,
+ ...props
+}: IconBaseProps & { narrow?: boolean }) {
+ return narrow
+ ?
+ : ;
}
diff --git a/src/components/primitives/EntityLink.tsx b/src/components/primitives/EntityLink.tsx
index 9fbfa094..8e9a38f6 100644
--- a/src/components/primitives/EntityLink.tsx
+++ b/src/components/primitives/EntityLink.tsx
@@ -31,6 +31,7 @@ export default function EntityLink({
truncate = true,
className,
classNameIcon,
+ uppercase,
debug,
}: {
icon: ReactNode
@@ -44,6 +45,7 @@ export default function EntityLink({
truncate?: boolean
className?: string
classNameIcon?: string
+ uppercase?: boolean
debug?: boolean
} & EntityLinkExternalProps) {
const classForContrast = () => {
@@ -81,6 +83,7 @@ export default function EntityLink({
prefetch,
title,
type,
+ uppercase,
className: clsx(
classForContrast(),
href && !badged && 'hover:text-gray-900 dark:hover:text-gray-100',
diff --git a/src/components/primitives/LabeledIcon.tsx b/src/components/primitives/LabeledIcon.tsx
index 9ab702e5..e0d15f4a 100644
--- a/src/components/primitives/LabeledIcon.tsx
+++ b/src/components/primitives/LabeledIcon.tsx
@@ -15,6 +15,7 @@ export default function LabeledIcon({
classNameIcon,
children,
iconWide,
+ uppercase = true,
debug,
}: {
icon?: ReactNode,
@@ -23,6 +24,7 @@ export default function LabeledIcon({
classNameIcon?: string,
children: ReactNode,
iconWide?:boolean,
+ uppercase?: boolean,
debug?: boolean,
}) {
return (
@@ -44,7 +46,8 @@ export default function LabeledIcon({
}
{children && type !== 'icon-only' &&
{children}
diff --git a/src/lens/PhotoLens.tsx b/src/lens/PhotoLens.tsx
index 604bdb44..1e462f0a 100644
--- a/src/lens/PhotoLens.tsx
+++ b/src/lens/PhotoLens.tsx
@@ -13,13 +13,15 @@ export default function PhotoLens({
prefetch,
countOnHover,
className,
+ shortText,
}: {
lens: Lens
countOnHover?: number
+ shortText?: boolean
} & EntityLinkExternalProps) {
return (
+ make?.toLocaleLowerCase() === LENS_MAKE_APPLE;
+
+export const isLensApple = ({ make }: Lens) =>
+ isLensMakeApple(make);
+
+export const formatAppleLensText = (
+ model: string,
+ includePhoneName?: boolean,
+) => {
+ const [
+ _,
+ phoneName,
+ side,
+ focalLength,
+ aperture,
+ ] = (/iPhone ([0-9a-z]{1,3}(?: (?:Pro|Max))*).*?(back|front).*?([0-9\.]+)mm.*?f\/([0-9\.]+)/gi.exec(model) ?? []);
+
+ const format = (lensName: string, includeFocalLength = true) => {
+ let result = '';
+ if (includePhoneName) {
+ result += `${phoneName} `;
+ }
+ result += lensName;
+ if (!includePhoneName) {
+ result += ' Camera';
+ }
+ if (includeFocalLength && focalLength) {
+ result += ` (${focalLength}mm)`;
+ }
+ return result;
+ };
+
+ if (side === 'front') {
+ return format('front', false);
+ } else if (side === 'back') {
+ switch (phoneName) {
+ case '13 Pro':
+ switch (aperture) {
+ case '1.8': return format('Wide');
+ case '1.5': return format('Main');
+ case '2.8': return format('Telephoto');
+ }
+ case '14 Pro':
+ switch (aperture) {
+ case '2.2': return format('Wide');
+ case '1.78': return format('Main');
+ case '2.8': return format('Telephoto');
+ }
+ case '15 Pro':
+ switch (aperture) {
+ case '2.2': return format('Wide');
+ case '1.78': return format('Main');
+ case '2.8': return format('Telephoto');
+ }
+ case '16 Pro':
+ switch (aperture) {
+ case '2.2': return format('Wide');
+ case '1.78': return format('Main');
+ case '2.8': return format('Telephoto');
+ }
+ }
+ }
+
+ return model;
+};
\ No newline at end of file
diff --git a/src/lens/index.ts b/src/lens/index.ts
index 7aa7182f..9e36c191 100644
--- a/src/lens/index.ts
+++ b/src/lens/index.ts
@@ -1,10 +1,9 @@
import { Photo } from '@/photo';
import { parameterize } from '@/utility/string';
+import { formatAppleLensText, isLensMakeApple } from './apple';
const LENS_PLACEHOLDER: Lens = { make: 'Lens', model: 'Model' };
-const LENS_MAKE_APPLE = 'apple';
-
export type Lens = {
make: string
model: string
@@ -58,41 +57,13 @@ export const lensFromPhoto = (
? { make: photo.lensMake, model: photo.lensModel }
: fallback ?? LENS_PLACEHOLDER;
-const isLensMakeApple = (make?: string) =>
- make?.toLocaleLowerCase() === LENS_MAKE_APPLE;
-
-export const isLensApple = ({ make }: Lens) =>
- isLensMakeApple(make);
-
-const formatAppleLensText = (
- model: string,
- includePhoneName?: boolean,
-) => {
- if (model.includes('15 Pro')) {
- const phoneName = '15 Pro';
- if (model.includes('front')) { return includePhoneName
- ? `${phoneName}: Front Camera`
- : 'Front Camera'; }
- if (model.includes('f/2.2')) { return includePhoneName
- ? `${phoneName}: Wide Camera`
- : 'Wide Camera'; }
- if (model.includes('f/1.78')) { return includePhoneName
- ? `${phoneName}: Main Camera`
- : 'Main Camera'; }
- if (model.includes('f/2.8')) { return includePhoneName
- ? `${phoneName}: Telephoto Camera`
- : 'Telephoto Camera'; }
- }
- return model;
-};
-
export const formatLensText = (
{ make, model: modelRaw }: Lens,
length:
'long' | // Unmodified make and model
'medium' | // Make and model, with modifiers removed
'short' // Model only
- = 'short',
+ = 'medium',
) => {
// Capture simple make without modifiers like 'Corporation' or 'Company'
const makeSimple = make.match(/^(\S+)/)?.[1];
diff --git a/src/lens/meta.ts b/src/lens/meta.ts
index 696cb87b..6b7036ac 100644
--- a/src/lens/meta.ts
+++ b/src/lens/meta.ts
@@ -19,7 +19,7 @@ export const titleForLens = (
photos: Photo[],
explicitCount?: number,
) => [
- 'Shot on',
+ 'Lens:',
formatLensText(lensFromPhoto(photos[0], lens)),
photoQuantityText(explicitCount ?? photos.length),
].join(' ');
@@ -29,7 +29,7 @@ export const shareTextForLens = (
photos: Photo[],
) =>
[
- 'Photos shot on',
+ 'Lens:',
formatLensText(lensFromPhoto(photos[0], lens)),
].join(' ');
diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx
index 1efdeb09..9cffd626 100644
--- a/src/photo/PhotoLarge.tsx
+++ b/src/photo/PhotoLarge.tsx
@@ -295,6 +295,7 @@ export default function PhotoLarge({
lens={lens}
contrast="medium"
prefetch={prefetchRelatedLinks}
+ shortText
/>}
}
{showRecipeContent && recipeTitle &&
diff --git a/src/photo/PhotoLink.tsx b/src/photo/PhotoLink.tsx
index aa5d23a5..e0701cdd 100644
--- a/src/photo/PhotoLink.tsx
+++ b/src/photo/PhotoLink.tsx
@@ -11,16 +11,12 @@ import { clsx } from 'clsx/lite';
export default function PhotoLink({
photo,
- tag,
- camera,
- simulation,
- focal,
- recipe,
scroll,
prefetch,
nextPhotoAnimation,
className,
children,
+ ...categories
}: {
photo?: Photo
scroll?: boolean
@@ -34,7 +30,7 @@ export default function PhotoLink({
return (
photo
? {
if (nextPhotoAnimation) {
diff --git a/src/photo/PhotoPrevNext.tsx b/src/photo/PhotoPrevNext.tsx
index f8a943b2..502ced35 100644
--- a/src/photo/PhotoPrevNext.tsx
+++ b/src/photo/PhotoPrevNext.tsx
@@ -24,11 +24,7 @@ export default function PhotoPrevNext({
photo,
photos = [],
className,
- tag,
- camera,
- simulation,
- focal,
- recipe,
+ ...categories
}: {
photo?: Photo
photos?: Photo[]
@@ -44,42 +40,30 @@ export default function PhotoPrevNext({
const previousPhoto = photo ? getPreviousPhoto(photo, photos) : undefined;
const nextPhoto = photo ? getNextPhoto(photo, photos) : undefined;
+ const pathPrevious = previousPhoto
+ ? pathForPhoto({ photo: previousPhoto, ...categories })
+ : undefined;
+
+ const pathNext = nextPhoto
+ ? pathForPhoto({ photo: nextPhoto, ...categories })
+ : undefined;
+
useEffect(() => {
if (shouldRespondToKeyboardCommands) {
const onKeyUp = (e: KeyboardEvent) => {
switch (e.key.toUpperCase()) {
case 'ARROWLEFT':
case 'J':
- if (previousPhoto) {
+ if (pathPrevious) {
setNextPhotoAnimation?.(ANIMATION_RIGHT);
- router.push(
- pathForPhoto({
- photo: previousPhoto,
- tag,
- camera,
- simulation,
- focal,
- recipe,
- }),
- { scroll: false },
- );
+ router.push(pathPrevious, { scroll: false });
}
break;
case 'ARROWRIGHT':
case 'L':
- if (nextPhoto) {
+ if (pathNext) {
setNextPhotoAnimation?.(ANIMATION_LEFT);
- router.push(
- pathForPhoto({
- photo: nextPhoto,
- tag,
- camera,
- simulation,
- focal,
- recipe,
- }),
- { scroll: false },
- );
+ router.push(pathNext, { scroll: false });
}
break;
};
@@ -91,13 +75,8 @@ export default function PhotoPrevNext({
router,
shouldRespondToKeyboardCommands,
setNextPhotoAnimation,
- previousPhoto,
- nextPhoto,
- tag,
- camera,
- simulation,
- focal,
- recipe,
+ pathPrevious,
+ pathNext,
]);
return (
@@ -111,14 +90,10 @@ export default function PhotoPrevNext({
'items-center sm:items-start',
)}>
@@ -131,14 +106,10 @@ export default function PhotoPrevNext({
/