Merge pull request #105 from sambecker/focal-cmdk

Add focal length to CMD-K menu
This commit is contained in:
Sam Becker 2024-06-05 23:46:40 -05:00 committed by GitHub
commit 668f529ff0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 528 additions and 463 deletions

View File

@ -9,28 +9,28 @@
"analyze": "ANALYZE=true next build"
},
"dependencies": {
"@ai-sdk/openai": "^0.0.18",
"@aws-sdk/client-s3": "3.583.0",
"@aws-sdk/s3-request-presigner": "3.583.0",
"@ai-sdk/openai": "^0.0.21",
"@aws-sdk/client-s3": "3.591.0",
"@aws-sdk/s3-request-presigner": "3.591.0",
"@next/bundle-analyzer": "14.2.3",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.7",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@testing-library/react": "^16.0.0",
"@types/jest": "^29.5.12",
"@types/node": "^20.12.13",
"@types/node": "^20.14.2",
"@types/pg": "^8.11.6",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/parser": "^7.11.0",
"@typescript-eslint/eslint-plugin": "^7.12.0",
"@typescript-eslint/parser": "^7.12.0",
"@upstash/ratelimit": "^1.1.3",
"@vercel/analytics": "^1.3.1",
"@vercel/blob": "^0.23.3",
"@vercel/kv": "^2.0.0",
"@vercel/speed-insights": "^1.0.11",
"ai": "^3.1.21",
"ai": "^3.1.27",
"autoprefixer": "10.4.19",
"camelcase-keys": "^9.1.3",
"clsx": "^2.1.1",
@ -39,14 +39,14 @@
"eslint": "8.57.0",
"eslint-config-next": "14.2.3",
"exifr": "^7.1.3",
"framer-motion": "^11.2.9",
"framer-motion": "^11.2.10",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"nanoid": "^5.0.7",
"next": "^14.2.3",
"next-auth": "5.0.0-beta.18",
"next-themes": "^0.3.0",
"pg": "^8.11.5",
"pg": "^8.12.0",
"postcss": "8.4.38",
"react": "18.3.1",
"react-dom": "18.3.1",
@ -54,7 +54,7 @@
"sharp": "^0.33.4",
"sonner": "^1.4.41",
"swr": "^2.2.5",
"tailwindcss": "3.4.3",
"tailwindcss": "3.4.4",
"ts-exif-parser": "^0.2.2",
"typescript": "5.4.5",
"undici": "^6.18.2",

851
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -91,12 +91,16 @@ export default function CommandKClient({
const isOpenRef = useRef(isOpen);
const [isPending, startTransition] = useTransition();
const [keyPending, setKeyPending] = useState<string>();
const shouldCloseAfterPending = useRef(false);
useEffect(() => {
if (!isPending && shouldCloseAfterPending.current) {
setIsOpen?.(false);
shouldCloseAfterPending.current = false;
if (!isPending) {
setKeyPending(undefined);
if (shouldCloseAfterPending.current) {
setIsOpen?.(false);
shouldCloseAfterPending.current = false;
}
}
}, [isPending, setIsOpen]);
@ -312,7 +316,7 @@ export default function CommandKClient({
onClose={() => setIsOpen?.(false)}
fast
>
<div className={clsx('space-y-1.5', isPending && 'opacity-30')}>
<div className="space-y-1.5">
<div className="relative">
<Command.Input
onChangeCapture={(e) => setQueryLive(e.currentTarget.value)}
@ -324,6 +328,7 @@ export default function CommandKClient({
'focus:border-gray-200 focus:dark:border-gray-800',
'placeholder:text-gray-400/80',
'placeholder:dark:text-gray-700',
isPending && 'opacity-20',
)}
placeholder="Search photos, views, settings ..."
disabled={isPending}
@ -356,6 +361,7 @@ export default function CommandKClient({
heading={<div className={clsx(
'flex items-center',
'px-2',
isPending && 'opacity-20',
)}>
{accessory &&
<div className="w-5">{accessory}</div>}
@ -379,15 +385,17 @@ export default function CommandKClient({
annotationAria,
path,
action,
}) =>
<CommandKItem
key={`${heading} ${label}`}
}) => {
const key = `${heading} ${label}`;
return <CommandKItem
key={key}
label={label}
value={`${heading} ${label}`}
value={key}
keywords={keywords}
onSelect={() => {
if (path) {
startTransition(() => {
setKeyPending(key);
startTransition(async () => {
shouldCloseAfterPending.current = true;
router.push(path, { scroll: true });
});
@ -399,8 +407,10 @@ export default function CommandKClient({
accessory={accessory}
annotation={annotation}
annotationAria={annotationAria}
showSpinner={Boolean(path)}
/>)}
loading={key === keyPending}
disabled={isPending && key !== keyPending}
/>;
})}
</Command.Group>)}
{footer && !queryLive &&
<div className="text-center text-dim pt-3 sm:pt-4">

View File

@ -1,6 +1,6 @@
import { clsx } from 'clsx/lite';
import { Command } from 'cmdk';
import { ReactNode, useState } from 'react';
import { ReactNode } from 'react';
import Spinner from '../Spinner';
export default function CommandKItem({
@ -11,7 +11,8 @@ export default function CommandKItem({
accessory,
annotation,
annotationAria,
showSpinner,
loading,
disabled,
}: {
label: string
value: string
@ -20,10 +21,9 @@ export default function CommandKItem({
accessory?: ReactNode
annotation?: ReactNode
annotationAria?: string
showSpinner?: boolean
loading?: boolean
disabled?: boolean
}) {
const [isLoading, setIsLoading] = useState(false);
return (
<Command.Item
value={value}
@ -32,23 +32,26 @@ export default function CommandKItem({
'px-2',
accessory ? 'py-2' : 'py-1',
'rounded-md cursor-pointer tracking-wide',
'data-[selected=true]:bg-gray-100',
'data-[selected=true]:dark:bg-gray-900/75',
'active:!bg-gray-200/75 active:dark:!bg-gray-800/55',
...loading
? [
'data-[selected=true]:dark:bg-gray-900/50',
'data-[selected=true]:bg-gray-100/50',
] : [
'data-[selected=true]:dark:bg-gray-900/75',
'data-[selected=true]:bg-gray-100',
],
disabled && 'opacity-15',
)}
onSelect={() => {
onSelect?.();
if (showSpinner) {
setIsLoading(true);
}
}}
onSelect={onSelect}
disabled={loading || disabled}
>
<div className="flex items-center gap-2 sm:gap-3">
{accessory}
<span className="grow text-ellipsis truncate">
{label}
</span>
{annotation && !isLoading &&
{annotation && !loading &&
<span
className="text-dim whitespace-nowrap"
aria-label={annotationAria}
@ -57,8 +60,8 @@ export default function CommandKItem({
{annotation}
</span>
</span>}
{isLoading &&
<Spinner color="text" />}
{loading &&
<Spinner color="dim" />}
</div>
</Command.Item>
);

View File

@ -9,6 +9,11 @@ import {
absolutePathForFocalLengthImage,
} from '@/site/paths';
export type FocalLengths = {
focal: number
count: number
}[]
export const getFocalLengthFromString = (focalString?: string) => {
const focal = focalString?.match(/^([0-9]+)mm/)?.[1];
return focal ? parseInt(focal, 10) : 0;

View File

@ -14,6 +14,7 @@ import {
getPhotosNearId,
getPhotosMostRecentUpdate,
getPhotosMeta,
getUniqueFocalLengths,
} from '@/photo/db/query';
import { GetPhotosOptions } from './db';
import { parseCachedPhotoDates, parseCachedPhotosDates } from '@/photo';
@ -37,6 +38,7 @@ const KEY_PHOTO = 'photo';
const KEY_TAGS = 'tags';
const KEY_CAMERAS = 'cameras';
const KEY_FILM_SIMULATIONS = 'film-simulations';
const KEY_FOCAL_LENGTHS = 'focal-lengths';
// Type keys
const KEY_COUNT = 'count';
const KEY_HIDDEN = 'hidden';
@ -196,6 +198,12 @@ export const getUniqueFilmSimulationsCached =
[KEY_PHOTOS, KEY_FILM_SIMULATIONS],
);
export const getUniqueFocalLengthsCached =
unstable_cache(
getUniqueFocalLengths,
[KEY_PHOTOS, KEY_FOCAL_LENGTHS],
);
// No store
export const getPhotosNoStore = (...args: Parameters<typeof getPhotos>) => {

View File

@ -21,6 +21,7 @@ import {
getOrderByFromOptions,
} from '.';
import { getWheresFromOptions } from '.';
import { FocalLengths } from '@/focal';
const createPhotosTable = () =>
sql`
@ -283,6 +284,20 @@ export const getUniqueFilmSimulations = async () =>
})))
, 'getUniqueFilmSimulations');
export const getUniqueFocalLengths = async () =>
safelyQueryPhotos(() => sql`
SELECT DISTINCT focal_length, COUNT(*)
FROM photos
WHERE hidden IS NOT TRUE AND focal_length IS NOT NULL
GROUP BY focal_length
ORDER BY focal_length ASC
`.then(({ rows }): FocalLengths => rows
.map(({ focal_length, count }) => ({
focal: parseInt(focal_length, 10),
count: parseInt(count, 10),
})))
, 'getUniqueFocalLengths');
export const getPhotos = async (options: GetPhotosOptions = {}) =>
safelyQueryPhotos(async () => {
const sql = ['SELECT * FROM photos'];

View File

@ -10,15 +10,18 @@ import {
import {
pathForCamera,
pathForFilmSimulation,
pathForFocalLength,
} from './paths';
import { formatCameraText } from '@/camera';
import { photoQuantityText } from '@/photo';
import { formatCount, formatCountDescriptive } from '@/utility/string';
import { TagsWithMeta } from '@/tag';
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
import { IoMdCamera } from 'react-icons/io';
import { ADMIN_DEBUG_TOOLS_ENABLED, SHOW_FILM_SIMULATIONS } from './config';
import { labelForFilmSimulation } from '@/vendors/fujifilm';
import { getUniqueFocalLengths } from '@/photo/db/query';
import { formatFocalLength } from '@/focal';
import { TbCone } from 'react-icons/tb';
export default async function CommandK() {
const [
@ -26,15 +29,17 @@ export default async function CommandK() {
tags,
cameras,
filmSimulations,
focalLengths,
] = await Promise.all([
getPhotosMetaCached()
.then(({ count }) => count)
.catch(() => 0),
getUniqueTagsCached().catch(() => [] as TagsWithMeta),
getUniqueTagsCached().catch(() => []),
getUniqueCamerasCached().catch(() => []),
SHOW_FILM_SIMULATIONS
? getUniqueFilmSimulationsCached().catch(() => [])
: [],
getUniqueFocalLengths().catch(() => []),
]);
const SECTION_CAMERAS: CommandKSection = {
@ -61,11 +66,25 @@ export default async function CommandK() {
})),
};
const SECTION_FOCAL: CommandKSection = {
heading: 'Focal Lengths',
accessory: <TbCone
className="rotate-[270deg] text-[14px]"
/>,
items: focalLengths.map(({ focal, count }) => ({
label: formatFocalLength(focal)!,
annotation: formatCount(count),
annotationAria: formatCountDescriptive(count),
path: pathForFocalLength(focal),
})),
};
return <CommandKClient
tags={tags}
serverSections={[
SECTION_CAMERAS,
SECTION_FILM,
SECTION_FOCAL,
]}
showDebugTools={ADMIN_DEBUG_TOOLS_ENABLED}
footer={photoQuantityText(count, false)}