Add lenses to sidebar and cmdk

This commit is contained in:
Sam Becker 2025-03-16 17:46:23 -05:00
parent 3b7ec5a6c8
commit dc765ae4e7
9 changed files with 166 additions and 78 deletions

View File

@ -25,8 +25,9 @@ export default async function GridPage() {
const [
photos,
photosCount,
tags,
cameras,
lenses,
tags,
simulations,
recipes,
] = await Promise.all([
@ -41,7 +42,15 @@ export default async function GridPage() {
return (
photos.length > 0
? <PhotoGridPage
{...{ photos, photosCount, tags, cameras, simulations, recipes }}
{...{
photos,
photosCount,
cameras,
lenses,
tags,
simulations,
recipes,
}}
/>
: <PhotosEmptyState />
);

View File

@ -31,8 +31,9 @@ export default async function HomePage() {
const [
photos,
photosCount,
tags,
cameras,
lenses,
tags,
simulations,
recipes,
] = await Promise.all([
@ -43,18 +44,24 @@ export default async function HomePage() {
.catch(() => 0),
...(GRID_HOMEPAGE_ENABLED
? getPhotoSidebarData()
: [[], [], [], []]),
: [[], [], [], [], []]),
]);
return (
photos.length > 0
? GRID_HOMEPAGE_ENABLED
? <PhotoGridPage
{...{ photos, photosCount, tags, cameras, simulations, recipes }}
/>
: <PhotoFeedPage
{...{ photos, photosCount }}
{...{
photos,
photosCount,
cameras,
lenses,
tags,
simulations,
recipes,
}}
/>
: <PhotoFeedPage {...{ photos, photosCount }} />
: <PhotosEmptyState />
);
}

View File

@ -3,6 +3,7 @@ import {
getPhotosMetaCached,
getUniqueCamerasCached,
getUniqueFilmSimulationsCached,
getUniqueLensesCached,
getUniqueRecipesCached,
getUniqueTagsCached,
} from '@/photo/cache';
@ -17,8 +18,9 @@ import { getUniqueFocalLengths } from '@/photo/db/query';
export default async function CommandK() {
const [
count,
tags,
cameras,
lenses,
tags,
recipes,
filmSimulations,
focalLengths,
@ -26,8 +28,9 @@ export default async function CommandK() {
getPhotosMetaCached()
.then(({ count }) => count)
.catch(() => 0),
getUniqueTagsCached().catch(() => []),
getUniqueCamerasCached().catch(() => []),
getUniqueLensesCached().catch(() => []),
getUniqueTagsCached().catch(() => []),
SHOW_RECIPES
? getUniqueRecipesCached().catch(() => [])
: [],
@ -38,8 +41,9 @@ export default async function CommandK() {
]);
return <CommandKClient
tags={tags}
cameras={cameras}
lenses={lenses}
tags={tags}
simulations={filmSimulations}
recipes={recipes}
focalLengths={focalLengths}

View File

@ -26,6 +26,7 @@ import {
pathForCamera,
pathForFilmSimulation,
pathForFocalLength,
pathForLens,
pathForPhoto,
pathForRecipe,
pathForTag,
@ -44,13 +45,11 @@ import { RiToolsFill } from 'react-icons/ri';
import { BiLockAlt, BiSolidUser } from 'react-icons/bi';
import { HiDocumentText } from 'react-icons/hi';
import { signOutAction } from '@/auth/actions';
import { TbChecklist, TbCone, TbPhoto } from 'react-icons/tb';
import { getKeywordsForPhoto, titleForPhoto } from '@/photo';
import PhotoDate from '@/photo/PhotoDate';
import PhotoSmall from '@/photo/PhotoSmall';
import { FaCheck } from 'react-icons/fa6';
import { addHiddenToTags, formatTag } from '@/tag';
import { FaTag } from 'react-icons/fa';
import { formatCount, formatCountDescriptive } from '@/utility/string';
import CommandKItem from './CommandKItem';
import { CATEGORY_VISIBILITY, GRID_HOMEPAGE_ENABLED } from '@/app/config';
@ -59,11 +58,17 @@ import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
import InsightsIndicatorDot from '@/admin/insights/InsightsIndicatorDot';
import { PhotoSetCategories } from '@/photo/set';
import { formatCameraText } from '@/camera';
import { IoMdCamera } from 'react-icons/io';
import { labelForFilmSimulation } from '@/platforms/fujifilm/simulation';
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
import { formatFocalLength } from '@/focal';
import { formatRecipe } from '@/recipe';
import IconLens from '../icons/IconLens';
import { formatLensText } from '@/lens';
import IconTag from '../icons/IconTag';
import IconCamera from '../icons/IconCamera';
import IconPhoto from '../icons/IconPhoto';
import IconRecipe from '../icons/IconRecipe';
import IconFocalLength from '../icons/IconFocalLength';
import IconFilmSimulation from '../icons/IconFilmSimulation';
const DIALOG_TITLE = 'Global Command-K Menu';
const DIALOG_DESCRIPTION = 'For searching photos, views, and settings';
@ -98,8 +103,9 @@ const renderToggle = (
});
export default function CommandKClient({
tags,
cameras,
lenses,
tags,
recipes,
simulations,
focalLengths,
@ -198,7 +204,7 @@ export default function CommandKClient({
setQueriedSections(photos.length > 0
? [{
heading: 'Photos',
accessory: <TbPhoto size={14} />,
accessory: <IconPhoto size={14} />,
items: photos.map(photo => ({
label: titleForPhoto(photo),
keywords: getKeywordsForPhoto(photo),
@ -250,10 +256,30 @@ export default function CommandKClient({
CATEGORY_VISIBILITY
.map(category => {
switch (category) {
case 'cameras': return {
heading: 'Cameras',
accessory: <IconCamera size={14} />,
items: cameras.map(({ camera, count }) => ({
label: formatCameraText(camera),
annotation: formatCount(count),
annotationAria: formatCountDescriptive(count),
path: pathForCamera(camera),
})),
};
case 'lenses': return {
heading: 'Lenses',
accessory: <IconLens size={14} className="translate-y-[0.5px]" />,
items: lenses.map(({ lens, count }) => ({
label: formatLensText(lens, 'medium'),
annotation: formatCount(count),
annotationAria: formatCountDescriptive(count),
path: pathForLens(lens),
})),
};
case 'tags': return {
heading: 'Tags',
accessory: <FaTag
size={10}
accessory: <IconTag
size={13}
className="translate-x-[1px] translate-y-[0.75px]"
/>,
items: tagsIncludingHidden.map(({ tag, count }) => ({
@ -263,19 +289,9 @@ export default function CommandKClient({
path: pathForTag(tag),
})),
};
case 'cameras': return {
heading: 'Cameras',
accessory: <IoMdCamera />,
items: cameras.map(({ camera, count }) => ({
label: formatCameraText(camera),
annotation: formatCount(count),
annotationAria: formatCountDescriptive(count),
path: pathForCamera(camera),
})),
};
case 'recipes': return {
heading: 'Recipes',
accessory: <TbChecklist
accessory: <IconRecipe
size={15}
className="translate-x-[-1px]"
/>,
@ -288,9 +304,7 @@ export default function CommandKClient({
};
case 'films': return {
heading: 'Film Simulations',
accessory: <span className="w-3">
<PhotoFilmSimulationIcon className="translate-y-[0.5px]" />
</span>,
accessory: <IconFilmSimulation size={14} />,
items: simulations.map(({ simulation, count }) => ({
label: labelForFilmSimulation(simulation).medium,
annotation: formatCount(count),
@ -300,9 +314,7 @@ export default function CommandKClient({
};
case 'focal-lengths': return {
heading: 'Focal Lengths',
accessory: <TbCone
className="rotate-[270deg] text-[14px]"
/>,
accessory: <IconFocalLength className="text-[14px]" />,
items: focalLengths.map(({ focal, count }) => ({
label: formatFocalLength(focal)!,
annotation: formatCount(count),
@ -313,7 +325,7 @@ export default function CommandKClient({
}
})
.filter(Boolean) as CommandKSection[]
, [tagsIncludingHidden, cameras, recipes, simulations, focalLengths]);
, [tagsIncludingHidden, cameras, lenses, recipes, simulations, focalLengths]);
const clientSections: CommandKSection[] = [{
heading: 'Theme',

View File

@ -41,6 +41,15 @@ export const getLensFromParams = ({
model: parameterize(model),
});
export const sortLensesWithCount = (
a: LensWithCount,
b: LensWithCount,
) => {
const aText = formatLensText(a.lens);
const bText = formatLensText(b.lens);
return aText.localeCompare(bText);
};
export const lensFromPhoto = (
photo: Photo | undefined,
fallback?: Lens,
@ -55,15 +64,24 @@ const isLensMakeApple = (make?: string) =>
export const isLensApple = ({ make }: Lens) =>
isLensMakeApple(make);
const formatAppleLensText = (model: string) => {
if (model.includes('front')) {
return 'Front Camera';
} else {
if (model.includes('15 Pro')) {
if (model.includes('f/2.2')) { return 'Wide Camera'; }
if (model.includes('f/1.78')) { return 'Main Camera'; }
if (model.includes('f/2.8')) { return 'Telephoto Camera'; }
}
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;
};
@ -84,13 +102,16 @@ export const formatLensText = (
);
const model = isLensMakeApple(make)
? formatAppleLensText(modelRaw)
? formatAppleLensText(modelRaw, length === 'medium')
: modelRaw;
switch (length) {
case 'long':
case 'medium':
return `${make} ${model}`;
case 'medium':
return doesModelStartWithMake
? model.replace(makeSimple, '').trim()
: model;
case 'short':
return doesModelStartWithMake
? model.replace(makeSimple, '').trim()

View File

@ -11,19 +11,22 @@ import { useEffect } from 'react';
import { useAppState } from '@/state/AppState';
import clsx from 'clsx/lite';
import { Recipes } from '@/recipe';
import { Lenses } from '@/lens';
export default function PhotoGridPage({
photos,
photosCount,
tags,
cameras,
lenses,
tags,
simulations,
recipes,
}: {
photos: Photo[]
photosCount: number
tags: Tags
cameras: Cameras
lenses: Lenses
tags: Tags
simulations: FilmSimulations
recipes: Recipes
}) {
@ -65,6 +68,7 @@ export default function PhotoGridPage({
<PhotoGridSidebar {...{
tags,
cameras,
lenses,
simulations,
recipes,
photosCount,

View File

@ -24,16 +24,21 @@ import IconCamera from '@/components/icons/IconCamera';
import IconRecipe from '@/components/icons/IconRecipe';
import IconTag from '@/components/icons/IconTag';
import IconFilmSimulation from '@/components/icons/IconFilmSimulation';
import IconLens from '@/components/icons/IconLens';
import { Lenses, sortLensesWithCount } from '@/lens';
import PhotoLens from '@/lens/PhotoLens';
export default function PhotoGridSidebar({
tags,
cameras,
lenses,
tags,
simulations,
recipes,
photosCount,
photosDateRange,
}: {
tags: Tags
lenses: Lenses
cameras: Cameras
simulations: FilmSimulations
recipes: Recipes
@ -48,6 +53,47 @@ export default function PhotoGridSidebar({
addHiddenToTags(tags, photosCountHidden)
, [tags, photosCountHidden]);
const camerasContent = cameras.length > 0
? <HeaderList
key="cameras"
title="Cameras"
icon={<IconCamera size={15} />}
items={cameras
.sort(sortCamerasWithCount)
.map(({ cameraKey, camera, count }) =>
<PhotoCamera
key={cameraKey}
camera={camera}
type="text-only"
countOnHover={count}
prefetch={false}
contrast="low"
hideAppleIcon
badged
/>)}
/>
: null;
const lensesContent = lenses.length > 0
? <HeaderList
key="lenses"
title="Lenses"
icon={<IconLens size={15} />}
items={lenses
.sort(sortLensesWithCount)
.map(({ lensKey, lens, count }) =>
<PhotoLens
key={lensKey}
lens={lens}
type="text-only"
countOnHover={count}
prefetch={false}
contrast="low"
badged
/>)}
/>
: null;
const tagsContent = tags.length > 0
? <HeaderList
key="tags"
@ -91,27 +137,6 @@ export default function PhotoGridSidebar({
/>
: null;
const camerasContent = cameras.length > 0
? <HeaderList
key="cameras"
title="Cameras"
icon={<IconCamera size={15} />}
items={cameras
.sort(sortCamerasWithCount)
.map(({ cameraKey, camera, count }) =>
<PhotoCamera
key={cameraKey}
camera={camera}
type="text-only"
countOnHover={count}
prefetch={false}
contrast="low"
hideAppleIcon
badged
/>)}
/>
: null;
const recipesContent = recipes.length > 0
? <HeaderList
key="recipes"
@ -183,8 +208,9 @@ export default function PhotoGridSidebar({
/>}
{CATEGORY_VISIBILITY.map(category => {
switch (category) {
case 'cameras': return camerasContent;
case 'tags': return tagsContent;
case 'cameras': return camerasContent;
case 'lenses': return lensesContent;
case 'recipes': return recipesContent;
case 'films': return filmsContent;
}

View File

@ -1,21 +1,24 @@
import {
getUniqueCamerasCached,
getUniqueFilmSimulationsCached,
getUniqueLensesCached,
getUniqueRecipesCached,
getUniqueTagsCached,
} from '@/photo/cache';
import {
getUniqueCameras,
getUniqueFilmSimulations,
getUniqueLenses,
getUniqueRecipes,
getUniqueTags,
} from '@/photo/db/query';
import { SHOW_FILM_SIMULATIONS, SHOW_RECIPES } from '@/app/config';
import { SHOW_FILM_SIMULATIONS, SHOW_LENSES, SHOW_RECIPES } from '@/app/config';
import { sortTagsObject } from '@/tag';
export const getPhotoSidebarData = () => [
getUniqueTags().then(sortTagsObject).catch(() => []),
getUniqueCameras().catch(() => []),
SHOW_LENSES ? getUniqueLenses().catch(() => []) : [],
getUniqueTags().then(sortTagsObject).catch(() => []),
SHOW_FILM_SIMULATIONS
? getUniqueFilmSimulations().catch(() => [])
: [],
@ -25,8 +28,9 @@ export const getPhotoSidebarData = () => [
] as const;
export const getPhotoSidebarDataCached = () => [
getUniqueTagsCached().then(sortTagsObject),
getUniqueCamerasCached(),
SHOW_LENSES ? getUniqueLensesCached() : [],
getUniqueTagsCached().then(sortTagsObject),
SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [],
SHOW_RECIPES ? getUniqueRecipesCached() : [],
] as const;

View File

@ -2,7 +2,7 @@ import { Photo } from '.';
import { Camera, Cameras } from '@/camera';
import { PhotoDateRange } from '.';
import { FilmSimulation, FilmSimulations } from '@/simulation';
import { Lens } from '@/lens';
import { Lens, Lenses } from '@/lens';
import { Tags } from '@/tag';
import { FocalLengths } from '@/focal';
import { Recipes } from '@/recipe';
@ -43,8 +43,9 @@ export interface PhotoSetCategory {
}
export interface PhotoSetCategories {
tags: Tags
cameras: Cameras
lenses: Lenses
tags: Tags
recipes: Recipes
simulations: FilmSimulations
focalLengths: FocalLengths